初心者のSECCON Beginners Writeup

先週開催されたCECCON Beginnersに参加しました。
CTFエンジョイ勢なので基本的な問題しか解けませんでしたが、せっかくなのでWriteup書きます。

概要

日時:2019 年 5 月 25 日 (土) 15:00 - 2019 年 5 月 26 日 (日) 15:00 (24 時間) 場所:場所: オンライン
形式: チーム戦 Jeopardy
チームメンバー:自分以外に2人(僕以外は今年からセキュリティ系の会社に就職したので今後に期待。)

結果

666人中131位
あまりきちんと解けたとは言えませんでしたが、それでも頑張った方かな。。。


【Web】[warmup] RAMEN

Web問題の一問目、基本的なSQLインジェクション

f:id:ylab-203:20190601103129p:plain
web1-1

最初に手を付けた問題がこれ。
午後三時くらいだったので飯テロによって早くもHPが削られました。

f:id:ylab-203:20190601103533p:plain
web1-2

Webページ中に店員さんの名前と一言コメントを検索するフォームがありましたので、
こちらにSQLインジェクションを仕掛けます。

流はは下記の通りです
1. 基本的なSQLインジェクションが仕掛けられるか確認
2. UNIONで情報を取得できるか確認
3. テーブルの名前とスキーマを取得
4. フラグが入っていそうなテーブルを表示

1. 基本的なSQLインジェクションが仕掛けられるか確認
まずは適当にSQLの構文に引っかかってくれそうな文字列を入れてみます。

f:id:ylab-203:20190601114943p:plain
web1-3
すると下記のようなエラーが発生しました。
f:id:ylab-203:20190601115013p:plain
web1-4
よって、CGIプレースホルダ等の仕組みを利用せず、入力フォームで直接SQLを操作できそうと推察できます。
そして後ろ側で走っているSQLは以下の様なものであると予想できます。
SELECT name,comment FROM table_name WEARE name = '[フォームに記載した内容]' ;

次にSQLインジェクションの定石を試します。
1' or "1" = "1" #
これがうまく行けば対象のテーブルに記載されているデータをすべて取得することができます。
細かい説明は割愛しますが、簡単な説明は下記の通りです。
* 「1'」:本来フォームに入れてSQLの検索対象になる文字列として適当に1を入れている。
* 「or 」:SQLの検索条件。orの左右が正ならそれにマッチしたデータが返ってくる。
* 「"1" = "1" 」:絶対にtrueになる条件。
* 「# 」:以降のSQL文はコメントアウト。#の後ろにスペースを入れるのがキモ。

さて、上記の文字列をフォームに入れてSEARCHをクリックします。

f:id:ylab-203:20190601122256p:plain
web1-5

特に変化はありません。
しかし、今回はエラーが出なかった = SQL文として正しい構造であることがわかりました。

2. UNIONで情報を取得できるか確認
UNIONとは、UNIONは二つ以上のSELECT FROM〜の結果を、統合して表示してくれる仕組みです。
とはいっても、現状テーブル名やカラム名は不明なため、"null"を利用して下記の通り試してみます。
1' or "1"="1" UNION SELECT null, null #

f:id:ylab-203:20190601123243p:plain
web1-6

すると、もともと表示されていた3行に加えて、なにも表示されていない行が1行追加で表示されました。
これにより、UNIONを利用することで別のテーブル等の情報を表示できることが確認できました。

3. テーブルの名前とスキーマを取得
UNIONで他のテーブルの情報が表示できそうであることを確認した後は、それらの一覧を表示させてみます。

1' or "1"="1" UNION SELECT table_name, column_name FROM information_schema.columns #

INFORMATION_SCHEMA COLUMNS テーブルというRDBが標準で保持しているテーブルにアクセスし、テーブル名(table_name)とテーブル内のカラムに関する情報(column_name)を表示させます。
参考: dev.mysql.com

f:id:ylab-203:20190601124526p:plain
web1-7

上記の文字列を入力すると、最初の3行に加え様々な情報が見えるようになりました。
(DB初心者なので詳しくはわかりませんが、おそらくRDBにデフォルトで準備されているテーブル等の情報かなと思ってます。)

そして下の方まで探してみると、明らかにフラグが入っていそうなテーブルを見つけました。

