2026-07-12 20:22:15 +01:00

176 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""
Sandbox Slip - CTF challenge (medium, misc)
Supports two modes:
- TCP (default): listens on HOST:PORT and serves a netcat-style REPL for each connection.
- STDIN: reads lines from stdin and writes results to stdout (local interactive mode).
Usage:
python3 server.py # TCP mode (default)
python3 server.py --mode stdin # stdin/stdout REPL
python3 server.py --host 0.0.0.0 --port 9000
"""
import socketserver
import threading
import sys
import traceback
import os
import argparse
HOST = "0.0.0.0"
PORT = 9000
FLAG_FILE = "flag.txt"
WELCOME = """Welcome to PySafe REPL v0.3
This REPL runs your Python expressions in a restricted environment.
Type a single expression per line. Type QUIT to exit.
Available helper object: helper
>> """
# Vulnerable helper intentionally included to demonstrate sandbox escape.
def helper():
"""A small helper function that does nothing useful for players."""
return "helpful"
# Ensure flag exists
if not os.path.exists(FLAG_FILE):
with open(FLAG_FILE, "w") as f:
f.write("CTF{sandbox_slip_helper_globals}\n")
# Prepare the sandbox environment (shared by TCP and STDIN modes)
# Note: __builtins__ is set to empty dict to simulate a naive sandbox.
safe_globals = {"__builtins__": {}}
safe_locals = {"helper": helper}
def evaluate_source(src: str, g: dict, l: dict):
"""
Evaluate a single-line source in the restricted environment.
Tries eval() first, then exec() for statements.
Returns a tuple (success:bool, output:str).
"""
src = src.rstrip("\n")
if src.strip().upper() == "QUIT":
return True, "QUIT"
if src.strip() == "":
return True, "" # empty line -> no output
try:
# Try eval for expressions
result = eval(src, g, l)
return True, repr(result)
except SyntaxError:
# Try exec for statements
try:
exec(src, g, l)
return True, "OK"
except Exception as e:
tb = traceback.format_exc()
return False, f"Execution error: {e}"
except Exception as e:
return False, f"Error: {e}"
# ---------- TCP handler ----------
class REPLHandler(socketserver.StreamRequestHandler):
def handle(self):
addr = self.client_address[0]
print(f"[+] Connection from {addr}")
# Use instance-local copies so per-connection state can diverge if needed
g = safe_globals
l = safe_locals
try:
self.wfile.write(WELCOME.encode())
while True:
line = self.rfile.readline()
if not line:
break
src = line.decode().rstrip("\n")
if src.strip().upper() == "QUIT":
self.wfile.write(b"Goodbye.\n")
break
ok, out = evaluate_source(src, g, l)
# If source was empty, don't print extra blank line (just prompt)
if out == "":
self.wfile.write(b">> ")
continue
# If QUIT sentinel returned
if out == "QUIT":
self.wfile.write(b"Goodbye.\n")
break
# Send result or error
self.wfile.write((out + "\n").encode())
self.wfile.write(b">> ")
except ConnectionResetError:
pass
except Exception as e:
print("Handler exception:", e)
finally:
print(f"[-] Disconnected {addr}")
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
def run_tcp_server(host: str, port: int):
srv = ThreadedTCPServer((host, port), REPLHandler)
print(f"Sandbox Slip (TCP) listening on {host}:{port}")
try:
srv.serve_forever()
except KeyboardInterrupt:
print("Shutting down TCP server...")
srv.shutdown()
srv.server_close()
# ---------- STDIN / STDOUT loop ----------
def run_stdin_server():
"""
Interactive REPL on stdin/stdout. Useful for local testing.
Reads from sys.stdin, writes responses to sys.stdout, prompts with '>> '.
"""
g = safe_globals
l = safe_locals
sys.stdout.write(WELCOME)
sys.stdout.flush()
try:
while True:
# Read one line from stdin
line = sys.stdin.readline()
if not line:
# EOF
break
src = line.rstrip("\n")
if src.strip().upper() == "QUIT":
sys.stdout.write("Goodbye.\n")
sys.stdout.flush()
break
ok, out = evaluate_source(src, g, l)
if out == "":
sys.stdout.write(">> ")
sys.stdout.flush()
continue
if out == "QUIT":
sys.stdout.write("Goodbye.\n")
sys.stdout.flush()
break
sys.stdout.write(out + "\n")
sys.stdout.write(">> ")
sys.stdout.flush()
except KeyboardInterrupt:
sys.stdout.write("\nInterrupted. Exiting.\n")
sys.stdout.flush()
# ---------- CLI ----------
def main():
parser = argparse.ArgumentParser(description="Sandbox Slip (TCP or STDIN REPL)")
parser.add_argument("--mode", choices=["tcp", "stdin"], default="tcp",
help="Run mode: 'tcp' to listen on network (default), 'stdin' for local REPL")
parser.add_argument("--host", default=HOST, help="Host to bind in TCP mode")
parser.add_argument("--port", type=int, default=PORT, help="Port to bind in TCP mode")
args = parser.parse_args()
if args.mode == "stdin":
run_stdin_server()
else:
run_tcp_server(args.host, args.port)
if __name__ == "__main__":
main()