Skip to content

Latest commit

 

History

History

Truth of NPM

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Truth of NPM

  • Category: Web
  • Score: 371/500
  • Solves: 6

Description

Do you ever wonder how much weight does adding a NPM package to your project add?

Overview

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.

Solution

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.