Viettel Mates CTF 2018 - Viettel Store

Solves: 74 | Points: 100 | Category: Crypto

Challenge description

Thank God It’s Weekend! Let’s go shopping!
nc 13.251.110.215 10001

Challenge resolution

For this challenge, we were also provided the following Python code :

import time
import string
import random
from hashlib import sha256
from urlparse import parse_qsl

money = random.randint(1000000, 5000000)
signkey = ''.join([random.choice(string.letters+string.digits) for _ in xrange(random.randint(8,32))])
items = [
('Samsung Galaxy S9', 19990000),
('Oppo F5', 5990000),
('iPhone X', 27790000),
('Vivo Y55s', 3990000),
('Itel A32F', 1350000),
('FLAG', 999999999)
]

def view_list():
for i, item in enumerate(items):
print "%d - %s: %d VND" % (i, item[0], item[1])

def order():
try:
n = int(raw_input('Item ID: '))
except:
print 'Invalid ID!'
return
if n < 0 or n >= len(items):
print 'Invalid ID!'
return
payment = 'product=%s&price=%d&timestamp=%d' % (items[n][0], items[n][1], time.time()*1000000)
sign = sha256(signkey+payment).hexdigest()
payment += '&sign=%s' % sign
print 'Your order:\n%s\n' % payment

def pay():
global money
print 'Your order: '
payment = raw_input().strip()
sp = payment.rfind('&sign=')
if sp == -1:
print 'Invalid Order!'
return
sign = payment[sp+6:]
payment = payment[:sp]
signchk = sha256(signkey+payment).hexdigest()
if signchk != sign:
print 'Invalid Order!'
return

for k,v in parse_qsl(payment):
if k == 'product':
product = v
elif k == 'price':
try:
price = int(v)
except:
print 'Invalid Order!'
return

if money < price:
print 'Sorry, you don\'t have enough money'
return

money -= price
print 'Your current money: $%d' % money
print 'You have bought %s' % product
if product == 'FLAG':
print 'Good job! Here is your flag: %s' % open('flag').read().strip()

def main():
print 'Viettel Store'
print 'You were walking on the street. Suddenly, you found a wallet and there are %d VND inside. You decided to go to Viettel Store to buy a new phone' % money
while True:
print 'Your wallet: %d VND' % money
print '1. Phone list'
print '2. Order'
print '3. Pay'
print '4. Exit'
try:
inp = int(raw_input())
print 'Your option: ', inp
if inp == 1:
view_list()
elif inp == 2:
order()
elif inp == 3:
pay()
elif inp == 4:
break
except:
break

if __name__ == '__main__':
main()

As we can see, in order to retrieve the flag from the server, we have to buy it.
However, its price is much more than what we can afford.
Indeed, we can see that we will get 5000000 VND at max while the price of the flag is 999999999 VND.

The idea here is to trick the server by ordering the flag but with a different price, one we can afford.
Let’s say 0 VND for instance (or even a negative value if you are really really greedy for money…).

We notice an interesting portion inside the code :

for k,v in parse_qsl(payment):
if k == 'product':
product = v
elif k == 'price':
try:
price = int(v)
except:
print 'Invalid Order!'
return

If we send an order containing two prices, the second one will overwrite the first one.

For instance :

product=FLAG&price=999999999&timestamp=1529259896444814&price=0&sign=6855f5f611b2b1be9319bd62e6e204ddb4cdd675a5c90cefe2e49d5f744c3d0b

Unfortunately, a signature based on the order string is generated and verified by the server to ensure that the order has not been tampered with.
The signature is generated server side using a private signing key prepended to our data, a key which we cannot retrieve.
But, we know the plaintext (our order) and the hash signature.
As mentionned before, we also know that the secret key is prepended to our data.

All of this lead us to to a hash length extension attack.
I won’t describe the attack in details since someone else has already done that in such a manner that I wouldn’t be clearer : Link - Skullsecurity Blog.

Briefly, it consists in exploiting the way our data is padded so that it can be SHA-256-ed (yeah I just invented a word, you know what I mean).
By playing with the padding, we can forge a valid order containing custom padded data along with a valid signature.

HashPump is a tool for making hash length extension attacks easier.
In our case, we will use its Python bindings.
All we need to provide to the tool are the following :

  • the original order
  • its signature
  • the data we want to append (the modified price in our case)
  • the length of the server private key used for signing

We don’t know the exact length of the key but since the length varies between 8 and 32, we can just bruteforce it.

Eventually, we come up with the following exploit script :

#!/usr/bin/env python

from pwn import *
import hashpumpy
import re

# Menu
def menu(choice1, choice2):
r.recvuntil('Exit\n')
r.sendline(choice1)
if choice1 == '2':
r.recvuntil('ID: ')
r.sendline(choice2)
return r.recv().replace('Your order:\n', '')
elif choice1 == '3':
r.recvuntil('order:')
r.sendline(choice2)
r.recv()
return r.recvuntil('Your wallet')
return r.recv()

# Hash length extension attack
def gen_exploit(order, payload, keylength):
sp = order.rfind('&sign')
sign = order[sp+6:].rstrip()
payment = order[:sp]
hash, message = hashpumpy.hashpump(sign, payment, payload, keylength)
return message + '&sign=' + hash

# Pwny pwny run run
r = remote('13.251.110.215', 10001)
order = menu('2', '5') # order the flag
for keylength in range(8,33):
exploit = gen_exploit(order, '&price=0', keylength)
ans = menu('3', exploit)
if 'Invalid' not in ans:
log.success('Flag : ' + re.findall('matesctf{.*}', ans)[0])

Running the script, we get the flag :

florent@kali:~# python viettel.py 
[+] Opening connection to 13.251.110.215 on port 10001: Done
[+] Flag : matesctf{e4sy_3xt3nti0n_4tt4cK_x0x0}
[*] Closed connection to 13.251.110.215 port 10001