Viettel Mates CTF 2018 - Viettel Store

Solves: 74 | Points: 100 | Category: Crypto

Challenge description

Thank God It’s Weekend! Let’s go shopping!
nc 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():
n = int(raw_input('Item ID: '))
print 'Invalid ID!'
if n < 0 or n >= len(items):
print 'Invalid ID!'
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!'
sign = payment[sp+6:]
payment = payment[:sp]
signchk = sha256(signkey+payment).hexdigest()
if signchk != sign:
print 'Invalid Order!'

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

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

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'
inp = int(raw_input())
print 'Your option: ', inp
if inp == 1:
elif inp == 2:
elif inp == 3:
elif inp == 4:

if __name__ == '__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':
price = int(v)
print 'Invalid Order!'

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

For instance :


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):
if choice1 == '2':
r.recvuntil('ID: ')
return r.recv().replace('Your order:\n', '')
elif choice1 == '3':
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('', 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 
[+] Opening connection to on port 10001: Done
[+] Flag : matesctf{e4sy_3xt3nti0n_4tt4cK_x0x0}
[*] Closed connection to port 10001