CSIT Jan 25 Mini Challenge
This blog post talks about CSIT Jan 25 Challenge. (Reverse Engineering + SSL/TLS knowledge)

Introduction
Happy Lunar New Year everyone! Wishing all a prosperous year ahead. Alright, back to the challenge writeup! This was by far the toughest mini-challenge I have completed thus far and I would not have completed it without external help. There are multiple parts to this challenge so lets break it down.
Walkthrough
Analyze "ancient-runes.zip"
Extract the zip file and you'll find 3 files. The first hint is to decompile the files, but before that, lets try running it.
- dev_server
- client
- 蛇年吉祥.pcapng
After running the dev_server file, you'll see that it actually downloads 3 files. Running the client file will also give you a log message that downloads 2 more files.
- http://34.57.139.144:80/REVW/DEV_ca.crt
- http://34.57.139.144:80/REVW/DEV_client.crt
- http://34.57.139.144:80/REVW/DEV_client.key
- http://34.57.139.144:80/REVW/DEV_server.crt
- http://34.57.139.144:80/REVW/DEV_server.key
A total of 5 files are now included in your directory. Lets move on and try to gather some infomation within the files. To unpack the files, I followed a unpacking python executables guide 1. Do note that theres different methods when unpacking different versions of python executables. For our case, I attempted to use Uncompyle6 2 for python < 3.8 but failed as the files are python 3.10. Following the guide, you should have no troubles unpacking the executables and retrieving the partial python source file.
client
A snippet of the code is below. You may encounter some errors like "Unsupported opcode: BEFORE_ASYNC_WITH (94)" when decompiling python files but there is work around it as mentioned in issue #234 3 in Github. Simply replace line 1166 with the error shown in your decompiled python file and it should fix it. Of course, you may face another error, just repeat the steps. If you get a segmentation error, its probably intended and you shouldnt be able to get more information out of the function.
CA_CERT = 'DEV_ca.crt'
CLIENT_CERT = 'DEV_client.crt'
CLIENT_KEY = 'DEV_client.key'
CLIENT_SERVER = f'''http://{SERVER_IP}:{SERVER_PORT}/REVW/'''
CLIENT_PATH = ''
CERT_URL = f'''{CLIENT_SERVER}{CLIENT_CERT}'''
CERT_PATH = f'''{CLIENT_PATH}{CLIENT_CERT}'''
KEY_URL = f'''{CLIENT_SERVER}{CLIENT_KEY}'''
KEY_PATH = f'''{CLIENT_PATH}{CLIENT_KEY}'''
CA_URL = f'''{CLIENT_SERVER}{CA_CERT}'''
CA_PATH = f'''{CLIENT_PATH}{CA_CERT}'''
validate(CERT_URL, CERT_PATH, logging)
validate(KEY_URL, KEY_PATH, logging)
validate(CA_URL, CA_PATH, logging)
context = generate_client_context(CERT_PATH, KEY_PATH, CA_PATH, logging)
async def listen_messages(uri, headers, log):
Unsupported opcode: BEFORE_ASYNC_WITH (94)
pass
# WARNING: Decompyle incomplete
async def send_commands(uri, headers, log):
Unsupported opcode: BEFORE_ASYNC_WITH (94)
pass
# WARNING: Decompyle incomplete
def start_client():
_string = generate_random_string()
headersl = {
'X-ARBOC': f'''{_string}-listener''' }
headerss = {
'X-ARBOC': f'''{_string}-sender''' }
uri = f'''wss://{WS_IP}:{WS_PORT}/'''
listener_thread = None((lambda : asyncio.run(listen_messages(uri, headersl, logging))), **('target',))
listener_thread.daemon = True
listener_thread.start()
asyncio.run(send_commands(uri, headerss, logging))
dev_server
Follow the same steps as you did with the client file and you should get something like this.
FLAG = '7eb66acfb3652e80ef006143b4e5b6565b84b51355b26e39d2979a3bc873ba394ecae0061bd9522a9639ac4488733ad97d5e5acfb1e3e6f7'
def header_extraction(websocket):
headers = websocket.request.headers['X-ARBOC']
parts = headers.split('-', 1)
clientId = parts[0]
clientType = parts[1]
return [clientId,clientType]
async def handler(websocket):
Unsupported use of GET_AITER outside of SETUP_LOOP
Unsupported opcode: END_ASYNC_FOR (98)
parts = header_extraction(websocket)
clientId = parts[0]
clientType = parts[1]
if clientId not in clients:
clients[clientId] = {
clientType: websocket }
else:
clients[clientId][clientType] = websocket
# WARNING: Decompyle incomplete
async def broadcast(message, websocket):
Warning: Stack history is not empty!
Warning: block stack is not empty!
to_remove = []
senderId = header_extraction(websocket)[0]
for client in clients:
try:
if '5n@k3' not in message:
if client == senderId:
await clients[client]['listener'].send(f'''r00t: {message}''')
else:
await clients[client]['listener'].send(f'''{senderId}: {message}''')
elif len(message) <= 5:
if client == senderId:
await clients[client]['listener'].send('s()p3rR00+: run --help')
else:
parts = message.split('#')
if len(parts) > 2:
command = parts[1]
args1 = parts[2]
if command == '90' and len(args1) > 5:
os.system(f'''ls {args1[:5]}''')
if command == '01':
os.system('ip addr')
if command == '33':
os.system('hostname')
if command == '27':
os.system('cat /proc/cpuinfo')
if command == '18':
os.system('whoami ')
if command == '88':
try:
decrypted_message = decrypt_3des(bytes.fromhex(FLAG), bytes.fromhex(args1))
if client == senderId:
await clients[client]['listener'].send(f'''s()p3rR00+: {decrypted_message}''')
finally:
pass
E = None
try:
logging.debug(E)
if client == senderId:
await clients[client]['listener'].send('s()p3rR00+: flag{this_is_real_definitely_not_fake}')
finally:
E = None
del E
E = None
del E
continue
continue
if command == '79':
os.system('dmesg | tail -n 10')
if command == '61':
os.system('ps aux')
if command == '49':
os.system('uptime -p')
if command == '55':
os.system('lscpu')
if command == '72':
os.system(f'''{args1[:2]}''')
continue
else:
to_remove.append(client)
for client in to_remove:
del clients[client]
return None
Hmm, things are starting to look interesting? There seems to be a hard coded flag embedded in the dev_sever that is encrypted by a custom function that hints to be 3DES. The second parameter looks like it has to be a key. The format for the command has to be 5n@k3#command#args1. Command 88 looks particularly interesting, but lets come back to it later. There is also a function called header_extraction and start_client that looks to take in some sort of header in this form 'X-ARBOC': f'''{_string}-listener''' } or 'X-ARBOC': f'''{_string}-sender''' }
Connecting to my local instance that is running dev_server
With all the certs that was previously downloaded, it is likely that we need to use some sort of TLS connection to the server. Lets try using wscat to connect.
wscat --connect wss://0.0.0.0:43055 --key DEV_client.key --cert DEV_client.crt --ca DEV_ca.crt -H "X-ARBOC: my-listener"
Successful connection! I did a test with some of the commands just to see how its reflected on the server side. However, I tried command 88 but was unable to provide a valid key. Also, its likely that the true flag is not hardcoded into this file but actually hosted on the challenge server where NIAN is located. The next step is to connect to the server.

