Posted onInctfViews: Disqus: Word count in article: 1.2kReading time ≈4 mins.
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?
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.allimport *
PCAP_FILE = "web_shell.pcap"
packages = rdpcap(PCAP_FILE) for i, p inenumerate(packages): if p.haslayer(Raw): raw = p[Raw].load ifb"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.1200 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.
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.
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 base64 import re import urllib.parse import zlib
defextract_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() ifnot 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) ifmatch: current_offset = int(match.group(1))
# Match Response: If we have an offset, convert hex response to char elif current_offset != -1and 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
defprocess_pcap(packets, regex): """Iterates through packets, searching for encrypted payloads via regex.""" print(f"[*] Analyzing {len(packets)} packets...")
for i, p inenumerate(packets): if p.haslayer(Raw): raw_payload = p[Raw].load match = re.search(regex, raw_payload) ifmatch: 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)
defprint_final_flag(): """Sorts fragments by offset and prints the assembled flag.""" ifnot flag_data: print("\n[-] No flag fragments found in the analyzed packets.") return
sorted_chars = [flag_data[k] for k insorted(flag_data.keys())] final_flag = "".join(sorted_chars)
defcheck_packet_by_content(packets, content): """Heuristic search for specific raw bytes in a PCAP.""" for i, p inenumerate(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