-
-
Save alexbrasetvik/7b0aad575d7ce650fb3486b8857a8588 to your computer and use it in GitHub Desktop.
Demo of two of Javascript's sharp edges
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "sharp-edges", | |
"version": "1.0.0", | |
"description": "This sample code has intentional serious security holes", | |
"main": "unsafely-demo-two-RCEs.js", | |
"scripts": { | |
"start": "node unsafely-demo-two-RCEs.js" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* This is an intentionally unsafe web-app. There are two RCEs hiding in it. | |
It binds to localhost:3000 for 5 minutes before it stops itself. (Two RCEs, you know..) | |
It's a very dummy skeleton app that tries to frame the bugs in a reasonably | |
realistic scenario, though the app is clearly very incomplete. | |
*/ | |
const http = require('http'); | |
// Imagine that we look up something in an object. | |
const searchProfiles = { | |
"samples": { | |
// In this case we might be building something where a certain index has | |
// some canned search requests, which our API will look up and use | |
"recent": {"query": {"range": {"@timestamp": {"gte": "24h"}}}} | |
}, | |
// Different index has different profiles. Nothing weird with nested objects. | |
"logs": { | |
// Returning callables for extra flexibility is quite common. | |
"since": function (params) { | |
return {"query": {"range": {"@timestamp": {"gte": params }}}} | |
}, | |
}, | |
}; | |
// Simple micro-templating based on https://johnresig.com/blog/javascript-micro-templating/ | |
// E.g. template('Hello, <%= name %>', {"sourceURL": "my-template.html"})({"name": "World"}) | |
var _template_cache = {}; | |
function template(str, options){ | |
// Figure out if we're getting a template, or if we need to | |
// load the template - and be sure to cache the result. | |
options = options || {} | |
var maybeSourceURL = options.sourceURL ? '//# sourceURL=' + options.sourceURL : ''; | |
if(_template_cache[str]) { | |
return _template_cache[str]; | |
} | |
var fn = _template_cache[str] = ( | |
// Generate a reusable function that will serve as a template | |
// generator (and which will be cached). | |
new Function("obj", maybeSourceURL + | |
"var p=[],print=function(){p.push.apply(p,arguments);};" + | |
// Introduce the data as local variables using with(){} | |
"with(obj){p.push('" + | |
// Convert the template into pure JavaScript | |
str | |
.replace(/[\r\t\n]/g, " ") | |
.split("<%").join("\t") | |
.replace(/((^|%>)[^\t]*)'/g, "$1\r") | |
.replace(/\t=(.*?)%>/g, "',$1,'") | |
.split("\t").join("');") | |
.split("%>").join("p.push('") | |
.split("\r").join("\\'") | |
+ "');}return p.join('');") | |
) | |
return fn; | |
}; | |
/* A very basic non-malicious request looks like this: | |
curl localhost:3000 -d $'{"index": "samples", "profile": "recent", "sortBy": "latest", "sortOptions": {}}' | |
{"results":[{"title":"Just a dummy","body":"Something something quick brown fox. Dogs!"}],"prettyResults":["<h1>Just a dummy</h1><p>Something something quick brown fox. Dogs!</p>"]} | |
There are two different approaches to RCE-ing this app. Good luck. | |
*/ | |
http.createServer(function (req, res) { | |
// Canned sorts with an alias | |
const sorts = { | |
"latest": {"@timestamp": "desc"}, | |
"price": {"price": {"order": "asc", "mode": "min"}} | |
}; | |
let body = []; | |
req.on('data', chunk => { body.push(chunk) }); | |
req.on('end', () => { | |
res.statusCode = 400; // Err-ing unless things look good. | |
try { | |
let request = JSON.parse(body); | |
console.log("INFO: Got a request:", request); | |
// Look up the profile for the specified index. | |
let profilesForIndex = searchProfiles[request.index]; | |
let searchRequest = profilesForIndex && profilesForIndex[request.profile]; | |
if(!searchRequest) { res.write("unknown profile"); return; } | |
// The profiles could be callables, when a request isn't a trivial canned template. | |
// This is coming straight from the config, and not from the JSON payload, but it's not | |
// like JSON.parse will produce functions, so this should be safe. | |
// Functions returning functions is pretty common (factory factories!), so handle that. | |
while(typeof searchRequest === 'function') { | |
searchRequest = searchRequest(request.params); | |
} | |
// If the request has a sortBy, base our sorting on that. | |
// If there's sortOptions, override any sorting with that. | |
if(request.sortBy) { | |
// Pick a canned sort | |
var sortConfig = sorts[request.sortBy]; | |
if(! sortConfig) { res.write("unknown sort config"); return } | |
if(request.sortOptions) { | |
Object.assign(sortConfig, request.sortOptions) | |
} | |
searchRequest.sort = sortConfig; | |
} | |
// TODO: Point to actual Elasticsearch server and use the query we've made. | |
// The narrative of the above code is of course that we're making up some kind of | |
// ES query that we want to process. It's not necessary to demonstrate the bugs, | |
// so we just assume some results here. | |
let results = [{ | |
"title": "Just a dummy", | |
"body": "Something something quick brown fox. Dogs!" | |
}], | |
// The template is pretty simple and static for now | |
resultTemplate = template('<h1><%= title %></h1><p><%= body %></p>'); | |
let response = { | |
"results": results, | |
"prettyResults": results.map(result => { | |
return resultTemplate(result); | |
}) | |
}; | |
res.statusCode = 200; | |
res.setHeader('Content-Type', 'application/json'); | |
res.write(JSON.stringify(response)); | |
} catch(err){ | |
console.log("err: ", err); | |
res.write("bad request m8"); | |
} finally { | |
res.end(); | |
} | |
}) | |
// This app is dangerous! Binding to localhost isn't as safe as you might think, so don't leave | |
// this app running for long. https://medium.com/@brannondorsey/attacking-private-networks-from-the-internet-with-dns-rebinding-ea7098a2d325 | |
}).listen(3000, '127.0.0.1'); | |
setTimeout(function() { | |
// Two RCEs in this thing. Really, don't start it and forget about it. | |
console.log("Goodbye! I'm not safe to keep running"); | |
process.exit() | |
}, 60*5*1000); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment