Skip to content

Instantly share code, notes, and snippets.

@alexbrasetvik
Last active September 7, 2023 05:22
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexbrasetvik/7b0aad575d7ce650fb3486b8857a8588 to your computer and use it in GitHub Desktop.
Save alexbrasetvik/7b0aad575d7ce650fb3486b8857a8588 to your computer and use it in GitHub Desktop.
Demo of two of Javascript's sharp edges
{
"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 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