📜 Description du challenge
A friend of yours has created a web application that allows you to check the availability of your locally hosted services. He assured you that it is secure and even allowed you to run it as a test user!
Prove him wrong by reading the flag.txt file on the server.
~ The flag can be found in the file: /tmp/flag.txt
🕵️ Proof of Concept
Here, we are dealing with a Python backend that accepts two input parameters, TOKEN
and CMD
. The purpose of this application is to perform a ping command to a given IP address (CMD
). We quickly understand that our goal will be to perform a command injection.
Source Code Analysis
General Analysis
We notice that the payloads sent are URL-encoded. The TOKEN
payload is then used to perform a SQL query, the result of which determines the user of the application. The Command
object is then initialized with the obtained user and the CMD
parameter payload, and the Run
function of the object is called.
cmd = unquote("")
token = unquote("")
# Get user that holds the given token
r = cursor.execute('SELECT username FROM users WHERE token LIKE ?', (token,))
try:
user = r.fetchone()[0]
except:
user = "test"
command = Command(cmd, user)
try:
result = command.Run()
except Exception as e:
result = f'command was not executed, error : {e}'
Analysis of the Command CLASS
The Command object initializes with the two parameters mentioned above, both of which are strings. The Run function calls the PreProd_Sanitize
function if the user is “dev,” or Prod_Sanitize
if it’s any other user. It then executes a command using subprocess.run
and the value of the CMD parameter.
def Run(self):
if self.user == "dev":
cmd_sanitize = self.PreProd_Sanitize(self.command)
else:
cmd_sanitize = self.Prod_Sanitize(self.command)
# At the moment we don't have internet access.
# We should only ping localhost to avoid server timeout
result = subprocess.run(["/bin/ash", "-c", f"ping -c 1 {cmd_sanitize}"], capture_output=True, text=True)
if result.returncode == 0:
return result.stdout
else:
return result.stderr
Prod_Sanitize
calls shlex.quote(s)
to return a shell-escaped version of the string s
. Upon reviewing documentation, we understand that there are no known ways to bypass or exploit this function.
def Prod_Sanitize(self, s:str) -> str:
return shlex.quote(s)
PreProd_Sanitize
on the other hand, is a manual function built by the developer to sanitize the value of the CMD
parameter.
def PreProd_Sanitize(self, s:str) -> str:
"""My homemade secure sanitize function"""
if not s:
return "''"
if re.search(r'[a-zA-Z_*^@%+=:,./-]', s) is None:
return s
return "'" + s.replace("'", "'\"'\"'") + "'"****
In conclusion, we deduce that we must achieve the following bypasses to exploit a command injection vulnerability:
- Authenticate as the developer to ensure that our
CMD
payload passes through thePreProd_Sanitize
function. - Find a way to bypass the
PreProd_Sanitize
function to perform a command injection.
Injecting Wildcards in the SQL LIKE Clause
After reviewing the functions and libraries used to perform the SQL query, we see that it is relatively secure and that classic SQL injection does not seem possible at first glance.
The peculiarity of this SQL query is that it contains a LIKE clause after a WHERE condition. It suffices to check if there is a way to exploit this LIKE clause, even if the input we control is a string.
And bingo! After some research, we find a technique known as “SQL LIKE clauses wildcard injection.” In SQL, we have two types of wildcards:
%
matches any sequence of zero or more characters._
matches any single character. These wildcards can also be used within a string.
In other words, if we manage to inject a wildcard into our string, we will be able to retrieve the developer’s token. Looking at the source code in Settings
, we see that the database contains only two users, test
and dev
, and only dev
has a token.
We simply need to inject a wildcard character present in the token string to retrieve it. In this case, the simple payload %a%
, which matches what was described above, allows us to authenticate as dev.
Command Injection and Filter Bypass
Now that we are authenticated as dev
, we can move on to the next step, which is bypassing the PreProd_Sanitize
filter to achieve RCE.
Let’s analyze the filter. If our payload is empty, the value of CMD
is null. The second condition includes a regex that states if our payload does not contain:
- Alphabetical characters (uppercase or lowercase)
- Or the following characters:
_*^@%+=:,./-
Then it will be returned.
If the second condition is not met, the function surrounds our payload with single quotes and replaces any single quotes in our payload with `’"’"'.
After extensive research and local testing, we notice that if our payload does not pass the second condition, achieving RCE will not be possible. No known bypasses exist for the s.replace("'", "'\"'\"'")
function, and our payload is sufficiently escaped to prevent code injection. EReviewing the [Snyk article][https://snyk.io/fr/blog/command-injection-python-prevention-examples/] on Python command injection examples, particularly those using format strings, reveals that the only way to achieve RCE is for our payload to be concatenated directly in subprocess.run(["/bin/ash", "-c", f"ping -c 1 {cmd_sanitize}"], capture_output=True, text=True)
. Thus, we focus on bypassing the regex in the second condition.
The regex [a-zA-Z_*^@%+=:,./-]
allows us to use operators like ;
, &&
, etc. Sending a semicolon (;
), for example, should bypass the filter. Indeed, this results in a command error we haven’t seen before:
This confirms the possibility of RCE on our target. Now, we need to construct a command injection payload without alphabetical or special characters like _*^@%+=:,./-
.
To do so, we suspect a specific encoding type might help. Is there an encoding type using only numerical characters and interpretable by Linux (bash, ash, or others)? Yes, octal. We simply need to learn how to execute Linux commands using octal encoding.
The answer is $''
. The $'...'
mechanism is a quote form in certain shells like bash to handle escape sequences. When using $'...'
, the shell interprets the escape sequences inside the quotes as special characters or octal/hexadecimal representations. For instance, $'\151\144'
is interpreted as the string id
. Example:
Now we can construct our final payload and obtain the flag:
cat flag.txt
–>$'\143\141\164' $'\146\154\141\147\056\164\170\164'
Notes: Be careful not to encode spaces between the command and its argument. For example, it would be interpreted as part of the string, and a command like cat file
would be treated as cat_file
.
Here’s the flag: FLAG{W3lc0me_T0_Th3_Oth3r_S1de!}
Thanks to owne and Brumens for this very educational challenge!
📚 References
- https://www.pentester.es/like-sqli/
- https://github.blog/engineering/user-experience/like-injection/
- https://cqr.company/web-vulnerabilities/sql-wildcard-injection/
- https://snyk.io/fr/blog/command-injection-python-prevention-examples/
- PortSwigger - OS Command Injection
- Arbitrary code execution
- CWE-20: Improper Input Validation
- CWE-94: Improper Control of Generation of Code (‘Code Injection’)