Mutex Lock
just solved distributed systems
This challenge was the second hardest in web, with ten solves. As I discuss later in this writeup, I wrote this challenge in response to new trends in web security. Backdoored and malicious packages are becoming increasingly common, especially in the npm ecosystem. Entire companies have been created to help identify these problems, and yet I’ve never seen one in a CTF before.
Step 1: Getting our bearings
In this challenge, we’re presented with a simple “web mutex” interface. The interface allows us to create and lock a mutex, and unlock it given the pasword we got when we acquired the lock.
Now, as far as I can tell, there’s nothing exploitable at all in this – the operations are very simple, and there’s nothing nefarious going on. The flag is stored in the env, but there’s no way to get to it using this simple web server code.
Step 2: Finding inconsistencies
However, if you look in the frontend source, you’ll see something very curious: a button that takes you to /flag
. What happens when you go there?
A “Not Found” page. Boring, right? But what if you go to another URL that shouldn’t exist, like /asdf
?
The 404 page is different… but the /flag
route isn’t in the provided source. What’s going on?
Another way to discover this is by running the web server locally. If you download the ZIP file and run npm install
and then node index.js
, and go to localhost:3000/flag
, here’s what you see.
Okay, something really weird is going on. This is a normal Express 404 page. The tampering is gone!
Clearly, something must’ve changed in the install step. At this point, there are two ways you might notice what’s going on:
- The
package.json
file has a call tonpm update
in thepreinstall
script, which might be changing the paackages. You can then diff thepackage-lock.json
against the one in the ZIP file to see what changed. - You might also notice that instead of running
npm install
in the Dockerfile, it runsnpm ci --ignore-scripts
, which would skip thepreinstall
npm update
step.ci
also does a clean install, directly from thepackage-lock.json
. If you run this locally, the non-normal 404 page shows up. There’s definitely something going on with the pacakges, and again, you can diff thepackage-lock.json
file to find it, pre- and post-update.
Step 3: Exploiting the dependency
Diffing package-lock.json
will show that express
, despite what package.json
is telling you, isn’t coming from NPM – it’s coming from GitHub. This is an issue with NPM. Typically, when you install a package from GitHub, it’ll show you it’s from GitHub in package.json
. However, it isn’t required to be there to pass validation when doing a clean install. You can replace it with a simple version number, like it would be with an installation from the NPM registry, and as long as the package-lock.json
is still there, it’ll keep silently installing from GitHub. Even npm audit
won’t show that this is secretly happening. This is a huge issue with npm.
As an aside, GitHub doesn’t even show package-lock.json
diffs in PRs, calling them “too long”. Without some external tool monitoring a project’s package-lock.json
, you could easily slip in your own version of a dependency in a routine PR, and create a backdoor that nobody would notice. Scary stuff, and we’re already starting to see high-profile supply-chain attacks like this. I believe this is part of the future of web security and vulnerability analysis, which, again, is what inspired me to write a challenge like this.
Anyways, here’s the diff you’ll see:
"node_modules/express": {
"version": "4.19.1",
"resolved": "git+ssh://[email protected]/nkalupahana/express.git#ce12ff3ac1377b0e5f371a77460b3938ae15d63b",
}
If we go to this commit of this repo, what do we find?
Without the pwd
parameter, we get a 404 page. But with it?