Introduction
- Category: Web
- Description: Login as admin to get flag, so easy right?
- Language: NodeJS
TL;DR
- Idea: You can send a JSON with
__proto__to bypass theusername=admincheck, and crack md5 by using online rainbow table to get flag. It works becausereq.bodyhas a null prototype while the{}inObject.assign()doesn’t. - Payload:
curl http://puzzler7.imaginaryctf.org:5001/login -H 'Content-Type: application/json' --data '{"password":"admin","__proto__":{"username":"admin"}}'
Analyze
At the first glance, we see a login form like the below image.
Whenever I see a challenge like this I usually press Ctrl+U to see the source code.
Line 13 tells us some hints, this might be the source code at the URI /source. I come into /source and saw the source code down below:
1 | const express = require("express"); |
Combine the source code when I press Ctrl+U and travel to /source, I can confirm that the middleware app.post('/login) will trigger whenever I fill in the login form and press theLogin button.
users constance has a table of key-value and if you try to hash those keys with the same md5 algorithm, you will see the value after hashed admin and guest have the same value in the table.
At this stage, you might see how to get the flag by sending the payload username=admin&password=admin.
But things don’t go easy like this, an if statement if (req.body.username === 'admin' && !localIPs.includes(req.ip)) { return res.end('Admin is only allowed from localhost') } not allow you to get the flag.
Sometimes they implement a server using Nginx, we can bypass the if statement above by sending X-Forwarded-For header. But this time it won’t work.
No more beating around the bush, i will straightforwardly talk about the bug at the middleware app.post('/login). The bug is spotted when they use Object.assign() to clone the new instance of req.body.
The
Object.assign()method copies all enumerable own properties from one or more source objects to a target object. It returns the modified target object.
One thing to note that req.body also has a null prototype. So we can pollute the req.body object and leverage Object.assign() to create a new object with the polluted prototype.
By default, when we press the Login button, the client will send Content-Type: application/x-www-form-urlencoded header.
So we have to change into Content-Type: application/json and send the payload {"password":"admin","__proto__":{"username":"admin"}},
The if (req.body.username === 'admin' && !localIPs.includes(req.ip)) { return res.end('Admin is only allowed from localhost') } check the username properties of the req.body object but it doesn’t checkreq.body.__proto__.username which have the same result when we access by auth.username.
PoC:
Flag: ictf{omg_js_why_are_you_doing_this_to_me}