f:id:ylab-203:20190601130126p:plain
web1-8

4. フラグが入っていそうなテーブルを表示
最後に、フラグが入っているであろうテーブルと、そのカラムの内容を表示させます。

1' or "1"="1" UNION SELECT flag, null FROM flag #
flagテーブルの中にはflagというカラム1つしか入っていません。
UNIONで紐づけて表示を行う場合は、他のSELECT文で表示させているカラムの個数(「名前」、「一言」の2列分)と揃える必要があるため、あえてnullを追記しています。

f:id:ylab-203:20190601130728p:plain
web1-8

そしてめでたくフラグを取得することができました。


【Misc】containers

問題として提示されたのは「e35860e49ca3fa367e456207ebc9ff2f_containers」という名前のファイル一つでした。
「コンテナ」という名前と16進数で記載されたファイル名からdocker初めは関連の問題かと考えていました。
(コンテナexportしたものかな。。。?でもファイル容量が32KBしかないしな。。。)

viでファイルを強引に開いてみると、先頭には下記のように記載されていました。

CONTAINER.FILE0.<89>PNG^M
^Z
^@^@^@^MIHDR^@^@^@<80>^@^@^@<80>^H^F^@^@^@Ã>aË^@^@^@^AsRGB^@®Î^\é^@^@^@^DgAMA^@^@±<8f>^Küa^E^@^@^

一見バイナリファイルに見えますが、先頭の「CONTAINER.FILE0.」は読めるので、明らかにファイルに直接書き込んだ雰囲気がありました。
そして目についたのは「PNG」。viをバイナリエディタモードにして確認してみると下記のようになっていました。

00000000: 434f 4e54 4149 4e45 522e 4649 4c45 302e  CONTAINER.FILE0.
00000010: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000020: 0000 0080 0000 0080 0806 0000 00c3 3e61  ..............>a
00000030: cb00 0000 0173 5247 4200 aece 1ce9 0000  .....sRGB.......
00000040: 0004 6741 4d41 0000 b18f 0bfc 6105 0000  ..gAMA......a...
・・・(以下略)

0x10バイト目から「.PNG」で始まっているのを見るに、このファイルには画像が埋め込まれていそうだなと考えました。
(※ 「.PNG(8950 4e47)」はPNGマジックナンバーです、下記参照。)

qiita.com

まずは余計な0x00〜0x10バイト目までを削除してみました。

00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0080 0000 0080 0806 0000 00c3 3e61  ..............>a
00000020: cb00 0000 0173 5247 4200 aece 1ce9 0000  .....sRGB.......
00000030: 0004 6741 4d41 0000 b18f 0bfc 6105 0000  ..gAMA......a...

そんなうまくいくわけ無いだろうなと思いつつ保存して確認してみましたが、案の定特に何も表示されませんでした。

f:id:ylab-203:20190601134254p:plain
misc1-1

さて、エディタに戻って他に情報がないか探してみると、「.PNG」が先頭以外にも複数あることを発見しました。

