前言
github项目地址:
https://github.com/bowu678/php_bugs
首先这篇文章是面向PHP代码审计的萌新以更好的入坑而准备的,所以会比较详细,并且是以萌新的角度来解题,hh,我也是萌新。
0x01extract变量覆盖
<?php
$flag='xxx';
extract($_GET);
if(isset($shiyan)){
$content=trim(file_get_contents($flag));
if($shiyan==$content){
echo'ctf{xxx}';
}else{
echo'Oh.no';
}
}
?>
首先通读一波代码
这里有几个函数
extract()函数从数组中将变量导入到当前的符号表。
isset()检测变量是否设置,并且不是 NULL。
trim()函数移除字符串两侧的空白字符或其他预定义字符。
file_get_contents()函数把整个文件读入一个字符串中。
首先extract()函数从数组中将变量导入到当前的符号表。
符号表的概念:
符号表是指当前php页面中,所有变量名称的集合,可以使用函数get_defined_vars直接获得当前所有已定义变量列表的多维数组
$_GET 变量是一个数组,内容是由 HTTP GET 方法发送的变量名称和值。
这样或许你还是不理解,那么好,我们来看一个例子。
<?php
$a = 1;
$b = array("a"=>2);
extract($b);
var_dump($a);
运行结果是:
int(2)
这里的变量a的值不是1吗?为什么变成2了呢?
extract函数将a这个键值映射成变量名,而键值被映射为变量值
也就是说上面一个php等同于
<?php
$a = 1;
$a = 2;
var_dump($a);
好,那么回归正题,看到后面部分
if(isset($shiyan)){
$content=trim(file_get_contents($flag));
if($shiyan==$content{
echo'ctf{xxx}';
}else{
echo'Oh.no';
}
}
判断shiyan变量是否设置,设置就将trim(file_get_contents($flag))
赋值给content变量,但是这里我们发现file_get_contents这个函数是拿来读文本的,也就是说它读flag变量是什么都没有的,然后trim函数移除字符串两侧的空白字符或其他预定义字符。
也就是$content='';
最后将shiyan变量和content变量进行比较,相等即输出flag。
所以思路很明确,我们只需要传入一个shiyan变量为空就可以了
故payload:
/?shiyan=
这里放出另一种思路,通过php伪协议
/?shiyan=a&flag=php://input
POST:a
0x02绕过过滤的空白字符
<?php
$info = "";
$req = [];
$flag="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
ini_set("display_error", false); //为一个配置选项设置值
error_reporting(0); //关闭所有PHP错误报告
if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt
die("have a fun!!"); //die — 等同于 exit()
}
foreach([$_GET, $_POST] as $global_var) { //foreach 语法结构提供了遍历数组的简单方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾处的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
}
}
function is_palindrome_number($number) {
$number = strval($number); //strval — 获取变量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 获取字符串长度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
if(is_numeric($_REQUEST['number'])) //is_numeric — 检测变量是否为数字或数字字符串
{
$info="sorry, you cann't input a number!";
}
elseif($req['number']!=strval(intval($req['number']))) //intval — 获取变量的整数值
{
$info = "number must be equal to it's integer!! ";
}
else
{
$value1 = intval($req["number"]);
$value2 = intval(strrev($req["number"]));
if($value1!=$value2){
$info="no, this is not a palindrome number!";
}
else
{
if(is_palindrome_number($req["number"])){
$info = "nice! {$value1} is a palindrome number!";
}
else
{
$info=$flag;
}
}
}
echo $info;
这个代码比较长,很多萌新肯定就被唬住了
好,没关系,我们从头开始分析
$info = ""; //定义string(字符串)
$req = []; //定义array(数组)
$flag = "xxx"; //定义string(字符串)
ini_set("display_error", false); //为一个配置选项设置值
error_reporting(0); //关闭所有PHP错误报告
好,开始看下面一部分
if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt
die("have a fun!!"); //die — 等同于 exit()
}
这段代码讲的是如果没有传入number参数(number参数没有设置),就会在header头添加一个hint(提示),并且die一个have a fan!! 这个die的意思差不多就是exit+echo的意思了,这个hint的意思肯定也就是源代码的存放位置了,但是我们这个审计是直接上源码的,不存在这些的,好了废话不多说,看下一部分。
foreach([$_GET, $_POST] as $global_var) { //foreach 语法结构提供了遍历数组的简单方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾处的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
}
}
首先使用了foreach循环遍历数组,$_GET作为键名,$_POST作为键值,那么上个题目我们也说过了$_GET,所以这里的$_POST同理,无非就是两个不同的传值方法而已,这个foreach循环将$_GET和$_POST传入的值给赋值到global_val这个变量,然后又使用了一个foreach循环将键值和键名分别赋值给key和value变量,但是这都不重要,没错,一般foreach循环不是拿来输出变量的,而是拿来遍历处理数组里的数据的,很明显,这里作为键值的value经过处理了,trim函数上题也说过(trim()函数移除字符串两侧的空白字符或其他预定义字符)
is_string()函数(判断变量是否为字符串)如果指定变量为字符串,则返回 TRUE,否则返回 FALSE。
is_string($value) && $req[$key] = addslashes($value);
这段代码的意思是,先判断value变量是否为字符串,如果是字符串,那么就将经过addslashes()(在每个双引号(")前添加反斜杠)函数处理后的值赋给value变量和$req$key
接下来我们看定义的函数is_palindrome_number(看字面意思就是判断是不是回文)
function is_palindrome_number($number) {
$number = strval($number); //strval — 获取变量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 获取字符串长度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
首先strval函数上面已经有解释了不多赘述,这段代码大概的意思就是,如果i<j的时候就会将number的第i位和第j位进行对比,如果不一样就会返回false
然后i+1,j-1,直到j<i。
if(is_numeric($_REQUEST['number'])) //is_numeric — 检测变量是否为数字或数字字符串
{
$info="sorry, you cann't input a number!";
}
elseif($req['number']!=strval(intval($req['number']))) //intval — 获取变量的整数值
{
$info = "number must be equal to it's integer!! ";
}
需要绕过is_numeric函数,需要返回false才能进行下个判断,绕过的方法很简单加个%00即可返回false,然后看到下个判断,number这个变量的值得经过两个类型转换之后还不能等于自身才会跳到最后一个判断,而后面一个判断value1变量和value2(strrev()函数反转字符串)是否相等,相等就进入下一个判断,也就是判断它是否为回文数,而最后这个判断number这个变量是否为回文,不是则输出flag,所以这个点很矛盾,number这个变量又得是回文数又不能是回文数。
整个题目的意思是,想要拿到flag,首先需要绕过is_numeric函数,而且有需要是回文,又不能是回文数这样的一个数,绕过is_numeric函数很简单,在传入的值最前面和最后面加%00即可,而满足这种情况的数我们可以使用科学记数法来绕过[0e-0(=0)]
,
故payload:
\?number=0e-0%00
第二个payload:
\?number=%00%2B%00
0x03多重加密
<?php
<?php
include 'common.php';
$requset = array_merge($_GET, $_POST, $_SESSION, $_COOKIE);
//把一个或多个数组合并为一个数组
class db
{
public $where;
function __wakeup()
{
if(!empty($this->where))
{
$this->select($this->where);
}
}
function select($where)
{
$sql = mysql_query('select * from user where '.$where);
//函数执行一条 MySQL 查询。
return @mysql_fetch_array($sql);
//从结果集中取得一行作为关联数组,或数字数组,或二者兼有返回根据从结果集取得的行生成的数组,如果没有更多行则返回 false
}
}
if(isset($requset['token']))
//测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:进行字符串压缩
//unserialize: 将已序列化的字符串还原回 PHP 的值
$db = new db();
$row = $db->select('user=\''.mysql_real_escape_string($login['user']).'\'');
//mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。
if($login['user'] === 'ichunqiu')
{
echo $flag;
}else if($row['pass'] !== $login['pass']){
echo 'unserialize injection!!';
}else{
echo "(╯‵□′)╯︵┴─┴ ";
}
}else{
header('Location: index.php?error=1');
}
?>
好家伙,又这么多代码,好勒,之所以叫代码审计,那肯定是有方法的,肯定不是从头看到尾,不要杠我,以后要是审thinkphp,难道去把全部代码都熟悉一次?不可能对吧,那么我们首先看输出flag的点
if($login['user'] === 'ichunqiu'){
echo $flag;
}
login这个数组里的以user为键名的值得等于ichunqiu,那么得看到login数组是从哪里传递过来的
if(isset($requset['token']))
//测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
//gzuncompress:进行字符串压缩
}
可以看到是$request传入的token然后进行了base64解密,gzuncompress,gz解压缩,unserialize反序列化操作。
那么也就是说,我们只要控制login[user]=ichunqiu就能输出flag,反向操作就行,也就是先进行序列化,然后gzcompress,在进行serialize序列化操作。
那么生成token
<?php
$a = array('user'=>'ichunqiu');
echo base64_encode(gzcompress(serialize($a)));
故payload:
/?token=eJxLtDK0qi62MrFSKi1OLVKyLraysFLKTM4ozSvMLFWyrgUAo4oKXA==