The Dojo Challenge, Shell escape, asked participants to exploit a broken access control in a SQL statement combined with a command injection vulnerability to achieve a remote code execution (RCE) to capture the flag.
π‘ Want to create your own monthly Dojo challenge? Send us a message on Twitter!
The winners
Congrats to zyp3, HannanHaseeb and nomish_ for the best write-ups π₯³
The swag is on its way! π
Subscribe to our Twitter and/or LinkedIn feeds to be notified of upcoming challenges.
Read on to find out how one of the winners managed to solve the challenge.
TALKIE PWNII #1: VIDEO CHALLENGE WRITE-UP
OVERALL BEST WRITE-UP
We want to thank everyone who participated and reported to the Dojo challenge. Below is the best write-up overall:
βββββ START OF zyp3βs REPORT βββββ
Description
OS Command Injection is a security vulnerability that occurs when an attacker is able to execute arbitrary operating system commands on a server. This typically occurs when an application improperly sanitizes user input, allowing malicious commands to be injected into dangerous function or bad functionality implementation. This vulnerability can lead to unauthorized access, data theft, or full system compromise if exploited.
Exploitation
In this challenge we are presented with a web application that allow us to check for the availability of our local hosted services. The application waits for the user to send a token along with a command argument. The token
argument is used to perform a sort of authorization access and the cmd
argument allows the user to specify the IP of the host he wants to check the availability of.
Code analysis - PART 1: User input
If we inspect the source code and the different components on the network processing our input, we have the following things :
- URL Encoding (for both
token
andcmd
) --> This one is classic and we do not really need to think about it here - WAF Blacklist --> This will blacklist the
flag.txt
keyword, but as we will see, we do not need to worry about this blacklist either. - Finally the python code handling our input :
cmd = INPUT
token = INPUT
This simply do the reverse of the URL encoding --> URL decoding.
Nothing very special for now. But things will get more complex in the following parts
Code analysis - PART 2: 'Command' class
When examining the source code, the interesting class is the Command
class. This class mainly gives the functionality to execute the ping
command as we can see here :
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
Our focus instantly goes on the cmd_sanitize
variable. Our goal is to see if we can inject arbitrary shell commands within it in order to exploit an OS command injection vulnerability. However, as we will see, this parameter is protected (or should I say : almost...)
This variable is coming directly from the cmd
argument the user is able to supply. However, before getting into the ping
command, it is sanitized by "one" function that has two different implementations depending on the user we are executing the code with :
def Run(self):
if self.user == "dev":
cmd_sanitize = self.PreProd_Sanitize(self.command)
else:
cmd_sanitize = self.Prod_Sanitize(self.command)
If we are the user dev
then the PreProd_Sanitize
method is used, otherwise the Prod_Sanitize
function is used.
The Prod_Sanitize
is safe, because it is using the shlex.quote
function. However, the PreProd_Sanitize
is a custom filter implemented by the developer who made the app. But before analyzing this, we must determine how to get to this function. In other words, we must find a way to fill the user
property of the Command
class with the 'dev'
string.
Code analysis - PART 3: Let me be the 'dev' I just want to 'test'...
In order to be able to reach the (hypothetical) vulnerable function PreProd_Sanitize
, we need to supply the correct value to the user
property of the Command
class. The code that is doing all of this is the following :
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}'
Okay, we have quite a few things here. First, we have a request to a database, which is selecting all the username
in the users
table that validates the condition WHERE token LIKE ?
.
The result is then used with the fetchone
function to get the first row of the database. Then it gets the index 0
to get the first element of the set returned (basically first column) and use this value to fill the user
variable. However, if nothing is returned by the database, the user
value is filled in with the test
value.
With all of this, we may already infer something. The only way to get the dev
value, is to get it from the database. But can we do that ?
The flaw resides in the LIKE
operator. With this operator, the %
character stands for an unknown string of 0 or more characters. Here are some examples :
%a --> Matches 12345JKLa
a% --> Matches a12345JKL
And if we combine the two examples above :
%a% --> Matches {ANYTHING}a{ANYTHING}
This means that we can match an arbitrary string, as long as the "middle" character is within it.
With this in mind, and based on the name of the variable on which is made the SQL "filter" (token
), we can suppose that the token
for the user will look like a long string composed of random alpha-numerical characters such as
ec11144cabad434ca4e1e9474bf061d8
Now that we know all of this, we can try the following idea :
If the database holds the users, the dev
user is very likely to be in it. Furthermore, since it is the developer, it is likely to be in the first entry of the database since it must have been created first. What we will try then, is to supply the payload %a%
in the token
parameter in order to make the SQL request return the dev
user.
Let's try this :
If we put the following standard values, we execute the command as the test
user.
However, with the correct payload, we execute the command as dev
:
We finally found the way to get this dev
user. This means that our cmd
argument is going through the PreProd_Sanitize
function
Code analysis - PART 4: Bypass the command filter
This is the final part that will show how arbitrary commands can be injected and executed.
The PreProd_Sanitize
function is the following one :
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("'", "'\"'\"'") + "'"
The interesting part is the second if
. Basically, if we put any letter, or special characters within this set : _*^@%+=:,./-
, the code will put a "filter" on single quotes with the last line that will enclose our payload within a single quoted strings, with escaped single quotes --> There's no way we are getting out of this.
return "'" + s.replace("'", "'\"'\"'") + "'"
However, if our payload is not matched by the regex, it is remaining the same.
if re.search(r'[a-zA-Z_*^@%+=:,./-]', s) is None:
return s
But this means that we must perform command injection with no letters. Well, let's do that then ! (Can we ?)
Let's begin with a basic payload, a single quote : '
Here is the result :
We get a shell error. This is encouraging and means that we are most likely manipulating the command. But if we put any letter in the payload, we match the regex and our payload is sanitized and enclosed with single quotes.
Payload 'a
:
After some research, I stumbled upon the ANSI-C notation. The ANSI-C notation is the following one :
$'β¦'
This syntax allows to use escape sequences and several encoding inside the quoted string. And among these encoding, the octal
encoding is compatible with this feature. Here is a simple test in my local shell to trigger the ls command :
The value 154
and 163
are the octal
representation of the l
and s
character, and as we can see, with the ANSI-C notation, it is executing the command ls
. We just found our way to write command without letters, and thankfully, $
\
and '
characters are not within the regex, we can hence use them in our payload.
With this in mind, the following payload should execute the ls
command on the remote server just after the ping
command (Note the &
character to run the ls
command right after the ping
command):
& $'\154\163'
Great, this means we can run any command we want on the remote server. Let's dump the /tmp/flag.txt flag with the command cat /tmp/flag.txt
which, converted to octal, is the following :
cat = $'\143\141\164'
/tmp/flag.txt = $'\57\164\155\160\57\146\154\141\147\56\164\170\164'
Final payload :
& $'\143\141\164' $'\57\164\155\160\57\146\154\141\147\56\164\170\164'
We finally got the content of the file :
PoC
To summarize all of this, here are the key elements to exploit the vulnerability :
-> token
: Must be filled with the value %a%
-> cmd
: Must be filled with the value & $'\143\141\164' $'\57\164\155\160\57\146\154\141\147\56\164\170\164'
Risk
OS Command Injection poses a severe risk to system security, as it enables attackers to execute unauthorized operating system commands directly on the server. This level of access can allow attackers to manipulate files, retrieve sensitive data, escalate privileges, and even take full control of the system. Since the injected commands run with the application's privileges, attackers can potentially bypass authentication, exfiltrate data, and disrupt services.
If left unmitigated, OS Command Injection vulnerabilities can lead to data breaches, service downtime, and compromise of critical infrastructure, resulting in significant financial and reputational damage.
Remediation
To fully correct the vulnerability, two corrections must be applied.
First, on the SQL request, the LIKE
operator should be replaced by an "equal" sign in order to match the exact token. This way, only a user knowing the token could use the dev
user :
Even if this first fix would correct this vulnerability in this scenario, it is better to also correct the vulnerability in the Command
class. The PreProd_Sanitize
function should be removed, and only the Prod_Sanitize
that is using the shlex.quote
function should be used.
Furthermore, it is important to note that using system commands in such an application is always dangerous and the risks related to this usage should be carefully taken into considerations.
βββββ END OF zyp3βs REPORT βββββ