すらすと’s 生活の跡

不真面目な元高専生によるなにか

KOSEN セキュリティ・コンテスト 2018 Write Up その他

福岡博多市で開催されたKOSEN セキュリティ・コンテストに insecure として,現地参加してきた.

今回イベントのHP -> KOSENセキュリティコンテスト2018

前日に猫カフェに行ったりしつつ福岡入り.ホテルでは何問か常設CTFの問題をやるなどした.

弊チームの参加者はtheoldmoon0602 (ふるつき),ptr-yudai (師匠),yoshiking (よしキング),thrust2799 (スラスト,私).
今回は:pro:なふるつき氏と師匠氏,今回初参加で特訓を積み重ねたよしキングという同一研究室メンバーに加えてもらう形で参加させていただいた.

蓋を開けてみれば,ほとんど3氏が問題を解いて,私はNetworkしかsubmitできなかった....やはりよしキングは:pro:だった.
insecureは一位で優勝,ありがたいことにSECCON2018に出れます.やった.

f:id:thrust2799:20180902202310p:plain

そんなこんながあった大会の,Write Up. 一応,解けた順に,触ったものも含めて.

[Network 11] ログインしてフラグを入手せよ。 (Score: 150)

問題内容:
ヒント:
我々は秋葉原ラジオ会館上空に飛来したタイムマシンを用いて、未来の超高性能コンピュータを入手した。
そのコンピュータによると、MD5の値 b21424f30227ac8bc08c69216c30815 のハッシュ化前の値は以下であるらしい。
c932836c1feff27841c03453e81d5b13:oX3Ar2V0BQA=34c6176e8e33d6da83cc500028b8f9c8de95b91d:00000001:OWEyZWEyNmZhNzAyYTUwMzM0MzRjYzMxZDljZGY2OTU=:auth:71998c64aea37ae77020c49c00f73fa8

なんというSteins;Gate....これは布教ですわぁ(いいぞ).
問題としてはHTTPのDigest認証を突破するタイプ.
ちなみに研究室で勉強したり前日に復習したりしてsolverが手元にあった問題だった.なんという幸運!

wiresharkで問題のパケットを開いて,フィルタ (tcp.stream eq 0)でFollow TCP Stream.

f:id:thrust2799:20180902202902p:plain

やはり401エラーが返され認証が必要とされている.
また,WWW-Authenticateフィールドで必要そうなものが返されている.

'WWW-Authenticate'内容:

Digestrealm="Digest Auth"
nonce="bt2zImd0BQA=c6284989d568c3b3483e649ed0d4b440918fb05e"
algorithm=MD5
qop="auth"

次に,フィルタ (tcp.stream eq 1)でFollow TCP Stream.(見切れた部分は401エラーで先ほどと同じ)

今度は200で通り,フラグが存在するっぽいリンクが書かれたページがある.
どうやらDigest認証でhttp://digest.kosensc2018.tech/flag.txtにアクセスしたいらしい.
ちなみに,HTTP送信ヘッダのHostフィールドはアクセス先のホスト名が格納されている.

他に有用な情報は得られなかったので,この情報で突破したい.
つまり,パスワード回避してDigest突破したい!

ここで,もう一度フィルタ(tcp.stream eq 1)でのTCP Streamを見てみる.

f:id:thrust2799:20180902202918p:plain

どうやら,認証に成功した状態にはヘッダの要素として次のものが挙げられそうである.

GET / HTTP/1.1
Host: digest.kosensc2018.tech
Authorization: 
  Digest username="tanaka", 
  realm="Digest Auth", 
  nonce="zwFHJGd0BQA=50c6205549fdb0f63aa3f780a7504cc82864010d", 
  uri="/", 
  cnonce="ZmMyNDM3NzcyNWI3ZGI5NjQyNjhiNTAwZDkxZjM4YzQ=", 
  nc=00000001, 
  qop=auth, 
  response="dce3409758e948c5ba76fb121f089812", 
  algorithm="MD5"

この情報のうち,WikipediaのDigest認証Qiita先生に聞いてみたところ,次の意味があるそう.

  Digest username: ユーザー名 
  realm: 認証領域名
  nonce: サーバー生成のランダム文字列 
  uri: 'http://digest.kosensc2018.tech'で表示されるページからの相対パス
  cnonce: クライアント生成のランダム文字列
  nc: カウント
  qop: Digestの生成方法,bodyを含めるか,大体"auth"な気がする
  response: 'あるデータ'のalgorithmフィールドによる方法でのハッシュ
  algorithm: responceハッシュの生成方法,大体MD5な気しかしない

