CSIT Mini Challenge 2025

The Nian Awakens - Feb

Decrypt to Defend!

Legends speak of enchanted artifacts scattered across the region when Nian was first captured. These artifacts contain vital information of Nian's weaknesses. It's up to you to decrypt their secrets and uncover information to bring the beast down.

Nian is currently resting at 34.123.42.200:80. When you're ready, face the beast and save Chinese New Year 2025.

The following three files were provided:

  • client - Python Executable

  • dev_server - Python Executable

  • 蛇年吉祥.pcapng - Encrypted Network Traffic

DetectItEasy

Using DetectItEasy, we identify that both executables are Python programs packed with PyInstaller. After extracting the Python scripts using PyInstxtractor and decompiling with PyLingual, we get:

  • client.py: Client for connecting to WebSocket server

  • dev_server.py: Development server with encrypted test flag

  • common.py: Shared functions

  • constants.py: Logging configuration and hints

I began by running both executables. The programs downloaded the necessary TLS certificates for mutual TLS (mTLS) connection, but the client is not working.

└─$ ./client
2025-02-09 20:20:06,723 - INFO - DEV_client.crt not found. Downloading...
2025-02-09 20:20:07,180 - INFO - Downloaded file from http://34.57.139.144:80/REVW/DEV_client.crt to DEV_client.crt
2025-02-09 20:20:07,181 - INFO - DEV_client.key not found. Downloading...
2025-02-09 20:20:07,645 - INFO - Downloaded file from http://34.57.139.144:80/REVW/DEV_client.key to DEV_client.key
2025-02-09 20:20:07,645 - INFO - DEV_ca.crt not found. Downloading...
2025-02-09 20:20:08,094 - INFO - Downloaded file from http://34.57.139.144:80/REVW/DEV_ca.crt to DEV_ca.crt

└─$ ./dev_server
2025-02-09 20:20:16,217 - INFO - DEV_server.crt not found. Downloading...
2025-02-09 20:20:16,667 - INFO - Downloaded file from http://34.57.139.144:80/REVW/DEV_server.crt to DEV_server.crt
2025-02-09 20:20:16,667 - INFO - DEV_server.key not found. Downloading...
2025-02-09 20:20:17,282 - INFO - Downloaded file from http://34.57.139.144:80/REVW/DEV_server.key to DEV_server.key
2025-02-09 20:20:17,282 - INFO - DEV_ca.crt found.
2025-02-09 20:20:17,302 - INFO - server listening on 0.0.0.0:33073
2025-02-09 20:20:17,303 - INFO - server listening on [::]:34313
2025-02-09 20:20:17,303 - INFO - Server started...

After reviewing the source code, it became apparent that the program relies on certain environmental variables. I configured these variables as follows:

export LOGGING_VERBOSITY="REG0D" 
export LOGGING_MORE_VERBOSITY="y0uReOnToSomEThinG"
export WS_IP=0.0.0.0
export WS_PORT=12345
# The WebSocket port is depends on the port the server running

The constants.py file provided several hints that trigger under specific conditions:

  • HINT_1: Failure to download TLS certificates from SERVER_IP:SERVER_PORT

  • HINT_2: Invalid SSL certificate detection

  • HINT_3: Failed connection attempt to WS_IP:WS_PORT

HINT_1 = '[HINT] Have you tried REading the binaries? Where are you downloading them from?'
HINT_2 = '[HINT] Have you tried REading the binaries? Is your SSL Context malformed or what?'
HINT_3 = "[HINT] Have you tried REading the binaries? Bruh we can't make a connection..."

Now it works on the local setup.

└─$ ./client
2025-02-08 09:03:54,497 - INFO - DEV_client.crt found.
2025-02-08 09:03:54,497 - INFO - DEV_client.key found.
2025-02-08 09:03:54,497 - INFO - DEV_ca.crt found.
>> Welcome to the chat!

Before proceeding to find flag, I decided to first connect to the remote server. The development certificates do not work with the remote server. From certificate download URL, the "REVW" in http://34.57.139.144:80/REVW/DEV_server.key represented "DEV" in Base64. Following this pattern, we determined that production certificates should be located under "PROD" in Base64 (UFJPRA==).

We retrieved the CA certificates:

wget http://34.57.139.144:80/UFJPRA==/PROD_ca.crt
wget http://34.57.139.144:80/UFJPRA==/PROD_ca.key

Subsequently, we generated client certificates for mTLS:

openssl genrsa -out DEV_client.key 2048
openssl req -new -key DEV_client.key -out DEV_client.csr -subj "/CN=client"
openssl x509 -req -in DEV_client.csr -CA PROD_ca.crt -CAkey PROD_ca.key -CAcreateserial -out DEV_client.crt -days 365 -sha256

After copying PROD_ca.crt to DEV_ca.crt, we can now connect to the remote server.

└─$ export WS_IP=34.123.42.200
└─$ export WS_PORT=80
└─$ ./client
2025-02-09 20:40:02,416 - INFO - DEV_client.crt found.
2025-02-09 20:40:02,416 - INFO - DEV_client.key found.
2025-02-09 20:40:02,416 - INFO - DEV_ca.crt found.
>> Welcome to the chat!

Now, lets go back to find the flag. The flag is found at the dev_server.py by sending a message in the format 5n@k3#88#ENCRYPTION_KEY where the ENCRYPTION_KEY is used to decrypt the encrypted flag using 3DES.

FLAG = '7eb66acfb3652e80ef006143b4e5b6565b84b51355b26e39d2979a3bc873ba394ecae0061bd9522a9639ac4488733ad97d5e5acfb1e3e6f7'
if '5n@k3' not in message:
    # ...
