Every CTF requires at least one overly complicated notes app.
Author: Z_Pacifist
18th solve - r2uwu2 @ PBR | UCLA
When I first took a look at this challenge with my teammate Benson Liu (the bizzler), we thought we found the vulnerability.
However, the glee was short-lived.
The admin does not visit a page we control and also javascript is disabled so a leak is fairly hard.
After staring at the application really hard, we found the below endpoint that customises whether author
and title
fields are required or optional for the /create
endpoint.
There is a glaring injection in this. We can effectively put whatever protobuf we want into the schema. Changing the defaults of specific fields won't really get us much though as the defaults in the protobuf schema do not affect the /view
endpoints at all.
The endpoint /view/<note>
looked interesting as well:
It does some require
magic to cache notes. There are also some special hooks that run if we view healthcheck
note. I'm not really sure how to exploit any of this though and I did not end up exploiting this particular endpoint in my final solution
After not really finding too much interesting vulnerabilities other than the protobuf injection, I looked further into the protobuf injection. If we are parsing something from a protobuf, there is a chance that somewhere in the code, it sets arbitrary keys on objects to values controlled by us which would be a prototype pollution vulnerability.
I did some googling and found that protobufjs v7.2.3 is vulnerable to a prototype pollution (disclosed only a few months ago).
I read the blog post detailing the exploit and found below example:
Side Note: I found it amazing how this vulnerabilty was discovered. Apparently, the vulnerability researchers created jazzer to fuzz javascript. I did not know that javascript could be fuzzed and I just found it cool lol.
Side Note 2: I love how in the GitHub advisory, they repeatedly mention "This is not related to CVE-2022-25878" as protobufjs had a different PP vuln a year before.
I ran it in my console and verified that it worked.
Sine we can override properties on objects, I decided to try to override author
on healthcheck.json
as healthcheck.json
omits author
field.
I injected below protobuf before optional
and tried visiting /view/healthcheck
Aww, it looks like author
is escaped.
The only field in the template that is not escaped is content
. However, healthcheck.json
already has content
defined so a prototype pollution will have no effect.
We need something else for our prototype pollution.
I searched online for how to turn prototype pollution into an RCE in expressjs as I knew that there was some expressjs prototype pollution vulnerability a while ago so the area is well-explored. I found a hacktricks page dedicated to the topic.
I'll skip over the other child_process
functions as we can't use them, but it is possible to achieve arbitrary RCE if we can prototype pollute Object
and spawn is called
.
– from hacktricks
Let's break this down: what's happening is that somewhere in spawn
, there is a check for whether certain configuration options were passed in and if they were not, it will fall back to our polluted values.
When we run spawn('something')
, spawn
is internally using a shell to run a shell command something
. By setting shell
to /proc/self/exe
(current executable), we are setting the shell to be node
. Now, we cannot really control the arguments to node as those are fixed to something
and the argv
passed in during calls.
This is where NODE_OPTIONS
comes in. NODE_OPTIONS
allows us to pass in commandline arguments to node
but in the environment. NODE_OPTIONS
doe not let us use --eval
, but it does let us use --require
to load in an arbitrary library. By requiring /proc/self/cmdline
, we can execute current argv. Now, we just have to pollute argv0
and we can get arbitrary code execution.
I tested out the hacktricks exploit locally, but it didn't work as there seems to have been a "kEmptyObject fix" whatever that is. However, just like the comment mentions, if we provide argv and options to spawn, it does work.
In required-notes
, puppeteer should be using child_process
as it needs to spawn a chrome browser. Indeed, looking at puppeteer source, we see that spawn
is being called (luckily for us, with arguments).
This means that if we can pollute shell
, argv0
, and NODE_OPTIONS
, we can achieve an RCE when the admin bot is spawned.
I tried prototype polluting all the values in a single update to the protobuf schema, but only the first prototype pollution worked. However, modfiying the code to pollute one value at a time allowed me to pollute all the variables.
I developed a pollute.py
to execute the prototype pollutions:
After polluting the mentioned variables, I visited /healthcheck
Then, I visited /view/owo
and captured the flag!
bi0sctf{/oKP/DRGI9ZzPZDW093DXQ==}
PS: I'm not sure what that b64 decodes to as it failed when I ran it through
base64 -d
, if anyone knows, please dm me atr2uwu2
on discord/instagram.