ちなみに,nonceは一度認証せずにアクセスしたときに返されるものをそのまま使う必要がある.
これはスクリプトを組んだら問題がなさそうなので,あとはresponce.

ここで,responseの構成をWikipediaさんから引っ張ってくる.

A1 = ユーザ名 ":" realm ":" パスワード
A2 = HTTPのメソッド ":" コンテンツのURI
response = MD5( MD5(A1) ":" nonce ":" nc ":" cnonce ":" qop ":" MD5(A2) )

ここのMD5()はMD5でハッシュを取った結果を意味している.
ここで,新たに出てきたのはA1の'ユーザ名 ":" realm ":" パスワード'だった.
パスワードが分かんねぇつってんだろ!?!?!?

詰みかと思ったときのヒント,実はA1が書いてあるのである.

ヒントにあるそれぞれの値は,

response                     : 2b21424f30227ac8bc08c69216c30815
MD5(user:realm:password)    : c932836c1feff27841c03453e81d5b13
nonce                       : 34c6176e8e33d6da83cc500028b8f9c8de95b91d
nc                          : 00000001
cnonce                      : OWEyZWEyNmZhNzAyYTUwMzM0MzRjYzMxZDljZGY2OTU
qop                         : auth
MD5(REQUEST:URI)            : 71998c64aea37ae77020c49c00f73fa8

はい.

どうやらMD5(A1)は c932836c1feff27841c03453e81d5b13 を使いまわせそう.

ということで,必要な情報はそろったのでコーディングタイムです.
(ちなみにtypoで4回くらい401が返されたのは笑い話)

import requests
import hashlib

headers = requests.get('http://digest.kosensc2018.tech/flag.txt').headers
HA1 = 'c932836c1feff27841c03453e81d5b13'
HA2 = hashlib.md5('GET:/flag.txt'.encode('utf-8')).digest().hex()
nonce = headers['WWW-Authenticate'][35:87]
res = hashlib.md5((HA1 + ':' + nonce + ':00000001:OWEyZWEyNmZhNzAyYTUwMzM0MzRjYzMxZDljZGY2OTU=:auth:' + HA2).encode('utf-8')).digest()

flags = requests.get('http://digest.kosensc2018.tech/flag.txt', headers = {'Host':'digest.kosensc2018.tech','Authorization':'Digest username="tanaka", realm="Digest Auth", nonce="' + nonce + '", uri="/flag.txt", algorithm=MD5, response="' + res.hex() + '", qop="auth", nc=00000001, cnonce="OWEyZWEyNmZhNzAyYTUwMzM0MzRjYzMxZDljZGY2OTU="'})
print(flags)
flags.text

f:id:thrust2799:20180902202948p:plain

A . SCKOSEN{digest_auth_is_secure!}

ちなみに運営がフラグ設定間違っていたらしく,しばらく解答できなかった.おそらく1番乗り.

この問題のポイントは,responseがデコードされるか,'ユーザ名 ":" realm ":" パスワード'のハッシュ(サーバーの.htdigest)が漏れるとだめってお話でした.

[Network 10] Basic認証 (Score: 100)

問題内容:
パケットファイルを解析せよ

さて,Network2問目(1問目).こちらはパケットファイルを解析するだけなのか~?

というわけで,フィルタ(tcp.stream eq 0)でFollow TCP Stream.

f:id:thrust2799:20180902203020p:plain

いつもの.(401エラー)

やはり問題文通りBasic認証をしているっぽい.

次.フィルタ(tcp.stream eq 1)でFollow TCP Stream.

f:id:thrust2799:20180902203017p:plain

200が返されてなんかあるみたい.
flag.zipがあって,Johnさんのパスワードが解錠パスワード,と.
IPアドレスがHostフィールドにあって,アクセスはここからはできないみたいなので,パケットファイルにzipファイルが残っていそう.
Wiresharkさんにお願いしたいので,File -> Export Objects -> HTTP でパケット内のファイルオブジェクトを一覧表示.
予想通りflag.zipがあったのでエクスポートしていく.