else:
    parts = message.split('#')
    if len(parts) > 2:
        command = parts[1]
        args1 = parts[2]
        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}0')
            except Exception as E:
                logging.debug(E)
                if client == senderId:
                    await clients[client]['listener'].send('s()p3rR00+: flag{this_is_real_definitely_not_fake}')

Brute-forcing doesn't sounds practical here, so the key is likely provided somewhere.

Oh! I completely forgot about the encrypted 蛇年吉祥.pcapng. Now that we have the private key, we can decrypt the traffic directly in Wireshark.

Here are the decrypted messages:

r00t: Hi there, welcome!
r00t: So, you’re here because Nian is coming to kill us all?
r00t: I see… Well, I can help you with that. No worries. :)
r00t: I hope you’ve read enough into the ancient runes—those are your keys to success.
r00t: As for the key itself, it is the weakness of Nian: 红 火 热闹
r00t: take the 汉语拼音 of those 4 characters, then separate them with a "_" and pad them with 4 pluses
r00t: Oh? Missing some bytes?
r00t: Well… oh darn, Nian has come!
r00t: It has bitten off both my legs… I’m dying…
r00t: I don’t have time to tell you the last part of the key, but maybe if you look into the SSL certificates…
r00t: Encoding? I can’t remember… It sounds like the acid found in mandarin oranges and other citrus fruits…
r00t: Urgh… I’m sorry… I’m losing too much blood…
r00t: We are counting on you…

With this, we can construct the key as hong_huo_re_nao++++, but it is only 19 characters long, meaning we're still missing 5 bytes for the full 3DES key.

The messages also mentioned the SSL certificate, so let's revisit the DEV certificates we downloaded.

Interestingly, the extensions in DEV_server.crt seem unusual. Based on the earlier hint, decoding it using Citrix aligns with the given clue.

└─$ openssl x509 -in DEV_server.crt -text -noout
...
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Key Usage:
                Digital Signature, Non Repudiation, Key Encipherment
            X509v3 Subject Alternative Name:
                IP Address:0.0.0.0
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            1.2.12.5:
                ...PBFEJJDMPMFJNMHJJADFPBFEICCHPGFDNGHDJADFPJFMIPCKOKEPMKGPIICNPBFEIFCAOAEFJDDGLDBGPCFHIACFOFEAMFGAJCDHOAEFIPCKOBEEIGCDKGADPCFHJKDPPP
            1.2.12.6:
                ...FKNPHKJMDJPDFGIBCEPDFGJGDDPFFAIBCEKBAEODEGJKDPOOELILCOPIFNNIHNJLDOPEFBJJDMPLFOJCDHPMFJJJDMPNFINNHIIJCMOGEDIBCEOEEBJADFPIFNJNDIOPEK
            1.2.12.7:
                ...MPGKIGCDPFFANFHAJEDBPHFCIDCGPGFDJHDCPLFOJHDCOOELMOGLJKDPPCFHJHDCLHBCPCFHICCHONEIIOCLOGEDMGGDJDDGPNFIJEDBOMEJMMGJJIDNPBFEJMDJPJFMIK
            1.2.12.8:
                ...CPPOFLJPDKPCFHICCHKCAHOLEOIFCAKFAAPGFDJDDGPAFFJPDKPBFEJFDAOGEDMGGDIACFOPEKJNDILNBIPOFLJGDDPPFKJBDEPEFBIHCCOCEHMCGHIMCJOJEMJODLLOBL
            1.2.12.9:
                .$OHECICCHODEGJBDELBBEIDCGLDBGIBCELEBB
...

And using Cyberchef, we obtained the following:

The Last Five Bytes Are Wrong 
The Correct Bytes Combined Together Is Actually 
The Epoch Unix Timestamp In Seconds For Chinese New Year 2025

I was stuck at this step for a long time, as none of the combinations I tried seemed to work.

After extensive trial and error, the correct key turned out to be simply appending the timestamp to the hex representation of hong_huo_re_nao++++

from Crypto.Cipher import DES3
from Crypto.Util.Padding import pad, unpad

def decrypt_3des(ciphertext: bytes, key: bytes):
    iv = ciphertext[:DES3.block_size]
    cipher = DES3.new(key, DES3.MODE_CBC, iv=iv)
    decrypted_data = cipher.decrypt(ciphertext[DES3.block_size:])
    return decrypted_data.decode("utf-8","replace")

def main():
    ciphertext = bytes.fromhex('7eb66acfb3652e80ef006143b4e5b6565b84b51355b26e39d2979a3bc873ba394ecae0061bd9522a9639ac4488733ad97d5e5acfb1e3e6f7')
    key = b"hong_huo_re_nao++++".hex()
    key = key + "1738080000"
    result = decrypt_3des(ciphertext,  bytes.fromhex(key))
    print(f"Message: 5n@k3#88#{key}")
    print(f"Decrypted: {result}")

if __name__ == "__main__":
    main()
└─$ python decrypt.py 
Message: 5n@k3#88#686f6e675f68756f5f72655f6e616f2b2b2b2b1738080000
Decrypted: flag{now_you_are_ready_for_the_real_thing}

Now, sending the same message to the remote server, we finally got the flag! 🎉

└─$ s()p3rR00+: CSIT{f1R3_rEd_C0L0UR_L0UD_N01535_SCARE5_n1@n} 
Cool Batch

It was an incredibly fun challenge! Reversing the Python executable and navigating through the various steps was exciting, but figuring out the key was a pain for me. 😩

There was one hint given by CSIT that I never encountered—turns out, it was related to an issue with pycdc.

Is # WARNING: Decompyle incomplete the bane of your existence? 
A Corgi or Issue #234 can help you with that!

Singapore’s big day is almost here - Sep

Cool Batch

Last updated