Welcome to 2021! The future is today, and it is disappointing.
It is well known that any sane person, from time to time, opens the devtools' console in a browser of his choice to start typing random JavaScript. At least this is what I do, though not as often as I used to.
The goal is unclear, as most times I do not even think or keep track of what I’m trying. You know, I like to think of myself as some sort of JavazzScript improviser… The point is that from time to time you end up with funny snippets like this one:
lol = 'javascript:alert(1)';
({lol:location}=this);
Which are often as surprising as useless (they become obvious once you understand what it’s going on). Let’s say that what I look for are unexpected ways of executing alert(1)
, and it turns out that after some time you end up with a nice bag of tricks to exploit real stuff, or if you’re lucky to find some browser bugs.
Good CTFs are sources of inspiration
I had not done this for a while, but after hxp’s ctf (the only CTF I played this year :S) my curiosity awaken. There was a XSS challenge that consisted of exploiting hackmd.io, the challenge was rated “hard”, but I guess the only difficulty was to not feel overwhelmed by the large attack surface. You can see some solutions here: https://github.com/hackmdio/codimd/issues?q=xss.
My process was first to identify a feature with interesting behavior, and then exploit it. Easy. I soon ran into vega (a nice data visualization framework), which it turns out has an embedded JS parser and interpreter to run user-supplied code. The feature was promising, and after years of seeing failed attempts of safely rewriting JS I was confident that I could break it.
After some black-box testing (unfortunately I didn’t see the source until a few days later), I found a parsing mismatch between vega’s parser and JavaScript that allowed me to inject arbitrary code. For short, vega parses the user input, builds a sanitized AST, and generates sandboxed code that is passed to eval
. The bug consisted in tricking vega to interpret a comment as a division by regexp literal. From this it was easy to get XSS.
They quickly fixed the bug, although not its root cause, which means that I could easily bypass the fixes a few times (and I think is still possible to do again, it’s a nice challenge if you are bored). See more details: https://github.com/vega/vega/issues/3027.
But that’s not the story I want to tell today.
A real JS challenge
The interesting bit is that the restrictions of the sanitizer remembered me of a challenge I never fully solved: execute arbirtary code w/o assignments or explicit calls (no parenthesis nor backticks). In the past I had spent quite some time messing with this idea without success.
The rules are more or less as follows:
- Using
Symbols
is out of scope, as it’s kind of well known how to do that (also less interesting as it requires evaluation of the symbol itself):
// beautiful ones
"alert(1)" instanceof {[Symbol.hasInstance]:eval}
"alert(1)".search({[Symbol.search]:eval})
"alert(1)".match({[Symbol.match]:eval})
"alert(1)".split({[Symbol.split]:eval})
// is this even useful?
-{[Symbol.toPrimitive]:function(){console.log(arguments)}} // "number"
''+{[Symbol.toPrimitive]:function(){console.log(arguments)}} // "default"
String({[Symbol.toPrimitive]:function(){console.log(arguments)}}) // "string"
// meh...
eval(''+{[Symbol.toStringTag]:'=alert(1)'})
I’m not being exhaustive here, so the curious readers can try to come up with their own variants :)
- We are interested in arbitrary execution even when the object needs be passed into an apparently inoquous function (not to
eval
or similar), for instance:
RegExp({__proto__:RegExp.prototype,get constructor(){alert(1)}})
Or even better:
({__proto__:RegExp.prototype,get global(){alert(1)}}).flags
Here we are cheating with getters, which are out-of-scope as well, but the interesting bit is that the RegExp
constructor gets the constructor
of its argument, and that flags
in a getter that accesses other attributes.
-
No external dependencies. If you rely on a very popular library I guess it’s OK, but the solution won’t be as nice :P
-
Do not try to bypass the rules. A valid solution should be simple and elegant. Imagine an object literal that pops an alert when converted to string. That’s what we want.
Disclaimer: I have no solution, and do not even know if one exists.
Baby steps
As I was on holidays I decided to dig a bit into the ECMAScript spec while laying on the sofa, to see if I could find something useful. I started with the basic valueOf
/toString
methods:
-{valueOf:function(){console.log('valueOf',arguments);return undefined;},toString:function(){console.log('toString',arguments);return undefined;}} // change hint and return values of functions
Check ToPrimitive(input, [,PreferredType])
and OrdinaryToPrimitive(O, hint)
, which define how objects are “casted” when passed to methods or operators that expect a primitive type. ToPrimitive
will set hint
to “default” if there’s no preffered type, to “string” if specified, and to “number” otherwise. If method Symbol.toPrimitive
is defined it will call it passing the hint string as argument; otherwise fallbacks to OrdinaryToPrimitive
, which will call toString
and valueOf
(or valueOf
and toString
, depending on the hint), and return if one of them returns a non object. If none of them do so, it throws a TypeError
.
The problem here is that we do not control the arguments passed to the function. So we’ll try to see what else can be done.
Besides, these methods have been largely used to exploit TOC-TOU vulnerabilties. I would expect the different hint between a wrapper (that performs a check) and the consumer function to be another source of issues.
That object does not implement interface Window
Let me make a quick parenthesis here. If we try -{valueOf:alert}
, it throws TypeError: 'alert' called on an object that does not implement interface Window.
. On Safari we can skip this by -{valueOf:alert,__proto__:window}
.
Would be nice to understand what kind of check Firefox and Chromium use. Can it be spoofed from JavaScript?
What am I looking for?
I’m mostly interested by function definitions that use Get()
, Call()
and Invoke()
, as these are the main building blocks we need: sources are object properties, and sinks are method invocations. I still haven’t look at everything, but here are some “interesting” bits I’ve collected.
For example, this one should be self explanatory, when we call Array.prototype.toString
it implicitly calls the join
method:
''+{toString:[].toString,join:function(){console.log('join',arguments)}}
The next one is a bit weirder, but will help to understand things better:
''+{0:[],1:[],length:2,valueOf:''.link,toString:[].pop}
Did you try it? Ok, let’s repeat it, but slower:
// This is kind of equivalent
''+{0:[],1:[],length:2,valueOf:function(){console.log('valueOf');return ''.link.call(this)},toString:function(){console.log('toString');return [].pop.call(this)}}
Why the hell does it seem to iterate? First, valueOf
is invoked (with “default” or “number” hint) a single time (as String.prototype.link
always returns a primitive type). However, the link
function receives the this
array-like object as an argument, and calls the ToPrimivite
function with “string” hint, i.e., toString
is called. Array.prototype.pop
reduces the length
of our array-like object and returns an element, which in our case is an object, and thus valueOf
is called again (remember from before?).
It turns out that we are simply using recursion, in some weird implicit way, and the termination condition is that the popped element is a non-object, which holds when the array is empty, as it returns undefined
. Cool, right? :)
Ok, let’s see a couple more with implicit accesses that gives us string generation:
''+{toString:Error.prototype.toString,name:'foo',message:'bar'}
> foo: bar
''+{__proto__:RegExp.prototype,source:'foo',flags:'bar'}
> /foo/bar
Yeah, definitely not very interesting, but who knows if these things can come handy one day.
Another confusing one:
''+{__proto__:Date.prototype,toString:Date.prototype.toJSON,toISOString:function(){console.log('toISOString');return ''},valueOf:function(){console.log('valueOf')}}
We force ToPrimitive
to call toString
method by concatenating with a string. That calls Date.prototype.toJSON
which implicitly re-runs ToPrimitive
with hint “number” (thus calling valueOf
) and then invokes toISOString
.
We can also skip the reference to toJSON
by:
JSON.stringify({__proto__:Date.prototype,toISOString:function(){console.log('toISOString');return ''},valueOf:function(){console.log('valueOf')}})
Similar stuff, although less interesting in this case, looks like follows, where the Map
constructor implicitly calls the set
method, which could be polluted:
Map.prototype.set=eval;new Map([["alert(1)"]])
This is a rather interesting case that I didn’t have time to explore further, but the argument of the array’s method is (kind of) forwarded to each of its elements:
[1,{toLocaleString:console.log},3].toLocaleString('xx')
An open question is how to control de argument bypassing the format check.
And to finalize, this is possibly the most useful one right now:
({test:RegExp.prototype.test,exec:eval}).test('alert(1)')
Conclusions, if any
If it’s the first time you learn about this you now have a new playground. Use your devtools and explore :) If you already knew it, I would be happy to learn about your craziest vectors. I’m sure I’m still missing good ones.
You never know when these things might be useful, but learning them will certainly give you a strong background for other stuff. Be prepared.
As said, it’s still not clear to me whether it is possible to execute arbitrary functions with arbitrary argument(s) in this way. I just scratched the surface, so maybe I missed it, or maybe one needs to chain several of these tricks in a clever way, or needs to exploit an implementation bug. We’ll never know until we try :)
Finally, some questions that come to mind:
- Is any of these useful for good-old JSON hijacking?
- What if we add prototype pollution to the equation?
- Can this implicit flows cheat on taint analysis engines?
A couple more random tests
''+({then:console.log,toString:Promise.prototype.catch})
class XSS{constructor(){console.log('const',arguments)};static get [Symbol.species](){console.log('species');return XSS;}};"".matchAll({__proto__:RegExp.prototype,toString:[].toString,0:'g',join:[].join,length:1,global:0,ignoreCase:0,multiline:0,dotAll:0,unicode:0,sticky:0,source:0,flags:"alert(1),g",constructor:XSS})
Update 2021/01/10
This is probably the nicest vector I came up so far:
'alert(1)'.match({__proto__:RegExp.prototype,global:1,unicode:1,exec:eval})
It uses the default RegExp.prototype [ @@match ](string)
, which calls RegExpExec
, which in turn calls the method exec
with the controlled string.