代码审计-PHP_bugs(1)

代码审计-PHP_bugs(1)

前言:

最近在学习代码审计,说实话,入门代码审计的难度远远大于入门渗透测试。找了一些CMS尝试审计但是收获并不是很大,在Github上看到一个入门的项目,计划通过这个项目里的30几个PHP文件,来熟悉下代码审计的思路,并且学习掌握PHP的函数与功能,因为代码审计这件事说实话不可能像开发一样要把每个函数都学过去,我的思路是遇到哪个就学习哪个,继而再尝试审计CMS。

正文

PHP_Bug1-extract变量覆盖漏洞

源代码如下:

<?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函数,它的作用是从数组中将变量导入到当前的符号表。那加上\(_GET意思是创建变量,比如我上传一个GET型参数,例如?A=1,他就会创建一个变量\)A,并且值为1。再往下看一个if判断句,通过isset()函数进行判断,isset()函数的作用是检测变量是否已设置并且非 null,也就是说只要$shiyan的值不是null,即可进入条件判断式。

再往下看\(content由trim函数和file_get_contents函数对\)flag处理而成,其中trim函数的作用是移除字符串两侧的字符而file_get_contents即将\(flag的值所代表的文件读入字符串,又因为xxx在目录中不存在,所以\)content值为空,但是空不代表null,此时我们上传百年来$shiyan,并且不赋值即可读出flag,URL为:

http://127.0.0.1/php_bugs/1.php?shiyan

PHP_Bug2-绕过过滤的空白字符

因为篇幅过大,所以我们一步一步分析

首先定义了三个变量,其中包括了\(info,\)req,$flag,并且做好了配置。ini_set函数的作用的通过函数修改php.ini,使得错误信息不回显

$info = "";
$req = [];
$flag="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
ini_set("display_error", false); //为一个配置选项设置值
error_reporting(0); //关闭所有PHP错误报告

再往下看是一个条件判断语句,如果未上传啊GET型参数number则给出提示

if(!isset($_GET['number'])) {
    header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt
    die("have a fun!!"); //die — 等同于 exit()
}

这点我们通过抓包可以看到,说明我们需要上传GET型的number代码审计-PHP_bugs(1)

往下看是一个foreach函数,这个函数的首先呢遍历GET型参数变量和POST型参数变量并且将他们作为global_var传入函数,再将变量名和变量值分别对应为\(key和\)value。继而去除\(value两边的空白字符,然后判断\)value是否为字符串,如果为字符串,即将\(req数组中建立对应的\)key与\(value关系,并且\)value还被addslashes函数处理,对敏感字符进行了转义。

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 — 使用反斜线引用字符串
    }
}

再往下看定义了一个函数,这个函数的作用是判断传入的值是否对称,譬如12ww,21这类

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;
}

再往下看就比较有意思了首先呢判断number是否为数字,如果为数字的话就报错,并且\(req数组中number对应的值应该等于它的字符型的值,这个暂时不理解那先往下看。接下来的函数即要求\)number传入的回文数不是数字只能是字符,但是字符转化为数字再转化为字符的时候一直出错,简单的回文数已经无法绕过了,这是正式进行代码审计来完成绕过。经过查询发现可以使用空白字符,来绕过intval和is_number。

is_numeric函数对于空字符%00,无论是%00放在前后都可以判断为非数值,而%20空格字符只能放在数值后。所以,查看函数发现该函数对对于第一个空格字符会跳过空格字符判断,接着后面的判断!

该函数还可能造成sql注入,例如将‘1 or 1‘转换为16进制形式,再传参,就可以造成sql注入

通过再?number后添加%00,我们可以传入非数字字符串的字符,但是也可以令is_numeric函数判断为数字。

这里需要用到is_numeric的特性即+100可以判断为100,并且+100=100+,而这个+符号在判断是否回文的时候会使得函数报错,这样就完成了绕过的作用,+号URL编码为%20,则最后的payload为%00%2B1221

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;

PHP_Bug3-多重加密

首先看上传的过程,将GET、POST、SESSION、COOKIE上传的参数都放到$request数组中去

    include 'common.php';
    $requset = array_merge($_GET, $_POST, $_SESSION, $_COOKIE);
    //把一个或多个数组合并为一个数组

然后创建了一个进行SQL语句查询的类

  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       }    }

再往下看就是一个加密和反序列化的过程了,如果toekn上传了,则将token首先使用base64解码,然后压缩继而反序列化,所以我们需要自己另建一个PHP文件,将设想的值进行Base64加密。

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');    }

PHP文件内容如下:

<?php$arr = array(['user'] === 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));echo $token;?>

传入参数token即可

上一篇:Linux -查找功能


下一篇:POJ1038 Bugs Integrated, Inc. (状压DP)