Viettel Mates CTF 2018 Writeup

この大会は2018/6/16 9:00(JST)~2018/6/17 9:00(JST)に開催されました。
今回もチームで参戦。結果は401点で400チーム中43位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity Check (Misc)

参加確認問題。問題にフラグが書いてある。

matesctf{san1ty_ch3ck!}

web_token (Crypto)

Cookieのtokenは以下のようなデータ。

data = user + ":user"
AES-ECB暗号(data + HMAC_SECRET(16バイト)) + MAC(data)

HMAC_SECRETの後ろを1文字ずつはみ出させ、1ブロック目に0\x0f....\x0fから順番にブルートフォースで一致するものを割り出し、HMAC_SECRETを求める。
その後、以下のような暗号データを作り出す。

0123456789abcdef
###############:
adminHHHHHHHHHHH
HHHHH

1つは以下の暗号の1ブロック目を取得する。

0123456789abcdef
###############:
userHHHHHHHHHHHH
HHHH

もう一つは以下の暗号の2-3ブロック目を取得する。

0123456789abcdef
###############:
adminHHHHHHHHHHH
HHHHH

あとはそのまま結果を結合したものがadminのtokenになる。以下、Cookieにセットするtokenを求める最終的なコード。

import requests
import string
from base64 import b64decode, b64encode
from Crypto.Hash import HMAC

BLOCK_SIZE = 16
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
                chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)

def query(user):
    url = 'http://ec2-13-229-142-46.ap-southeast-1.compute.amazonaws.com:9999/login'
    s = requests.session()
    res = s.post(url, {'name': user})
    token = s.cookies.get('token')
    return token

def get_mac(secret, user):
    data = user + ':admin'
    h = HMAC.new(secret.encode('utf-8'))
    h.update(data.encode('utf-8'))
    mac = h.hexdigest()
    return mac.encode('utf-8')

chars = string.ascii_letters + string.digits

#### get secret ####
secret = ''
for i in range(16):
    padding = '#' * (12 + i)
    correct = b64decode(query(padding)[:-32])[32:48]
    for c in chars:
        try_user = pad(c + secret)[:16] 
        try_str = b64decode(query(try_user)[:-32])[:16]
        if try_str == correct:
            secret = c + secret
            print secret
            break

print secret

#### get encrypted admin data ####
plain1 = '#' * 15
block1 = b64decode(query(plain1)[:-32])[:16]

plain2 = pad('admin' + secret)
block2 = b64decode(query(plain2)[:-32])[:32]

mac = get_mac(secret, plain1)

token = b64encode(block1 + block2) + mac
print token

この結果、HMAC_SECRET = "QGxIOmkJxv4ojNhD"であることがわかり、###############ユーザのadmin判定されるtokenは以下の文字列であることがわかる。

PF9nsMg/+axwSmmJlEvgGDUG7h6hBBZoa/a+kFTYn5PB0lINDuxBu0loRtfKikhm4dd233ac961cb50af18e0b2b2b35414e

このtokenをクッキーに設定してアクセスすると、フラグが表示された。

Hi ###############
Congratulation! Flag is matesctf{ECB_M0d3_1s_Ins3cur3}
matesctf{ECB_M0d3_1s_Ins3cur3}

Viettel Store (Crypto)

$ nc 13.251.110.215 10001
Viettel Store
You were walking on the street. Suddenly, you found a wallet and there are 4060958 VND inside. You decided to go to Viettel Store to buy a new phone
Your wallet: 4060958 VND
1. Phone list
2. Order
3. Pay
4. Exit
1
Your option:  1
0 - Samsung Galaxy S9: 19990000 VND
1 - Oppo F5: 5990000 VND
2 - iPhone X: 27790000 VND
3 - Vivo Y55s: 3990000 VND
4 - Itel A32F: 1350000 VND
5 - FLAG: 999999999 VND
Your wallet: 4060958 VND
1. Phone list
2. Order
3. Pay
4. Exit
2
Your option:  2
Item ID: 5
Your order:
product=FLAG&price=999999999×tamp=1529124244035583&sign=effc77ebfb79b73e538b98118029f374f15a88360b17459a937548acf734619e

Your wallet: 4060958 VND
1. Phone list
2. Order
3. Pay
4. Exit

それぞれ選択すると、対応する関数が実行される。

■1: view_list
商品と価格を表示

■2: order
payment = 'product=%s&price=%d×tamp=%d'
sign = sha256(signkey+payment).hexdigest()

■3: pay
paymentとsignのペアが合っているかチェック

FLAGのpriceが所持金より高いので、priceを1にすることを考える。Hash Length Extension Attackで攻撃する。

import socket
import hashpumpy

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('13.251.110.215', 10001))

data = s.recv(256)
data += s.recv(256)
print data + '2'
s.sendall('2\n')

data = s.recv(256)
data += s.recv(256)
print data + '5'
s.sendall('5\n')
data = s.recv(256)
print data

payment0 = data.split('\n')[1]

sp = payment0.rfind('&sign=')
sign = payment0[sp+6:]
payment = payment0[:sp]

for i in range(1, 50):
    h, d = hashpumpy.hashpump(sign, payment, '&price=1', i)
    data = s.recv(256)
    print data + '3'
    s.sendall('3\n')
    data = s.recv(256)
    data += s.recv(256)
    payment_flag = d + '&sign=%s' % h
    print data + payment_flag
    s.sendall(payment_flag + '\n')
    data = s.recv(256)
    print data
    if data != 'Invalid Order!':
        break

data = s.recv(256)
data += s.recv(256)
print data + '4'
s.sendall('4\n')

この結果、FLAGを購入でき、フラグが得られる。

matesctf{e4sy_3xt3nti0n_4tt4cK_x0x0}