Java Agent Memshell - Demo inject memshell in Jira
Cũng lâu rồi không nghiên cứu gì nên bài này chủ yếu mình note lại về phương pháp triển khai memshell với Java Agent. Khác với các cách đã nêu trước đó (có thể dùng để init access và post-exploit) thì với java agent thiên về post-exploit nhiều hơn. Ví dụ ta đã compromised được server có 1 con webapp nhưng con này không thể nào triển khai memshell qua file jsp hay không có bug gì để triển khai, thì dùng Java agent là một giải pháp hoàn hảo để cắm shell persistent.
1. Java agent là gì ?
Java agent là một công nghệ/tính năng cho phép modify byte code của class đã load (hoặc chưa load) mà không làm ảnh hưởng đến quá trình complie code. Có 2 loại java agent là:
premain-Agent: Đúng như tên gọi thì agent sẽ được gọi trước khi start hàm và trước cả khi start JVM
agentmain-Agent: Agent sẽ được gọi sau khi đã start JVM
A. Ví dụ premain-Agent
Để minh họa mình tạo 1 Maven project với đoạn code sau
package com.example;
import java.lang.instrument.Instrumentation;
public class JavaAgentPremain {
public static void premain(String args, Instrumentation inst) {
System.out.println("Hello from premain");
}
}
Hàm premain sẽ được gọi khi premain-Agent được load. Để sử dụng được agent thì ta cần build thành file jar. Nhưng trước khi build ta phải khai báo trong resources/META-INF/MANIFEST.MF nội dung như sau:
Sau khi build thành file jar, mình sẽ tiến hành load agent vào chương trình chính. Đầu tiên tạo 1 chương trình chính như sau
public class Test {
public static void main(String[] args) {
System.out.println("Hello from Main");
}
}
Khi chạy chương trình chính ta thêm options -javaagent:"path_to_jar_file.jar"
Ta sẽ thấy code trong hàm premain sẽ thực thi trước hàm main. Nhìn vào cơ chế này ta sẽ biết được hạn chế lớn nhất của premain-agent là phải start chung với chương trình chính, do đó nếu triển khai memshell theo hướng này ta phải restart lại toàn bộ web thì memshell mới được inject. Vì hạn chế như vậy nên thông thường memshell được inject với agentmain-Agent
B. Ví dụ agentmain-Agent
Vì agentmain-Agent được load sau khi JVM start, nên ta sẽ dùng một số hàm đặt biệt để tương tác với JVM.
Ví dụ mình có đoạn code sau:
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class GetJVM {
public static void main(String[] args) {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
System.out.println(vmd.displayName() + " running with PID " + vmd.id());
}
}
}
Đoạn code trên sẽ lấy ra danh sách JVM bằng VirtualMachine.list() sau đó loop từng JVM và print ra process name + process ID
Kết quả khi chạy
Từ đây nếu muốn attach agent vào process nào ta chỉ cần filter theo tên process đó. Ví dụ mình có chương trình chính như sau
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
System.out.println("Hello from Main");
new Scanner(System.in).nextLine(); // Prevent application auto stop
}
}
Tiếp theo mình tạo agentmain-Agent như sau
package com.example;
import java.lang.instrument.Instrumentation;
public class JavaAgentMain {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
System.out.println("Hello from main agent");
}
}
Thay đổi file MANIFEST.MF thành như sau và build thành file jar:
Dùng đoạn code sau để attach agent vào chương trình chính
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class AttachAgent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
if(vmd.displayName().equals("Test")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("D:\\Labs\\Learn\\JavaSecLearn\\JavaAgent\\JavaAgent\\out\\artifacts\\JavaAgent_jar\\JavaAgent.jar");
virtualMachine.detach();
}
}
}
}
Khi chạy đoạn code trên ta sẽ thấy Hello from main agent được print ra tại chương trình chính
Lợi dụng việc này ta có thể inject memshell trực tiếp vào Runtime.
2. Giới thiệu Instrumentation
Để có thể modified được các class trong chương trình chính thì JVMTIAgent (JVM Tool Interface Agent) cung cấp cho chúng ta 1 interface cho phép giao tiếp với các class của target JVM. Interface này được gọi là Instrumentation
Ví dụ ta có thể lấy được tên các class của target JVM và check xem có modifed được không bằng đoạn code sau:
package com.example;
import java.lang.instrument.Instrumentation;
public class InstrumentationTest {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
Class [] classes = inst.getAllLoadedClasses();
for(Class cls : classes){
System.out.println("------------------------------------------");
System.out.println("Class name: "+cls.getName());
System.out.println("Is modifiable: "+inst.isModifiableClass(cls));
}
}
}
Kết quả khi attach agent:
Để modified được bytes code của class trong target JVM ta sẽ dùng method addTransformer của Instrumentation add 1 malicious Transformer, class malicious Transformer này sẽ overide method tranform. Method tranform sẽ được gọi trong quá trình attach agent để replace byte code của class trong target JVM. (https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/ClassFileTransformer.html)
Ví dụ mình thay đổi code chương trình chính thành như sau:
import static java.lang.Thread.sleep;
public class Test {
public static void main(String[] args) throws InterruptedException {
System.out.println("Hello from Main");
while (true) {
testMethod();
sleep(5000);
}
}
public static void testMethod(){
System.out.println("Method from test");
}
}
Mình có đoạn code sau để triển khai Instrumentation với agentmain-Agent
Ta sẽ dùng Javassist để modified byte code method testMethod của chương trình chính
Lưu ý: Đối với Java 8 trở xuống thì IntelliJ không tự động load được lib liên quan đến việc handle Instrumentation, do đó ta phải tự add vào project như libary 1 cách thủ công. Lib liên quan đến Instrumentation nằm trong thư mục C:\Program Files\Java\<jdk8_version>\lib\tools.jar
Môi trường demo hiện tại của mình là JDK17 nên không lo về vấn đề trên. Tuy nhiên vì ta dùng lib Javassist để load bytes, nên khi build agent jar file ta phải add thêm thư viện này vào jar file. Nếu không khi attach sẽ bị lỗi Class not found như dưới đây
Ta sẽ thêm lib Javassist vào build jar bằng cách add extracted directory như sau
Sau đó chỉnh sửa file MANIFEST.MF thành như sau để enable modified cho agent.
Khi ta attach agent vào chương trình chính, ta có thể thay đổi được bytecode của testMethod theo ý muốn
3. Triển khai memshell Tomcat trong Jira
A. Prepare
Tiếp theo ta sẽ bắt đầu inject memshell trong Tomcat bằng những gì đã tìm hiểu ở trên.
Như đã mình đã nói ở bài trước, ta có thể triển khai memshell trong Tomcat qua Filter thông qua method org.apache.catalina.core.ApplicationFilterChain#doFilter (Link bài trước) . Đối với phương pháp dùng Java agent, thì ta sẽ modified luôn method doFilter để chèn memshell
Để cho chân thực thì mình sẽ demo inject agent memshell vào web Jira.
Các bước setup Jira thì rất dễ nên mình không nói đến, mình sẽ nói một chút về lý do tại sao trong Jira thì nếu muốn có shell persistent ta phải dùng memshell.
Từ phiên bản Jira 10.x trở đi thì ta không thể truy cập trực tiếp vào file JSP để thực thi code được nữa, mà các file JSP sẽ được build thành class trong quá trình complie, chỉ còn file login.jsp và logout.jsp có thể truy cập trực tiếp. Nếu muốn custom JSP ta phải thêm config vào action.xml và restart lại Jira (tham khảo: https://developer.atlassian.com/server/jira/platform/jira-templates-and-jsps/). Do đó cần phải có 1 phương pháp khác để cắm shell persistent mà không cần restart lại service.
Trên mạng thì có 1 phương pháp sử dụng malicous plugins -> upload lên -> assign malicious Servlet -> shell (tham khảo: https://github.com/dubfr33/atlassian-webshell-plugin). Tuy nhiên tính năng upload plugins đã bị disable mặc định ở các phiên bản Jira 9.x+ (nguồn), muốn enable thì cũng phải restart lại Jira. Do đó sử dụng phương pháp Java agent để cắm memshell persistent là hợp lý và ít noise nhất.
Điều kiện để triển khai Java agent memshell đối với Jira là phải có quyền root hoặc quyền nào đó có thể attach được vào process JVM của jira.
B. Inject memshell
Phiên bản Jira mình dùng để demo là bản 10.3.3 (bản supported release mới nhất hiện tại)
Đầu tiên ta sẽ thử chạy code list proccess JVM đang chạy
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class GetJVM {
public static void main(String[] args) {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
System.out.println(vmd.displayName() + " running with PID " + vmd.id());
}
}
}
Với phiên bản Jira 10.3.3 dùng java 17 nên mình sẽ build đoạn code trên với JDK17 và run ở máy server. Kết quả khi chạy code:
Nguyên nhân của lỗi trên vì Java cài trên server là JRE chứ không phải JDK nên không thể load được lib. Để khắc phục ta có thể cài JDK trên máy server rồi chạy bình thường
Với Java version <8 ta có thể inject trực tiếp vào JRE luôn bằng cách upload file tools.jar lên server rồi dùng URLClassLoader để load tools.jar và dùng reflection để gọi để các method và class trong com.sun.tools.attach. Tuy nhiên với JRE version cao hơn mình không thực hiện được việc tương tự, mình có thể extract được jdk.attach từ JDK để load com.sun.tools.attach vào JRE, mình có thể load được class nhưng khi gọi method thì không get được process target JVM -> không rõ nguyên nhân. Mình note lại cách extract jdk.attach để research thêm
/usr/lib/jvm/java-17-openjdk-amd64/bin/jmod extract --dir /tmp/jdk-attach /usr/lib/jvm/java-17-openjdk-amd64/jmods/jdk.attach.jmod
cd /tmp/
cd /tmp/jdk-attach/classes
jar cf ../jdk-attach.jar .
Sau khi cài JDK và run đoạn code trên, ta sẽ thấy Jira được chạy bằng catalina với process id là 3490
Để check là Jira có thật sự load org.apache.catalina.core.ApplicationFilterChain hay không mình sẽ attach agent có đoạn code sau
Khi attach thành công thì Jira vẫn hoạt động bình thường khi truy cập
Nhưng nếu thêm parameter cmd thì memshell sẽ được thực thi
Tóm lại đây là 1 cách cắm shell persistent khá hay khi không cần phải perist file trên server, chỉ cần upload file exploit, load xong rồi xóa file. Vì là memshell nên chỉ bị xóa khi restart lại service -> tính ẩn mình cao. Tuy nhiên thiếu 1 chút nữa là cách này sẽ rất hoàn hảo vì với Jira dùng java 17 thì ta phải cài thêm JDK17 trên server mới có thể inject được memshell, đối với các web dùng java bản thấp < 8 thì ta có thể trực tiếp inject vào JRE luôn. Việc cài đặt thêm JDK vào server dẫn đến việc dễ bị detect hơn, không còn ẩn mình tuyệt đối 😥(mình sẽ cố gắng research thêm cách inject vào JRE version cao)