Table of contents
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=admin
check, and crack md5 by using online rainbow table to get flag. It works becausereq.body
has 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:
const express = require('express')
const crypto = require('crypto')
function md5(text) {
return crypto.createHash('md5').update(text).digest('hex')
}
const app = express()
const users = {
guest: '084e0343a0486ff05530df6c705c8bb4',
admin: '21232f297a57a5a743894a0e4a801fc3',
'1337hacker': '2ab96390c7dbe3439de74d0c9b0b1767'
}
const localIPs = ['127.0.0.1', '::1', '::ffff:127.0.0.1']
app.use(express.urlencoded({ extended: false }))
app.use(express.json())
app.get('/', (req, res) => {
res.send(`
<form action="/login" method="POST">
<div>
<label for="username">Username: </label>
<input name="username" type="text" id="username">
</div>
<div>
<label for="password">Password: </label>
<input name="password" type="password" id="password">
</div>
<button type="submit">Login</button>
</form>
<!-- /source -->
`)
})
app.post('/login', (req, res) => {
if (req.body.username === 'admin' && !localIPs.includes(req.ip)) {
return res.end('Admin is only allowed from localhost')
}
const auth = Object.assign({}, req.body)
if (users[auth.username] === md5(auth.password)) {
if (auth.username === 'admin') {
res.end(`Welcome admin! The flag is ${process.env.FLAG}`)
} else {
res.end(`Welcome ${auth.username}!`)
}
} else {
res.end('Invalid username or password')
}
})
app.get('/source', (req, res) => {
res.sendFile(__filename)
})
app.get('/package.json', (req, res) => {
res.sendFile('package.json', { root: __dirname })
})
const port = 5001 || process.env.PORT
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`)
})
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}