00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
・・・
000002c0: 0000 0000 4945 4e44 ae42 6082 4649 4c45  ....IEND.B`.FILE
000002d0: 312e 8950 4e47 0d0a 1a0a 0000 000d 4948  1..PNG........IH
・・・
00000510: ff57 6aad 0000 0000 4945 4e44 ae42 6082  .Wj.....IEND.B`.
00000520: 4649 4c45 322e 8950 4e47 0d0a 1a0a 0000  FILE2..PNG......
・・・
00000750: 61c6 f36a 3b5f b301 0000 0000 4945 4e44  a..j;_......IEND
00000760: ae42 6082 4649 4c45 332e 8950 4e47 0d0a  .B`.FILE3..PNG..
・・・(以下略)

これにより、以下のことが推測できました。
・.pngファイルは「8950 4e47」から始まり、「0000 0000 4945 4e44 ae42 6082」で終わる。
・各pngファイルの間には「FILEx」が目印として差し込まれている。
・つまり、「8950 4e47・・・」から「・・・ae42 6082」までを切り取れば画像ファイルの取り出しが可能。

先程先頭を切り取ったファイルの0バイト目から 0x2cc = 716 バイト目まで を下記のコマンドで出力します。
dd if=e35860e49ca3fa367e456207ebc9ff2f_containers of=flag1 skip=0 bs=716 count=1

すると「c」と表示された画像が出力されました。

f:id:ylab-203:20190601143836p:plain
misc1-2

これを続けること3時間、「}」まで画像を切り分けることによりflagを獲得しました。

f:id:ylab-203:20190601144628p:plain
misc1-3

(自分はddコマンドで一つずつ切り分けしましたが、スクリプト等でもっと簡単にできるようになりたいです。。。)


【Crypto】party

問題は2つのファイルから構成されていました。
1つ目はpythonのコードが書かれたファイル

from flag import FLAG
from Crypto.Util.number import bytes_to_long, getRandomInteger, getPrime


def f(x, coeff):
    y = 0
    for i in range(len(coeff)):
        y += coeff[i] * pow(x, i)
    return y


N = 512
M = 3
secret = bytes_to_long(FLAG)
assert(secret < 2**N)

coeff = [secret] + [getRandomInteger(N) for i in range(M-1)]
party = [getRandomInteger(N) for i in range(M)]

val = map(lambda x: f(x, coeff), party)
output = list(zip(party, val))
print(output)

2つ目は数字の書かれたファイル

[(5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787, 222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933), 
・・・(以下略)

フラグとなる文字列を「encrypt.py」で暗号化した結果が「encrypted」のようです。
文字列は「Crypto.Util.number」の「bytes_to_long()」で数値化し、ゴリゴリ計算した後リスト形式で出力しています。
そのため「encrypt.py」から計算式を組み立て、「encrypted」の値から逆算すれは回答が導き出せそうです。

計算

getRandomInteger()で得た値をA〜E、flagをxで表すと、リストcoeffとpartyは下記のように表せます。

coeff = [ x, A, B ] party = [ C, D, E ]

また、リストoutput(「encrypted」の中身)をl,m,nを用いて下記の通りに表します。

output = [ (C, l), (D, m), (E, n) ]

これをもとに「encrypt.py」の計算式を表すと下記のような連立方程式となります。

 x * C^{0} + A * C^{1} + B * C^{2} = l
 x * D^{0} + A * D^{1} + B * D^{2} = m
 x * E^{0} + A * E^{1} + B * E^{2} = n

C,D,E,l,m,nの値は既知なので、x,A,Bについて方程を解いて行きます。

 B = \displaystyle \frac{ ( n - l ) * ( D - C ) - ( m - l ) * ( E - C ) } {( E - C ) * ( D - C ) * ( E - D ) }

 A = - B * ( C + D ) +   \displaystyle \frac{ m - l } { D - C }

 x = l - A * C^{1} - B * C^{2}

以上をpythonに落とし込むと下記のようになります。

from Crypto.Util.number import bytes_to_long, getRandomInteger, getPrime, long_to_bytes

A = 0
B = 0
C = 510009 ・・・
D = 3084167 ・・・
E = 630891 ・・・
l = 222638 ・・・
m = 814179 ・・・
n = 340685 ・・・

B1 = (n-l)*(D-C)-(m-l)*(E-C)
B2 = (E-C)*(D-C)*(E-D)

B = int(B1/B2)
print("---B---")
print(B)

A = (-1)*B*(C+D)+int((m-l)/(D-C))
print("---A---")
print(A)

x = m-(A*D)-(B*D*D)
print("---x---")
print(x)

print(long_to_bytes(x))

最後xの値を出した後文字列に変換するために、long_to_bytes()を使用しました。(importを忘れずに)

Crypto.Util.number

実行結果は以下の通りとなります。

---B---
8559415 ・・・
---A---
6759741 ・・・
---X---
1757212 ・・・

ctf4b{just_d0ing_sh4mir} 

終わりに

SQLインジェクションの基本問題を意外とすんなり解くことができたため前回より成長を感じましたが、ゴリ押しで解いて時間を消費してしまったりpwnで手を付けられそうな問題がなかったりなど、まだまだ未熟だと感じる点が多々ありました。
チームの他のメンバーがセキュリティ本業なので、pwn等は任せてその他の細々とした問題(今回でいうとSliding puzzleなど)メインで解けるようになっていけば、チームとしてのバランスは取れるかなと思いました。