
CVE-2025-59059: Ошибочно приписанная RCE в Apache Ranger — исправление статического анализа
CVE: CVE-2025-59059
Затронутые версии: Apache Ranger <= 2.7.0
Исправлено в: Apache Ranger 2.8.0
CVSS (официально): 9.8 Критический
Фактическая серьёзность (по мнению автора): ~6.0 Средний — см. анализ ниже
Этот разбор не о новом эксплойте или рабочем PoC. Суть здесь в том, чтобы исправить ошибки публичного раскрытия CVE-2025-59059, а именно две вещи: в качестве уязвимого компонента назван не тот класс, и оценка CVSS не отражает реальные ограничения эксплуатируемости. Всё здесь основано на статическом анализе исходного кода Apache Ranger 2.7.0 и диффа патча, вошедшего в 2.8.0.
Официальный advisory гласит:
«Уязвимость удалённого выполнения кода в NashornScriptEngineCreator обнаружена в Apache Ranger версиях <= 2.7.0».
Это неверно или как минимум вводит в заблуждение. NashornScriptEngineCreator — не уязвимый класс. Если на то пошло, это более защищённый из двух связанных с Nashorn компонентов в кодовой базе — к этому мы ещё вернёмся.
Apache Ranger использует JavaScript-выражения для оценки политик фильтрации строк. Это правила, определяющие, получает ли конкретный пользователь доступ к конкретной записи в наборе данных. Чтобы выполнять эти выражения на JVM, Ranger встраивает JavaScript-движок. В версиях <= 2.7.0 этим движком является Nashorn — встроенный JS-движок Oracle, поставлявшийся с JDK 8 по 14.
Одно обстоятельство, которое advisory полностью упускает — и это очень важно для оценки масштаба воздействия, — состоит в том, что jdk.nashorn.api.scripting.NashornScriptEngineFactory вообще не является частью исходного кода Ranger. Это встроенный компонент JDK. Он был включён в JDK 8, объявлен устаревшим в JDK 11 и полностью удалён в JDK 15. Так что эта уязвимость достижима, только если вы запускаете Ranger на JDK 8–14. Всё, что работает на JDK 15+, не затронуто, потому что Nashorn там просто не существует, а ScriptEngineUtil просто молча переключается на GraalJS или JavaScriptEngineCreator.
Два класса в Ranger используют Nashorn. Они обращаются с ним совершенно по-разному.
NashornScriptEngineCreator — названный, но более безопасныйРасположен в:
agents-common/src/main/java/org/apache/ranger/plugin/util/NashornScriptEngineCreator.java
public class NashornScriptEngineCreator implements ScriptEngineCreator {
private static final String[] SCRIPT_ENGINE_ARGS = new String[] {
"--no-java", "--no-syntax-extensions"
};
@Override
public ScriptEngine getScriptEngine(ClassLoader clsLoader) {
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ret = factory.getScriptEngine(SCRIPT_ENGINE_ARGS, clsLoader, RangerClassFilter.INSTANCE);
...
}
private static class RangerClassFilter implements ClassFilter {
@Override
public boolean exposeToScripts(String className) {
LOG.warn("script blocked: attempt to use Java class {}", className);
return false;
}
}
}
Этот класс на самом деле делает три вещи для усиления безопасности движка:
--no-java — перекрывает прямой доступ к пространству имён java.* из скриптов--no-syntax-extensions — отключает нестандартный синтаксис NashornRangerClassFilter — блокирует весь доступ к Java-классам на уровне ClassFilter, возвращая false для всегоПуленепробиваемо ли это? Нет. Обходы Nashorn ClassFilter существуют через цепочки рефлексии и трюки с java.lang.invoke. Но он значительно сильнее ограничен по сравнению с тем, что мы видим в другом классе.
RecordFilterJavaScript — фактически уязвимый классРасположен в:
plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java
public class RecordFilterJavaScript {
static class SecurityFilter implements ClassFilter {
@Override
public boolean exposeToScripts(String s) {
return false;
}
boolean containsMalware(String filterExpr) {
// only checks for this one specific string
return filterExpr.contains("this.engine");
}
}
public static boolean filterRow(String user, String filterExpr, String jsonString) {
SecurityFilter securityFilter = new SecurityFilter();
if (securityFilter.containsMalware(filterExpr)) {
throw new MaskingException("cannot process filter expression due to security concern...");
}
// instantiates Nashorn directly — no --no-java, no --no-syntax-extensions
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine(securityFilter);
String script = " jsonAttr = JSON.parse(jsonString); " + filterExpr;
Bindings bindings = engine.createBindings();
bindings.put("jsonString", jsonString);
bindings.put("user", user);
boolean hasAccess = (boolean) engine.eval(script, bindings);
...
}
}
Сравните это с NashornScriptEngineCreator — разница очевидна:
--no-java — пространство имён java.* полностью открыто для скриптов--no-syntax-extensionsfilterExpr.contains("this.engine")filterExpr конкатенируется напрямую в скрипт, который выполняет NashornПоскольку --no-java не передан, пространство имён java.* полностью доступно. Техники обхода вообще не нужны:
var runtime = java.lang.Runtime.getRuntime();
runtime.exec("id");
Никакого this.engine здесь нет. Чёрный список полностью нерелевантен для этого вектора атаки.
Что интересно: разработчики явно знали как минимум об одном паттерне обхода. В тестовом файле TestRecordFilterJavaScript.java есть вот это:
RecordFilterJavaScript.filterRow("user",
"this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\"/Applications/Spotify.app/Contents/MacOS/Spotify\")')",
...);
Этот тест существует, чтобы подтвердить, что this.engine блокируется. А вот прямой путь через java.* заблокирован не был, и он вообще не требует this.engine.
RecordFilterJavaScript.filterRow() вызывается только из одного места:
plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAuthorizer.java
private boolean hasAccessToRecord(String schema, String user, ..., String jsonString, ...) {
RangerAccessResult result = plugin.evalRowFilterPolicies(request, null);
if (result.isRowFilterEnabled()) {
String filterExpr = result.getFilterExpr();
ret = RecordFilterJavaScript.filterRow(user, filterExpr, jsonString);
}
return ret;
}
filterExpr берётся из result.getFilterExpr() — это JavaScript-выражение, хранящееся в политике фильтрации строк Ranger. Злоумышленник с правами администратора политик может установить это выражение каким угодно. Когда любой пользователь затем обращается к ресурсу, управляемому этой политикой, срабатывает hasAccessToRecord(), которая извлекает filterExpr из хранилища политик, и filterRow() передаёт его в Nashorn практически без песочницы.
Цепочка атаки:
Доступ администратора политик
|
v
Создание/изменение политики фильтрации строк на ресурсе nestedstructure
|
v
filterExpr установлен в: java.lang.Runtime.getRuntime().exec("...")
|
v
Любой пользователь обращается к ресурсу → срабатывает hasAccessToRecord()
|
v
filterExpr попадает в filterRow() → Nashorn выполняет его → RCE