Dojo challenge #36 - Shell Escape winners and writeup

November 8, 2024

The winners of our latest monthly Dojo CTF challenge

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 and cmd) --> 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 —————