After recently learning about crypto challenges, I saw this challenge and the premise looked similar to what I had seen before. There was a script that verifies user input by requiring a signature hash to actually execute the code. This meant I had to somehow forge my own payload that would pass the validation check, and execute arbitrary code on the server.
The Challenge
The basic idea was a program that allowed users to execute a SQL script, but only if it was signed with the secret salt value. This salt was randomly generated with os.urandom(randint(8,100))
so it's not feasible to crack.
The main.py
script:
from random import randint
import json
import hashlib
import os
from util import executeScript
salt = os.urandom(randint(8,100))
def create_sample_signature():
dt = open('sample','rb').read()
h = hashlib.sha512( salt + dt ).hexdigest()
return dt.hex(), h
def check_signature(dt, h):
dt = bytes.fromhex(dt)
if hashlib.sha512( salt + dt ).hexdigest() == h:
return True
def challenge():
print("Welcome to Santa's database maintenance service.\
Please make sure to get a signature from mister Frost.\
")
while True:
try:
print('1. Get a sample script\
2. Update maintenance script.\
> ')
option = input().strip()
if option=='1':
data, sign = create_sample_signature()
payload = json.dumps({'script': data, 'signature': sign})
print(payload + '\
')
elif option=='2':
print('Please send your script and its signature.\
> ')
resp = input().strip()
resp = json.loads(resp)
if check_signature(resp['script'], resp['signature']):
script = bytes.fromhex(resp['script'])
res = executeScript(script)
print(res+'\
')
else:
print('Are you sure mister Frost signed this?\
')
else:
print('There is no such an option.\
')
exit(1)
except Exception as e:
print(e)
print('Invalid payload. Bye!')
exit(1)
def main():
try:
challenge()
except:
pass
if __name__ == "__main__":
main()
This references another script called util.py
with the following code:
import mysql.connector
def executeScript(script):
mydb = mysql.connector.connect(
host="localhost",
user="root",
password=""
)
mycursor = mydb.cursor()
lines = script.split(b'\
')
resp = ''
for line in lines:
print(line)
line = str(line)[2:-1]
mycursor.execute(line)
for x in mycursor:
resp +=str(x)
mydb.close()
return resp
This util script just executes the script in the SQL Server that is running, line by line.
For every payload, we need a script
and signature
value in JSON format.
The problem here is the fact that our input first gets passed to check_signature
which checks if our script with the salt prepended matches the signature. Since we don't know the salt value, it should be impossible for us to forge such a message.
The Length Extension Attack
Terms like SHA-256 or even SHA-512 might sound very secure, but all members of the SHA family have a vulnerability where an attacker can append arbitrary data to the hash and create a valid signature. All the attacker needs are the length of the salt (which is guessable) and a hash with the salt to append data to.
The idea of the attack is that after shuffling the bits around with SHA, the hash is just the internal registers appended to each other. This means we can take the hash and get back the registers' end values. Then we can just continue the normal SHA process with those registers to append data to the hash as if it was prepended with the salt. You can look for more information about this attack online.
With this attack we can append any data we want to the hash like this:
If we then submit the forged_hash
and sample_script+payload
the check will append the salt
to it and validate that the hash is correct.
Note: The actual attack also uses padding in between the sample_script
and payload
, to make sure everything lines up correctly. But using the length of the salt we can generate the correct to create the signature.
Simple version
Since this is a popular attack, a tool called hashpump was created to easily execute this attack. It also contains a Python module called hashpumpy
that we can use to automate this process.
Because we have the challenge script, we can change it a bit to make it easier for ourselves, and then eventually step up from there.
I changed the salt value to a constant value so the hashes are the same every time I run it. I set the value to the following string (28 characters) in the script:
salt = b"SomerandomsaltvalueIdontknow"
This helped with testing my attack, so I can brute force the length later.
The challenge gives us a sample script and hash combination to start. We can use this with hashpump to generate our payload.
Welcome to Santa's database maintenance service.
Please make sure to get a signature from mister Frost.
1. Get a sample script
2. Update maintenance script.
> 1
{"script": "55534520786d61735f77617265686f7573653b0a234d616b65207375726520746f2064656c6574652053616e74612066726f6d2075736572732e204e6f7720456c7665732061726520696e206368617267652e", "signature": "ad6059676e3d3e25bd8dbcde3b16c92c94fac75d8ce4603e32e46eab9fafba0a8f9b6951ac516a62f9b837bfa57cc7760ad3b34e526a7380f35253d59f9f7f47"}
The hashpump()
function expects 4 arguments: original_sig
, original_data
, data_to_add
and salt_length
. The first two are given to us for free, in the sample script (with the script expected in bytes form instead of hex). The data we want to end will eventually be SQL code to execute on the server, but for now we'll just use some test value to see if it passes the signature check. Finally, the salt length is the value we just set hardcoded to 28 for ease. Putting all these together into a script looks something like this:
from hashpumpy import hashpump
sample = {"script": "55534520786d61735f77617265686f7573653b0a234d616b65207375726520746f2064656c6574652053616e74612066726f6d2075736572732e204e6f7720456c7665732061726520696e206368617267652e", "signature": "ad6059676e3d3e25bd8dbcde3b16c92c94fac75d8ce4603e32e46eab9fafba0a8f9b6951ac516a62f9b837bfa57cc7760ad3b34e526a7380f35253d59f9f7f47"}
original_sig = sample['signature']
original_data = bytes.fromhex(sample['script'])
data_to_add = "test123" # Our payload
salt_length = 28 # Hardcoded to "SomerandomsaltvalueIdontknow"
result = hashpump(original_sig, original_data, data_to_add, salt_length)
# hashpump gives tuple of signature and data, so unpack into JSON format
payload = {
'script': result[1].hex(),
'signature': result[0]
}
print(payload)
Giving this payload to the modified challenge, we get a SQL error saying it can't connect to the SQL server because I don't actually have one running locally. But this means our code gets passed to the executeScript()
function and will get executed on the real remote server.
Solution
Now that we can forge a message with a known salt length, we can revert the challenge to the original state that generates a random salt from 8 to 100 characters. This is a low range though, and since we can keep trying to update the maintenance script we can just brute force every possible length.
Of course, we don't want to do that manually though, so I created a script using pwntools
that executes this attack for every possible salt length.
With some receiving and sending to the process or the remote server, we end up with something like this:
from pwn import *
from hashpumpy import hashpump
import json
r = process('./challenge.py')
# r = remote('188.166.158.108', 30301)
r.recvuntil(b'> ') # Welcome
r.sendline(b'1') # Get sample script
r.recvline()
sample = json.loads(r.recvline().decode().strip())
original_sig = sample['signature']
original_data = bytes.fromhex(sample['script'])
data_to_add = b"\
SELECT * FROM materials"
for i in range(8, 100):
salt_length = i
result = hashpump(original_sig, original_data, data_to_add, salt_length)
result = {
'script': result[1].hex(),
'signature': result[0]
}
r.recvuntil(b'> ')
r.sendline(b'2') # Choose update
r.recvuntil(b'> ')
r.sendline(bytes(json.dumps(result), 'utf-8')) # Send payload
r.recvline()
response = r.recvline()
if response != b'Are you sure mister Frost signed this?\
': # If not wrong
print("salt length:", salt_length)
print(response)
break
This script first fetches the sample script, and then generates possible signatures for the guessed salt_length
. If the response is something other than the error message, we know it passes the check and executes as SQL code.
The final thing to do now is to use some MySQL enumeration techniques to read the data in the database and get our flag.
A good start is to look at the table names. Using SELECT TABLE_NAME FROM information_schema.tables
we can get all the tables. The tables called users
and materials
look interesting. I first tried SELECT * FROM users
which yielded some results, but nothing that looked like the flag. Then I did the same for materials with SELECT * FROM materials
and in that data it included the flag!
HTB{h45hpump_15_50_c001_h0h0h0}