176 lines
5.7 KiB
Python
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()
|