Intent - Mitm
Challenge description:
You are given 2 files, the first is the client code and a PCAP file This pcap file contains the communication with the server, and somehow, among all that traffic, the client receives the flag from the server Sadly the server is NO LONGER RUNNING as someone spilled some Monster on it because they thought it would make it run faster, so the PCAP file and the client is all we have to go off, good luck!
import struct
import socket
import random
import pickle
import base64
import marshal
import types
import argparse
from sys import exit
def send_msg(sock, msg):
msg = struct.pack('>I', len(msg)) + msg
sock.sendall(msg)
def recv_msg(sock):
raw_msglen = sock.recv(4)
if not raw_msglen:
return None
msglen = struct.unpack('>I', raw_msglen)[0]
return sock.recv(msglen)
def enc_msg(key, msg: bytes):
if type(msg) == str:
msg = msg.encode('utf-8')
if type(key) == str:
key = int(key)
char_based_key = key % 256
ret = []
for char in msg:
ret.append(char ^ char_based_key)
char_based_key = (char_based_key + 13) % 256
return bytes(ret)
def dec_msg(key, msg):
return enc_msg(key, msg)
def whitefield_command(variables, variable, value):
variables[variable] = value
def amir_command(variables, variable, value):
if not variable in variables:
variables[variable] = []
variables[variable].append(value)
def exit_command(variables, variable, value):
flag = ''
enc_key = pickle.loads(base64.b64decode(value))
enc_secret = variables[variable]
# decrypt secret
for i in range(len(enc_secret)):
flag += chr(enc_secret[i] ^ enc_key[i])
# print(flag)
# exit(0)
# variables['flag'] = flag
class CommandHandler:
def __init__(self) -> None:
self.variables = {}
self.commands = {}
self.commands['whitefield'] = whitefield_command
self.commands['amir'] = amir_command
self.commands['exit'] = exit_command
self.commands['add_new_command'] = self.add_new_command
def add_new_command(self, command, func):
func = marshal.loads(base64.b64decode(func))
self.commands[command] = types.FunctionType(func, globals(), command)
def handle_command(self, command):
if type(command) == bytes:
command = command.decode('utf-8')
command = command.split(' ')
if len(command) == 0:
return
if command[0] == 'add_new_command':
self.add_new_command(command[1], command[2])
return
if command[0] in self.commands:
func_to_call = self.commands[command[0]]
if command[0] != 'add_new_command' and command[0] != 'exit':
command[2] = int(command[2])
func_to_call(self.variables, command[1], command[2])
else:
print("Unknown command: ", command[0])
def main():
# parser = argparse.ArgumentParser(description='Client for the AI nation of 0xearth')
# parser.add_argument('--port', type=int, default=8097, help='Port to connect to')
# args = parser.parse_args()
# Create a socket object
s = socket.socket()
# Define the port on which you want to connect
port = 8097 # args.port
print(f'Connecting to port {port} on localhost')
# connect to the server on local computer
try:
s.connect(('13.37.13.37', port))
except ConnectionRefusedError:
print("Connection refused, exiting...")
exit()
if not s.getsockname()[0].startswith('13.37'):
exit()
try:
# Diffie Helman Key Swap
modulus = int(recv_msg(s).decode())
base = int(recv_msg(s).decode())
# print(f"Received modulus: {modulus}, base: {base}")
client_secret = (base + 2) * 15
step_A = int(recv_msg(s).decode())
step_B = pow(base, client_secret, modulus)
send_msg(s, str(step_B).encode())
key = pow(step_A, client_secret, modulus)
except Exception as e:
print("Error in key swap, exiting...")
print(e)
exit()
handler = CommandHandler()
handler.variables['key'] = key
if not s.getsockname()[0].endswith('1'):
exit()
command_number = 0
while True:
msg = recv_msg(s)
if not msg:
break
if msg == b"Invalid response":
print("Invalid response, exiting...")
break
msg = dec_msg(key, msg)
if msg == 'exit':
break
if type(msg) == bytes:
msg = msg.decode()
handler.handle_command(msg)
key = handler.variables['key']
command_number += 1
response = "Command number " + str(command_number) + " completed"
response = enc_msg(key, response.encode())
send_msg(s, response)
print("Closing connection... Bye!")
if __name__ == '__main__':
main()
MITM
This challenge contains two files:
- 1 - client.py
- 2 - mitm.pcapng
The client.py
file is the client source code that communicate with the server, and the mitm.pcapng
file is packets from the communication between them,
so we need to understand how to read that packets to reach the flag.
client.py
In this file we can see that the client and the server do Diffie Helman Key Swap before they start any communication, another thing that we can see is that the client and the server communicate under TCP, so that means they’re going to do the Three-Way Handshake of TCP before any other communication.
client protocol:
In the funciton recv_msg()
we can fing the communication protocol between the client and the server,
this protocol is very simple:
[4 bytes] - size
[{size} bytes] - msg
mitm.pcapng
In this file we can see that Three-Way Handshake packets with the known flags:
- SYN
- SYN + ACK
- ACK After that we can see the Diffie Helman Key Swap packets.
Update client.py
So now we update the function recv_msg()
inside the client.py
file and we will let it get its packets data from the mitm.pcapng
file insted from the socket,
you can find the new functions inside the dummy_client.py string.
- Note: To run this script you must install Python3.12
Updated client code
import struct
import socket
import random
import pickle
import base64
import marshal
import types
import argparse
from sys import exit
from scapy.all import rdpcap, IP, Raw
__author__ = "Noam Afergan"
__ctf__ = "Intent CTF 2024"
__challenge__ = "Mitm"
__category__ = "Forensics"
__date__ = "2024-11-19"
main_data = b""
def get_data():
"""
This Generator get packets from the pcapng file and
add them into generat data var that simulate socket communication
by getting the data from this var insted from the socket.
"""
global main_data
packets = rdpcap("mitm.pcapng")
for i, packet in enumerate(packets):
if IP in packet and hasattr(packet[IP], 'payload') and len(packet[IP].payload) > 0:
if packet[IP].src == "13.37.13.37" and Raw in packet:
main_data += bytes(packet[Raw].load)
yield True
yield False
#var that store the generator
data_gen = get_data()
def data_addr(size):
"""
This function add more data into the global main_data var,
this function simulate the recv() data from socket function.
"""
global main_data
while len(main_data) < size:
if not next(data_gen):
print("[*] Error: End of data")
exit(0)
def process_pcap():
"""
This function get the data from the global data var by using this communication protocol:
[4 bytes] - size
[{size} bytes] - msg
"""
global main_data
data_addr(4)
raw_msglen = main_data[0:4]
if not raw_msglen:
return 0
msglen = struct.unpack('>I', raw_msglen)[0]
data_addr(4 + msglen)
res = main_data[4:4 + msglen]
main_data = main_data[4 + msglen:]
return res
def send_msg(sock, msg):
msg = struct.pack('>I', len(msg)) + msg
sock.sendall(msg)
def recv_msg():
"""
update the recv_msg func to return the messages that it recv
from the global main_data that recv them from the pcapng file insted from the real socket.
"""
try:
return process_pcap()
except StopIteration:
print("No more packets in PCAP file.")
return None
def enc_msg(key, msg: bytes):
if type(msg) == str:
msg = msg.encode('utf-8')
if type(key) == str:
key = int(key)
char_based_key = key % 256
ret = []
for char in msg:
ret.append(char ^ char_based_key)
char_based_key = (char_based_key + 13) % 256
return bytes(ret)
def dec_msg(key, msg):
return enc_msg(key, msg)
def whitefield_command(variables, variable, value):
variables[variable] = value
def amir_command(variables, variable, value):
if not variable in variables:
variables[variable] = []
variables[variable].append(value)
def exit_command(variables, variable, value):
flag = ''
enc_key = pickle.loads(base64.b64decode(value))
enc_secret = variables[variable]
# decrypt secret
for i in range(len(enc_secret)):
flag += chr(enc_secret[i] ^ enc_key[i])
print(flag)
exit(0)
variables['flag'] = flag
class CommandHandler:
def __init__(self) -> None:
self.variables = {}
self.commands = {}
self.commands['whitefield'] = whitefield_command
self.commands['amir'] = amir_command
self.commands['exit'] = exit_command
self.commands['add_new_command'] = self.add_new_command
def add_new_command(self, command, func):
func = marshal.loads(base64.b64decode(func))
self.commands[command] = types.FunctionType(func, globals(), command)
def handle_command(self, command):
if type(command) == bytes:
command = command.decode('utf-8')
command = command.split(' ')
if len(command) == 0:
return
if command[0] == 'add_new_command':
self.add_new_command(command[1], command[2])
return
if command[0] in self.commands:
func_to_call = self.commands[command[0]]
if command[0] != 'add_new_command' and command[0] != 'exit':
command[2] = int(command[2])
func_to_call(self.variables, command[1], command[2])
else:
print("Unknown command: ", command[0])
def main():
try:
# Diffie Helman Key Swap
modulus = int(recv_msg().decode())
base = int(recv_msg().decode())
print(f"Received modulus: {modulus}, base: {base}")
client_secret = (base + 2) * 15
step_A = int(recv_msg().decode())
step_B = pow(base, client_secret, modulus)
key = pow(step_A, client_secret, modulus)
except Exception as e:
print("Error in key swap, exiting...")
print(e)
exit()
handler = CommandHandler()
handler.variables['key'] = key
while True:
msg = recv_msg()
if not msg:
break
if msg == b"Invalid response":
print("Invalid response, exiting...")
break
msg = dec_msg(key, msg)
if msg == 'exit':
break
if type(msg) == bytes:
msg = msg.decode()
handler.handle_command(msg)
key = handler.variables['key']
print("Closing connection... Bye!")
if __name__ == '__main__':
main()