Introduction Link to heading
The following challenge details a crypto challenge hosted on HackTheBox
Challenge Name: Secure Signing
Challenge Points: 20 points
Challenge Diff: Easy
Challenge Creator: [azan121468](https://app.hackthebox.com/users/377797)
It is an easy challenge that tests the various properties of XOR and its application and misconfiguration.
The Challenge Link to heading
Looking through we have the following:
─$ ls
'Secure Signing.zip' server.py
Opening it, we get the following script
Server.py Link to heading
from hashlib import sha256
from secret import FLAG
WELCOME_MSG = """
Welcome to my Super Secure Signing service which uses unbreakable hash function.
We combine your Cipher with our secure key to make sure that it is more secure than it should be.
"""
def menu():
print("1 - Sign Your Message")
print("2 - Verify Your Message")
print("3 - Exit")
def xor(a, b):
return bytes([i ^ j for i, j in zip(a, b)])
def H(m):
return sha256(m).digest()
def main():
print(WELCOME_MSG)
while True:
try:
menu()
choice = int(input("> "))
except:
print("Try again.")
continue
if choice == 1:
message = input("Enter your message: ").encode()
hsh = H(xor(message, FLAG))
print(f"Hash: {hsh.hex()}")
elif choice == 2:
message = input("Enter your message: ").encode()
hsh = input("Enter your hash: ")
if H(xor(message, FLAG)).hex() == hsh:
print("[+] Signature Validated!\n")
else:
print(f"[!] Invalid Signature!\n")
else:
print("Good Bye")
exit(0)
if __name__ == "__main__":
main()
The script appears to work in the following ways:
- It imports sha256 class from hashlib and FLAG from secret
- It initializes a number of functions such as the custom
xorfunction and uses it in themainfunction - It then calls the main function which acts a
shellof sort,providing interface between the user and the server - The function provides the user with the chance of encoding(xoring) their message with the
FLAGas much as they want(Possible bruteforce detected) and then finally returning its hash value - Everything else, seems to be a rabbit hole (The 2nd option)
NB:
- The XOR function implements the
ZIPfunction. The zip function is unique and ensure that both iterables(anything that can be looped) are always the same length. This implies that:
if len(message) < len(flag):
flag = flag[:len(message)]
elif len(flag) < len(message):
message = message[:len(flag)]
- The digest value of sha256 is returned and not the hexdigest but it is still hexed so we can use the hexdigest
The attack Link to heading
The script has a vulnerability(an exploitation vector) provided by the 1st option. Why? It all stems out from the Identity Element property of XOR. This means:
A xor 0 = A
#Proof
>>> A = 10
>>> xor(A,0)[0] == A
True
This proves that if we send b"\x00" which is the bytes equivalent of 0 , we should get back the hash corresponding to a part of the flag. Since the xor function loops over individual bytes:```
(first byte of msg ^ first byte of flag) = first_enc value
(sec byte of msg ^ sec byte of flag) = sec_enc value ...
We can be able to extract piece by piece of the flag and compare it to our individual flag and hence obtain values of the flag through bruteforce by sending various lengths of b"\x00".
The Proof of Concept Link to heading
We check if our attack actually works and if we are able to get corresponding parts of the flag.
First, I’ll modify the server.py and introduce a secret.py inorder for this to work first on our local machine.
- Server.py (Modified)
from hashlib import sha256
from secret import FLAG
import socket
import threading
WELCOME_MSG = """
Welcome to my Super Secure Signing service which uses unbreakable hash function.
We combine your Cipher with our secure key to make sure that it is more secure than it should be.
"""
def xor(a, b):
return bytes([i ^ j for i, j in zip(a, b)])
def H(m):
return sha256(m).digest()
def handle_client(client_socket):
client_socket.send(WELCOME_MSG.encode())
while True:
client_socket.send("\n1 - Sign Your Message\n2 - Verify Your Message\n3 - Exit\n> ".encode())
choice = client_socket.recv(1024).decode().strip()
if choice == '1':
client_socket.send("Enter your message: ".encode())
message = client_socket.recv(1024).strip()
hsh = H(xor(message, FLAG)).hex()
client_socket.send(f"Hash: {hsh}\n".encode())
elif choice == '2':
client_socket.send("Enter your message: ".encode())
message = client_socket.recv(1024).strip()
client_socket.send("Enter your hash: ".encode())
hsh = client_socket.recv(1024).strip().decode()
if H(xor(message, FLAG)).hex() == hsh:
client_socket.send("[+] Signature Validated!\n".encode())
else:
client_socket.send("[!] Invalid Signature!\n".encode())
elif choice == '3':
client_socket.send("Good Bye\n".encode())
client_socket.close()
break
else:
client_socket.send("Try again.\n".encode())
def main():
try:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 1337))
server.listen(5)
print("[*] Listening on 0.0.0.0:1337")
while True:
client_socket, addr = server.accept()
print(f"[*] Accepted connection from {addr[0]}:{addr[1]}")
client_handler = threading.Thread(target=handle_client, args=(client_socket,))
client_handler.start()
except KeyboardInterrupt:
print("\n[*] Server shutting down.")
server.close()
if __name__ == "__main__":
# Check for required modules
try:
import socket
import threading
from hashlib import sha256
from secret import FLAG
except ImportError as e:
print(f"Error: {e.name} module is required but not installed.")
exit(1)
main()
- secret.py –> Requires to be in byte form else we cannot XOR it properly
FLAG = b"HTB{FAKE_FLAG_FOR_TESTING}"
We will be testing using pwntools as it provides various tools which are easy to use and already-built in. Knowing very well that the first few bytes of the flag(this helps sometimes in other challenges) contain HTB{ , we can use that so that we verify our attack.Let us see:
from pwn import xor,connect
from hashlib import sha256
# Sets up connection between the server and us
host,port = "127.0.0.1:1337".split(":")
rem = connect(host,port)
flag_part = b"HTB{"
# Verifying
part_1 = flag_part[:1] # H
rem.sendlineafter(b">", b"1") # Data should be sent as bytes as the server decodes the bytes and interprets it
rem.sendlineafter(b":", b"\x00" * 1) # Only one byte is required as the message is only 1 byte long
hash_1 = rem.recvuntil(b"\n").strip().split()[-1].decode() # Extracts the hash
print(f"Test 1: {hash_1 == sha256(part_1).hexdigest()}") # Does the first test (Is our first attack true)
part_2 = flag_part[:2] # HT
rem.sendlineafter(b">", b"1") # Data should be sent as bytes as the server decodes the bytes and interprets it
rem.sendlineafter(b":", b"\x00" * 2) # 2 bytes needed as we have 2 bytes of messgae
hash_2 = rem.recvuntil(b"\n").strip().split()[-1].decode() # Extracts the hash
print(f"Test 2: {hash_2 == sha256(part_2).hexdigest()}") # Does the first test (Is our first attack true)
part_3 = flag_part[:3] # HTB
rem.sendlineafter(b">", b"1") # Data should be sent as bytes as the server decodes the bytes and interprets it
rem.sendlineafter(b":", b"\x00" * 3) # We need 3 bytes since we have 3 bytes of message(HTB)
hash_3 = rem.recvuntil(b"\n").strip().split()[-1].decode() # Extracts the hash
print(f"Test 3: {hash_3 == sha256(part_3).hexdigest()}") # Does the first test (Is our first attack true)
rem.close()
Upon testing we get:
└─$ python3 sol.py
[+] Opening connection to 127.0.0.1 on port 1337: Done
Test 1: True
Test 2: True
Test 3: True
[*] Closed connection to 127.0.0.1 port 1337
So that means the idea is right and now we can write our exploit. NB:
- Notice that we knew the values of the flag before we run the sol.py. After the
{We know nothing, so we need to check every value of hash with a suspected value (bruteforce method). - We need to optimize it (making it seem fast and in the process) clean.
The Solution Link to heading
Keeping all that in mind, let us write the final script and run it:
from pwn import remote,string
from hashlib import sha256
# Sets up connection between the server and us
host,port = "127.0.0.1:1337".split(":")
rem = remote(host,port)
flag = "H"
progress_report = rem.progress(flag)
def bruteforce(flag_part):
rem.sendlineafter(b">", b"1") # Data should be sent as bytes as the server decodes the bytes and interprets it
rem.sendlineafter(b":", b"\x00" * (len(flag_part) + 1)) # So that we can retrieve the next value in the flag --> Add 1
hash_value = rem.recvuntil(b"\n").strip().split()[-1].decode() # Extract hash --> Obtained through initial testing
# Bruteforce every value in printable characters
for char in string.printable:
# Get the hash value of the flag+char and test it
char_hash = sha256((flag_part+char).encode()).hexdigest()
if char_hash == hash_value:
return flag_part + char
break
return flag_part
# The last character is obviously } so that kills the loop when it is achieved
while "}" not in flag:
flag = bruteforce(flag)
progress_report.status(flag)
rem.close()
Running it, we get:
└─$ python3 sol.py
[+] Opening connection to 127.0.0.1 on port 1337: Done
[+] H: HTB{FAKE_FLAG_FOR_TESTING}
[*] Closed connection to 127.0.0.1 port 1337
And then we can test it on the sever
Server-Side Link to heading
Replace the string with the copied address from HackTheBox (just copy, delete and paste). Upon running:
└─$ python3 sol.py
[+] Opening connection to 159.65.20.166 on port 30280: Done
[<] H: HTB{r0ll1n6_0v3r_x0r_w17h_h@5h1n6_0r@cl3_15_n07_s3cur3!@#}
[*] Closed connection to 159.65.20.166 port 30280
And like that, we get the flag.
Notes Link to heading
A lot of testing is first required, understanding the server.py is crucial to solving the challenge and you are mandated to know what to exploit.
Writing the solution also requires testing and debugging as you are required to fully understand what to do.
Always have an attack plan that you know is going to work
“Whatever the mind of man can conceive and believe, it can achieve.” — Napoleon Hill