2linenodejs
Description
Web | 13 solves
Sorry for my bad coding style :(
Author: ginoah
Solution
Prototype Pollution
Taking a look at the source, we see quite clearly that there is a prototype pollution here.
JSON.parse
will allow the __proto__
key, storing it as ['__proto__']
instead (which surprisingly works as a key when used here):
Great! We have a prototype pollution - how do we leverage it to an RCE?
require() Gadget
After performing the pollution, we don't have much of a choice where we want to go. Either nothing happens and process.exit()
is called, or we cause an exception and require('./usage')
is called. Causing an exception is pretty simple and I actually stumbled upon it early on when testing simple payloads.
If one of the key-value pairs is a mapping to null
, then Object.entries(V)
will yield a TypeError
since null
cannot be converted to an Object
.
If we look into the internal/modules/cjs/loader.js
, we see that in the trySelf
function, there is a possible gadget.
If readPackageScope
returns false
, then the destructuring assignment should leave pkg
and pkgPath
as undefined
, since the right-hand side is {}
. But if we pollute __proto__.data
and __proto__.path
, then we can control pkg
and pkgPath
.
But what is pkg
and pkgPath
? We could look at readPackageScope
and find out that it calls
readPackage
to populate the result, and readPackage
just reads the package.json
file of a Node.js module.
So pkg
appears to just be an object containing the package.json
fields and pkgPath
is the path to this package. Importantly, we see pkg.exports
being used a lot in the subsequent code path, and this makes sence given the following explanation of exports
in package.json
:
The
"exports"
field allows defining the entry points of a package when imported by name loaded either via anode_modules
lookup or a self-reference to its own name.
With this knowledge, we can confirm that the following exploit allows us to load any JavaScript file.
preinstall.js Gadget
Initially doing a simple search for all JavaScript files in the container (find / -name "*.js" 2>/dev/null
), we can find /opt/yarn-v1.22.19/preinstall.js
. Doing a bit of digging, we can find out that this script is added from here.
Immediately we see in this script that we have child_process.execFileSync
being called, which looks promising.
First off, to reach this code path we could need to pollute npm_config_global
to a truthy value.
process.execPath
is always /usr/bin/node
, and we can't control it. But we could control process.env.npm_execpath
since it is not set by default. Looking at the CLI documentation, the -e
or --eval
option looks promising! This would basically allow us to run inline JavaScript.
One issue is that because the regex matches up to the first space character, our JSON cannot have any spaces.
To get around this, we use ${IFS}
. For instance, we could pollute npm_execpath
to --eval=require('child_process').execSync('sleep${IFS}5')
.
The final payload was using wget
and command substitution to exfiltrate the /readflag
output.
This gives us the flag on our listening HTTP server.
Last updated