- Category: Web
- Score: 371/500
- Solves: 6
Do you ever wonder how much weight does adding a NPM package to your project add?
The deno web service allows you to query the size of any npm package, and it does this by importing the package.json
of the npm package, calculating the size of the package, and deleting all the files after the calculation. The target is to get RCE on it.
From the main.ts
, there is an obvious LFI flaw:
app.use(async (c: Context) => {
const page = c.req.path.slice(1) || 'index'
try {
const { handler } = await import(`./pages/${page}.tsx`)
return handler(c)
} catch {
return c.html('404 Not Found', 404)
}
})
which can be triggered by curl http://localhost:8000/../../path/to/file_without_tsx_extension
. But to exploit this you have to make a tsx
file present on its local file system.
The query.tsx
allows you to import package.json
of any npm package by:
const module = await import(`npm:${packageName}/package.json`, {
with: {
type: 'json'
}
})
then deletes all the unnecessary files afterward:
async function* walkPackageFiles(npmDir: string) {
for await (const entry of fs.walk(npmDir)) {
if (entry.isDirectory) continue
// registry.json is generated by deno
if (entry.name !== 'registry.json') {
yield entry
}
}
}
// omitted...
for await (const entry of walkPackageFiles(npmDir)) {
await fs.remove(entry.path)
}
async function queryPackage(packageName: string) {
// omitted...
let totalSize = 0
const ps = await asyncMapToArray(walkPackageFiles(npmDir), async entry => {
const { size } = await Deno.stat(entry.path)
totalSize += size
return Deno.remove(entry.path)
})
// omitted...
}
The key to this challenge is to use the fact that Deno.readDir
(used in fs.walk
) automatically skips any non UTF-8 files: fix: skip non-UTF-8 dir entries in Deno.readDir()
So you can create a npm package containing a non UTF-8 tsx file (e.g. \xff.tsx
), and publish it to npm. Once it is queried by the deno service, the file would be at /deno-dir/npm/registry.npmjs.org/$PACKAGE_NAME/$VERSION/exp%ff.tsx
.
Here, I create a backdoor package at exppkg, which simply eval the entire incoming body.
The last step is to escape the Deno sandbox with --allow-read --allow-write --allow-env
permission given. It is actually quite wellknown that you can read /proc/self/maps
to by ASLR and write shellcode to /proc/self/mem
to bypass the sandbox. However, this is eventually seen fixed in Permission escalation via open of privileged files with missing --deny
flag.
But if you actually try to see how it fix it, you will notice that it can be easily bypassed as it is just comparing strings. So you can create a symlink somewhere that link to /proc
or /proc/self
, then use it to access /proc/self/maps
and /proc/self/mem
.
The final exploit is exp.js.