
Сквозная Docker-лаборатория, воспроизводящая Apache log4j2 #4255 — обход allowlist в FilteredObjectInputStream через java.rmi.MarshalledObject (нефильтрованная десериализация → RCE) на Log4j 2.26.1 / JDK 17.
FilteredObjectInputStream через java.rmi.MarshalledObjectАвтономная лаборатория в контейнерах, воспроизводящая проблему Apache log4j2 #4255 целиком на официальных артефактах Log4j 2.26.1 на JDK 17, с положительными контролями, защищённым оракулом и проверенной мерой смягчения.
⚠️ Ответственное использование
Эта лаборатория содержит рабочую RCE через десериализацию против на данный момент неисправленной проблемы Log4j (#4255 ОТКРЫТА /
waiting-for-maintainerна момент написания; CVE не присвоен). Она работает полностью внутри одноразовых Docker-контейнеров на вашей машине и не подключается ни к чему внешнему, кроме Maven Central (для загрузки официальных jar-файлов) — никакая цель не затрагивается.
- Первоначальный репортёр (U-Sec / Wujie Security) не раскрывает свой PoC до выхода исправления. Это независимое воспроизведение, созданное для проверки защитных мер и разработки детектирования.
- Не запускайте это против систем, которыми вы не владеете или на тестирование которых у вас нет явного разрешения.
FilteredObjectInputStream (FOIS) в Log4j — это белый список десериализации на основе resolveClass.
В его белый список входит java.rmi.MarshalledObject. MarshalledObject хранит свою полезную нагрузку как
непрозрачный byte[], а MarshalledObject.get() десериализует эту нагрузку на новом, нефильтрованном
ObjectInputStream — поэтому белый список никогда не проверяет внутренний граф объектов.
Log4j запускает это сам: Log4jLogEvent$LogEventProxy (сериализованная сетевая форма LogEvent,
начиная с 2.8) переносит Message события внутри MarshalledObject и автоматически вызывает .get()
во время десериализации (readResolve() → message()). Любое приложение, которое читает сериализованный
LogEvent через FOIS, таким образом выполняет нефильтрованную десериализацию байтов атакующего — и
поскольку message() проглатывает возникающее исключение и возвращается к SimpleMessage, получатель
регистрирует безобидное событие и продолжает работать. Эксплойт бесшумен.
rel/2.26.1)| # | Расположение | Дефект |
|---|---|---|
| 1 | log4j-api …/util/internal/SerializationUtil.java | REQUIRED_JAVA_CLASSES содержит java.rmi.MarshalledObject |
| 2 | log4j-api …/util/FilteredObjectInputStream.java | переопределяет только resolveClass(); нагрузка objBytes объекта MarshalledObject для него невидима |
| 3 | log4j-core …/impl/Log4jLogEvent.java | LogEventProxy.marshalledMessage — это MarshalledObject<Message> |
| 4 | log4j-core …/impl/Log4jLogEvent.java | message() вызывает marshalledMessage.get() (без фильтра) и проглатывает все исключения |
Точная замена устаревшего ObjectInputStreamLogEventBridge из log4j-samples: неаутентифицированный
TCP-приёмник, который читает один сериализованный LogEvent на соединение через FOIS.
Атакующий отправляет один сериализованный объект; оракул — это файл доказательства, записываемый в каталог,
смонтированный только в контейнер приёмника, поэтому его появление доказывает, что код выполнился внутри
приёмника через десериализацию.
| # | Сценарий | Classpath жертвы | jdk.serialFilter | Ожидаемый результат |
|---|---|---|---|---|
| S1 | запрещённый гаджет отправлен на верхнем уровне | + гаджет | нет | отклонение — FOIS применяет свой белый список |
| S2 | тот же гаджет обёрнут в LogEvent | + гаджет | нет | rce — автоматический запуск (нужен класс гаджета на жертве) |
| S3 | сырой CommonsCollections6 на верхнем уровне | log4j + cc-3.2.1 | нет | отклонение — FOIS блокирует CC |
| S4 | CC6 вставлен в MarshalledObject | log4j + cc-3.2.1 только | нет | rce — класс атакующего на жертве не нужен |
| S5 | нагрузка S4 | log4j + cc-3.2.1 | !java.rmi.MarshalledObject | отклонение — мера смягчения |
| S6 | нагрузка S4 | log4j + cc-3.2.1 | maxdepth=5;maxbytes=1000000 | бесшумно — внутренний поток наследует фильтр; CC6 слишком глубокий |
| S7 | нагрузка S4 | log4j + cc-3.2.2 | нет | бесшумно — 3.2.2 отключает десериализацию небезопасных функторов |
rce = код выполнен. reject = FOIS выбросил исключение на внешнем потоке. silent = внешний объект обработан,
код не выполнился (заблокирован глубже, или версия гаджета безопасна).
./run.sh # или: make run
Требования: только Docker (JDK загружается как eclipse-temurin:17-jdk). Скрипт загружает
официальные jar-файлы и проверяет их по SHA-1 из Maven Central перед использованием. Закрепите другую версию
Log4j в уязвимом диапазоне с помощью LOG4J_VERSION=2.20.0 ./run.sh.
LogEvent на
приёмник на основе FOIS. Это не срабатывает от записи строки в лог (в отличие от Log4Shell) — нужны
сырые сериализованные байты, достигающие сокетного моста.LogEvent → writeReplace()/writeObject() в Log4j оборачивает Message
в MarshalledObject → гаджет прячется внутри его непрозрачного byte[] → на приёмнике
readResolve() → message() → MarshalledObject.get() открывает новый нефильтрованный поток →
гаджет → Runtime.exec. src/attacker/Attacker.java (poc2) вставляет чистый граф CommonsCollections6
в objBytes объекта MarshalledObject, поэтому класс атакующего на жертве не нужен.-Djdk.serialFilter='!java.rmi.MarshalledObject' на JVM приёмника (S5).
Оговорка: это также блокирует легитимные сериализованные объекты LogEventProxy (они тоже используют
MarshalledObject) — это эффективно, но не прозрачно для транспорта сериализованных логов.maxdepth/maxbytes. Процессный фильтр распространяется во
внутренний поток MarshalledObject, и глубина там перезапускается, поэтому maxdepth=5 блокирует CC6 (S6),
но неглубокий гаджет пройдёт. Зависит от глубины цепочки, не является границей.MarshalledObject из белого списка и
перенести маршализованное сообщение на фильтрованные writeWrappedObject/readWrappedObject в Log4j.LogEvent на основе FOIS и имеет подходящую версию гаджета
в своём classpath. Обычные развёртывания Log4j не запускают такой приёмник.net.server.TcpSocketServer
был вынесен из log4j-core в 2017 году; отсутствует начиная с 2.9.0). Современные приёмники — это
код приложений/примеров, который моделирует данная лаборатория.uid=0 в лаборатории — это корень контейнера — побега из Docker нет; RCE выполняется как
процесс приёмника.MarshalledObject, обходящий фильтр resolveClass) — известное ранее искусство;
см. обсуждение Apache #4168
(«Ужесточение десериализации Log4j 2.x»). Специфичный для Log4j автоматический запуск — вклад #4255.run.sh переносимый запускатель (с проверкой контрольных сумм, защищённый оракул)
Makefile make build | run | clean
src/victim/Receiver.java приёмник логов FOIS (замена ObjectInputStreamLogEventBridge)
src/attacker/Attacker.java построитель нагрузок: контроли, PoC-1, PoC-2 (CC6 + вставка байтов)
src/attacker/EvilMessage.java автономный гаджет PoC-1
docs/RESULTS.md матрица доказательств + анализ
Уязвимость сообщена U-Sec (Wujie Security) в Apache log4j2 #4255. Этот репозиторий — это независимая лаборатория воспроизведения/проверки для оборонительных исследований и разработки детектирования.