さて,Johnさんのパスワード...|ω・`)

f:id:thrust2799:20180902203011p:plain

john:s8oX*zlcro8?#wlblpr4 (ユーザー名:パスワード)

Basic認証さん!?

そう,Basic認証ではBase64エンコードされたユーザー名とパスワードが格納されており,さらにWiresharkさんは勝手にデコードまでしてくれるのである.

あとはこれで解凍して解答(激寒ギャグ).

A . SCKOSEN{B@$ic_@uth_is_un$@fe}

Basic認証はパケット取られるだけでアウトなのであまり使わないでねっていう問題でした.


ここまでが自力でSubmitできた問題.
全然強くない人なのでここからは最初の発想とかだけ.
役割分担したと思いたい....

[Crypto 08] シンプルなQRコード (Score: 200)

問題内容:
半分のQRコードを入手した。なんとか復元できないだろうか。

最初の発想といっても,これはこれで自分で全部やったと言えない問題.
実は別のCTF大会で出てたWrite Upをそのまま流用してきた感じ.

先人様ありがとうございました.
SECCON CTF 2013 online予選 forensics 400

さて,問題のQRコードはこれ.

f:id:thrust2799:20180902175230p:plain

とりあえず,マーカーとかの何も考えなくても修復できるところを修復.
これだけでもうQRっぽいけど,読み取れない.

そこで,先ほどのWrite Upを頼りにマスクパターン,誤り訂正bitの15[bit]を復元.
さらに修復を進めたQRコードがこちら.

f:id:thrust2799:20180902175315p:plain

これでも読み取れず,ちょっと悩んでいたところで師匠氏からお声がけ.
そのまま解いてもらってSubmit.

ところで,あとでホテルで聞いたところによると,もう読み取るだけだったそう.
pythonのstrong-qr-decoderってものを使ったらしく,新幹線の中で使ってみた.

まず,こちらのGitHubからclone.
次に,これに掛けられるように画像から'X' (黒いドット)と'_' (白いドット)のテキスト形式に変換.

変換スクリプトはこちら.

from PIL import Image

img = Image.open('broken_qr_.png')
x, y = img.size
qr = open('qr_data.txt', 'w')

for v in range(y):
  if v % 10 != 0:
    continue
  for u in range(x):
    if u % 10 == 0:
      if img.getpixel((u, v)) == (0, 0, 0, 255):
        qr.write('X')
      else:
        qr.write('_')
  qr.write('\n')

で,出てきたQRがこちら.

XXXXXXX_______X_X_____XXXXXXX
X_____X_______X_X_X___X_____X
X_XXX_X_______XXXX_X__X_XXX_X
X_XXX_X__________X__X_X_XXX_X
X_XXX_X_X_____X___X___X_XXX_X
X_____X_______X_XXX___X_____X
XXXXXXX_X_X_X_X_X_X_X_XXXXXXX
________X______XX__X_________
__XX__XXX_________XX_XX_X____
_______________X_XX_X___XX_X_
______X___________XXX___XXX_X
______________XXX_X_X_XX__XX_
______X_______X_X__XX_X_XXX_X
_______________X__X___X__XXX_
______X_________X__X_XXXX_XXX
___________________XX_XXX__XX
______X_______X_X___X___XX_X_
_______________X__XX___XX___X
______X_______X_XX_____XXXX_X
________________XXX_XXX___XXX
______X________XX_XXXXXXXXX_X
_________X__________X___X_XXX
XXXXXXX__X______X__XX_X_XXXX_
X_____X_______XX___XX___XX_X_
X_XXX_X_______X___X_XXXXXXX__
X_XXX_X__X____X_X_XX__X__XX__
X_XXX_X__X_______XX__X__X_X_X
X_____X_______X_XX_X_______XX
XXXXXXX_________XX_X_XX_XX___

こちらをstrong-qr-decoderに掛けて,

A . SCKOSEN{remove_rs_qr}

100の実績,もらっていいよね?

[Crypto 08] RSA? (Score: 300)

問題内容:
電話レンジ(仮)でのDメールに失敗し、冪演算のない世界線へ来てしまった。
この世界線は今までいた世界線RSA暗号に似た暗号で守られている。解読してみよう。

[+] Public key (n,e): (746149315120445105644911779735002615257864427638948809430146775899763900845401478319781237412694334410613686368021055483040278357991481684487813129494944899522460397699493087246505218096960286204364461639918177320793843718724747574381859928496072509749639870292090503328557688115529251888351927498428567126853657063832878452773780418783149866919316163560203180423276894765325460041738451797385343149303272504850808939009366426995340204717301911359661640524141292447180118808883848024348018576236114084330353144353115672904820149103681022683146560799116848906807498625698939954475819479000937838859292276566046219449122782347075051511004009561691484747596362940386608213329221089677679229620860810111092211338341215804260360529582967128623164620534814690277276443932913145186024258511492440354889009304938979173562941458331119511296138076261503871542382962671556195928643434506282416038086486816450594938254170549974935406839221438655021923764949572023494832426904174630809785240917462806646302389526945618245684059655484996935724994381587851879888451272335891503968849596157334527192713715105054266039301159419016747298458532769985135446278269957364121043506362499497443274900182869320930776855669486054000707293632271709427175565301,825821)
[+] Ciphertext = 34196057544535966582914714160221953732017949669815971420694645835609518260754242418191180642793
[+] Dec(c) == m?: True

ということで,累乗のないRSA暗号
掛け算かな?試してみよう.

d * e mod n ≡ 1
m * e mod n ≡ c
c * d mod n ≡ m

11 * 5 mod 3 ≡ 1
2 * 5 mod 3 ≡ 1
1 * 11 mod 3 ≡ 2

いけそうである.

ここで,この情報をよしキングに投げて自分は別の問題へ行ってしまった.

解けた後で教えてくれたのだが,Ciphertextをeで割ってエンコードを「ていっ」ってするだけだったらしい.
これは自宅に帰ってから書いたスクリプト

import codecs

e = 825821
c = 34196057544535966582914714160221953732017949669815971420694645835609518260754242418191180642793

codecs.decode(hex(c // e)[2:].encode('utf-8'), 'hex_codec')

A . SCKOSEN{Extended_Euclide@n_@lg0rithm}

これも100の実績がほしい.

[Web 16] 進撃せよ (Score: 300)

問題内容:
求めるものは壁の向こう側にある。
巨人になったつもりで、進撃せよ。

これは進撃の巨人ですね,私わからんけど.

問題のサーバーにアクセスするとこんな感じ.

f:id:thrust2799:20180902200148p:plain

で,それぞれクリックするとこんな感じ.

f:id:thrust2799:20180902200219p:plain

f:id:thrust2799:20180902200222p:plain

どうやらWAF (Web Application Firewall)がflagを検知してforbidenしているらしい.
で,肝心のリクエストは http://waf.kosensc2018.tech/ist/ZmxhZy50eHQ= って感じで,最後に何かBase64っぽい文字列.
試しにディレクトリトラバーサルな文字列をBase64して試した結果がこちら.

f:id:thrust2799:20180902200744p:plain

/etc/passwd が見つかってしまった....
とりあえずこれをチームで共有,ここから /etc/passwdWWW-data のユーザーディレクトリ等を参考にエスパー.
何とか /var/www/html/waf/files に一覧されてるファイルがあることが分かって,さてどうするかってなった次第.

と,ここでふるつき氏から大勝利情報 (WAFのphpコード)が降ってきて,あれよあれよという間にsubmit.

どうやらWAF部は1回しかBase64デコードしていないのにファイル名抽出部は無限にBase64デコードする仕様になっていたらしい.
2回 flag.txtBase64エンコードしたものを投げて,Flag獲得.

A . SCKOSEN{beyond_the_wall...}

運営はエスパーしてBase64を2回するのを想定していたそう.
ちなみにふるつき氏はこちらみたいにエスパーせずにプロセス情報を見たり設定ファイルにあたり付けて獲得した模様.
やり方がとてもスマート!


さて,今回触れた問題はこんなところでした.
さすがにNetworkは得意分野なのでできるんですが,ほかの分野が全然なので結局周りのチームの解いている人よりも全然取れていないのが実情.
優勝したけど個人としては精進を続けるしかないと思った大会だった.(毎回思ってるだろとか言わないで...)

最後に,公開されているチームメイトのWrite Upは以下.

高専セキュリティコンテスト 2018 #kosensc Writeup - ふるつき

高専セキュリティコンテスト2018のWrite Up - CTFするぞ

KOSEN SECCON 2018 - Writeup | ocamlab Kibela