Connecting to the remote challenge server
If you edit the command above to match the IP of the remote server, you'll get a TLS connection error. To debug this error, you can use openssl to test the TLS handshake with this command.
openssl s_client -connect 34.123.42.200:80 -key DEV_client.key -CAfile DEV_ca.crt -cert DEV_client.crt -state
<\RETURNS\>
SSL_connect:SSLv3/TLS read server certificate
SSL_connect:SSLv3/TLS read server certificate request
SSL_connect:SSLv3/TLS read server done
SSL_connect:SSLv3/TLS write client certificate
SSL_connect:SSLv3/TLS write client key exchange
SSL_connect:SSLv3/TLS write certificate verify
SSL_connect:SSLv3/TLS write change cipher spec
SSL_connect:SSLv3/TLS write finished
SSL_connect:error in SSLv3/TLS write finished
write:errno=0
....
....
Verify return code: 19 (self signed certificate in certificate chain)
From this error message, we can deduce that the handshake failed due to the certs being invalid. Lets try to find other certs. Recall earlier that upon running the dev_server file, it downloaded some certs from an endpoint IP/REVW/? I googled REVW but did not get anything back. I threw it into Cyberchef 4 and used the magic function. It turns out that REVW is DEV encoded in b64. The opposite of DEV is PROD (UFJPRA==), lets try to curl it and see if anything returns.
curl http://34.57.139.144:80/UFJPRA==/PROD_ca.crt
curl http://34.57.139.144:80/UFJPRA==/PROD_ca.key

Jackpot! Seems like PROD_ca.crt matches. I tried PROD_ca.key too and it returned the private key for the ca. I tried retrieving some client certs too but none of the paths worked. Theoretically, with both the CA's private and public key, you could sign your own self generated keys. Lets try that.
openssl genpkey -algorithm RSA -out client.key
cat client.key
openssl req -new -key client.key -out client.csr # enter some random information
openssl x509 -req -in client.csr -CA prod.crt -CAkey prod.key -CAcreateserial -out client.crt -days 365 -sha256
openssl verify -CAfile prod.crt client.crt
Now we should have a client.key and a client.crt that is signed by the CA cert that we found from /PROD/. We are finally able to connect to the remote server.
wscat --connect wss://34.123.42.200:80/ --key client.key --cert client.crt --ca prod.crt -H "X-ARBOC:r00t-listener"

Finding the decryption key
After a few failed attempts, I took some time to gather my thoughts. Honestly, I was stuck at this stage, unable to find the key to decrypt the flag. I asked around for some hints and was told that there are clues in the PCAP file. The PCAP file shows the TLS handshake followed by encrpyted application data. To decrypt the data, you have to tell WireShark the private keys for decryption. You can do that by going to Edit -> Preferences -> Protocols tree -> TLS 5 and then attaching the private keys "DEV_client.key" and "DEV_server.key" into the RSA Keys List. Once you press OK, you should see decrypted application data!

Once you've read through all the Line-based text data fields, you should come across these prompts.
- take the 汉语拼音 of those 4 characters, then separate them with a "_" and pad them with 4 pluses
- r00t: encoding???? i cant remember... it sounds like the acid found in mandarin oranges and other citrus fruits...
- i dont have time to tell you the last part of the key... but maybe if you look into the SSL certificates...
Following the instructions, the first part of the key should be: hong_huo_re_nao++++ --> Converted to Hex --> 686f6e675f68756f5f72655f6e616f2b2b2b2b.
In one of the hints provided in the challenge webpage, it says that SSL certs can be used to embed payloads. 6 Lets see if any of the certs have suspicious x509 extensions.
openssl x509 -in DEV_server.crt -text

Throwing all the text into CyberChef and looking for a citrix encoding/decoding scheme (Citrix CTX1 Decode), I managed to see some clear text!

The clear text says that the last 5 bytes are wrong... As of now we have "686f6e675f68756f5f72655f6e616f2b2b2b2b" which is 19 bytes. We are missing another 5 bytes. A 3DES key is 24 bytes in total. In GMT8, "1738080000" is the Unix timestamp for 29 Jan Midnight which is the start of CNY 2025. Lets append the strings together and see if it works.
The Final prompt for the flag
686f6e675f68756f5f72655f6e616f2b2b2b2b combined with 1738080000 = 5n@k3#88#<Insert concat of the 2 strings>
Congrats on finishing till the end! It was a really tough challenge.
