PHP代码审计总结

AUDIT 启

一切的用户输入都是有害的,危险的数据进入了危险的函数便产生了漏洞。从这2句话,不难看出审计的关键在于用户输入和函数因此 大概分出2类审计方法。

  1. 跟踪用户的输入数据,检查代码运行的逻辑漏洞。
  2. 检索危险的函数,分析其参数是否被用户输入控制,如果可以,就很有可能有漏洞。

网站功能分析

  • 注册功能

    我们要根据需求列出服务器端需要接收的数据,假设有用户名、密码、邮箱、手机号等

  • 注销功能

    根据需要我们要回收令牌资源,保存用户操作以及状态(操作日志、当前用户等级等)。

  • 发表评论功能

    要判断用户评论的是不是空、是不是恶意代码、回复的哪篇文章

  • 回复功能

    回复给谁,是否能够无限回复? 有没有权限限制

  • 下载文件功能

    下载哪个文件,是否有下载权限

  • 上传图片功能

    上传的是哪种类型的文件?允许上传多大的?保存在哪里?是谁上传的?

AUDIT 工具

Seay源代码审计系统:C#语言开发的的,针对PHP的代码审计,Windows下运行。一键自动化白盒审计、代码调试、正则编码、自定义插件

在这里插入图片描述

输入输出

在PHP中可由用户输入的变量

  • $_SERVER
  • $_GET
  • $_POST
  • $_COOKIE
  • $_REQUEST
  • $_FILES
命令注入
函数
system
exec
passthru
shell_exec
popen
proc_open
pcntl_exec
XSS - CSRF
函数
echo
print
printf
vprintf
<%=$test%>
文件包含
函数
include
include_once
require
require_once
show_source
highlite_file
readfile
flie_get_contents
fopen
<?php
    highlight_file(__FILE__);
    class emmm
    {
        public static function checkFile(&$page)
        {
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
            if (! isset($page) || !is_string($page)) {
                echo "you can't see it";
                return false;
            }

            if (in_array($page, $whitelist)) {
                return true;
            }

            $_page = mb_substr(
                $page,
                0,
                mb_strpos($page . '?', '?')
            );
            var_dump($_page);
            if (in_array($_page, $whitelist)) {
                return true;
            }

            $_page = urldecode($page);
            $_page = mb_substr(
                $_page,
                0,
                mb_strpos($_page . '?', '?')
            );
            var_dump($_page);
            if (in_array($_page, $whitelist)) {
                return true;
            }
            echo "you can't see it";
            return false;
        }
    }

    if (! empty($_REQUEST['file'])
        && is_string($_REQUEST['file'])
        && emmm::checkFile($_REQUEST['file'])
    ) {
        include $_REQUEST['file'];
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }  
?> 

在这里插入图片描述
包含上级目录的1.php文件

可以利用?截取hint.php,然后利用/使hint.php?成为一个不存在的目录,最后include利用…/…/跳转目录读取flag
file=hint.php?/…/ (当前目录)

代码注入
函数
eval
preg_replace /e
assert
call_user_func
call_user_func_array
create_function
SQL注入
sql语句
insert
update
select
delete
$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
echo $sql;

可以发现这是一个单引号注入
在这里插入图片描述
不过这里还定义了一个防止攻击函数,检测了常用的查询语句

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=>$valu){ 
//遍历用户输入数组
    AttackFilter($key,$value,$filter);
}

此时就不能注入了,代码中要求查询到的密码和用户输入的密码相同

if($key['pwd'] == $_POST['pwd']) {
        print "CTF{XXXXXX}";

在查询语句中加入group by pwd with roolup 就可以在输出结果底部多一条pwd置为NULL的记录
再在这个查询语句中加入limit 1 offset 2 ,即取一行记录,从2行开始取
利用这一点,当我们输入的密码为NULL的时候,查询语句返回的记录中密码也为空就通过了判断。

SQL注入2
<?php
$user = $_GET[user];
$pass = md5($_GET[pass]);
$sql = "select pw from php where user='$user'";
echo $sql;
  if (($row[pw]) && (!strcasecmp($pass, $row[pw]))) {
//如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。
    echo "<p>Logged in! Key:************** </p>";
}
else {
    echo("<p>Log in failure!</p>");
  }
?>

这里需要sql查询返回的pw存在,且和输入的密码的md5相同。
首先令user为空,目的是使返回记录为0条,再利用sql联合查询,返回一个我们可以指定的值。

select pw from php where user='' union select "abcd" -- -

#这里的查询结果是一条pw为abcd的记录。

代码要求pw和输入密码的md5都形同,现在这2个参数都可以控制,构造合适的值即可输出flag。

password=123
password_md5=md5(123)=202cb962ac59075b964b07152d234b70

令pw=password_md5

最终的Poc
user=' union select "202cb962ac59075b964b07152d234b70" -- -
pass=123

sql注入总结

文件管理
函数
copy
rmdir
unlink
delete
fwrite
chmod
fgetc
fgetcsv
fgets
fgetss
file
file_get_contents
fread
readfile
ftruncate
file_put_contents
fputcsv
fputs
php://input 文件读取绕过
$a=$_GET['a'];
if(stripos($a,'.'))
{
    echo 'Hahahahahaha';
    return ;
}
$data = @file_get_contents($a,'r');
if($data=="1112 is a nice lab!") 
{
    require("flag.txt");
    echo "flag";
}

这里通过get获取a值,在读取名字为a值的文件,要求文件内容为1112 is a nice lab!,a的值不能包含.,我们可以指定a值为php://input,这时data的值可以通过POST传入。

在这里插入图片描述

文件上传
函数
move_uploaded_file
变量覆盖
函数
extract将一个数组的key作为变量名,value作为值,赋值多个变量,同时对已经存在的变量可以起到覆盖更新原来内容的目的

在这里插入图片描述

一道简单的题目

<?php

$flag='flag.txt'; 
extract($_GET);
 if(isset($shiyan))
 { 
    $content=trim(file_get_contents($flag)); 
    //将flag文件的内容赋值给content变量
    if($shiyan==$content)
    { 
        echo'ctf{xxx}'; 
    }
   else
   { 
    echo'Oh.no';
   } 
   }

?>

分析代码,可以知道shiyan这个变量是通过extract函数将用户输入$$_GET中的元素全部定义了一遍后得到的。这里并没有对GET参数做限制所以可以直接发送flag参数,利用extract函数重新赋值。
在这个代码中想要取得ctf值,需要使content变量和shiyan变量相等,其中content的值是flag文件的内容,通过对flag变量的覆盖,我们可以读取任意文件内容,但这里我们什么都不知道,因此可以使flag为一个不存在的文件,这样使得content变量为空,再传参shiyan变量为空,这样就通过了if判断。

Poc:
shiyna=&flag=nothatfile
多重加密

追踪用户输入找到request[‘token’]

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

可以看到只要$login数组的user值为ichunqiu即可输出key。
那么关键在于这一条语句

$login = unserialize(gzuncompress(base64_decode($requset['token'])));

//首先base64解密
//然后解压缩
//再反序列化

我们只要反过来操作就可以得到需要的token。

在这里插入图片描述
题目给出的代码不能直接执行,稍微改动一下测试
在这里插入图片描述

Session绕过哦噢
<?php

$flag = "flag";

session_start(); 
if (isset ($_GET['password'])) {
    if ($_GET['password'] == $_SESSION['password'])
        die ('Flag: '.$flag);
    else
        print '<p>Wrong guess.</p>';
}
mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000));
?>

这个解题过程非常奇幻,测试的时候刚刚输入了password参数,手快就直接回车了,然后flag就出来了???
在这里插入图片描述
分析一下代码,Session的password在未登陆时为空,我们只要上传一个空的paasword即可绕过。

过滤绕过1

没有看到啥危险函数,使用正向分析追踪输入值
首先将函数拿出来单独分析,

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变量的整数部分是否首尾相同,形如121.

<?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()
 
}

可以看到这个页面需要GET参数number的存在,否则将会退出脚本

foreach([$_GET, $_POST] as $global_var) {   
    foreach($global_var as $key => $value) { 
        $value = trim($value);  //trim — 去除字符串首尾处的空白字符(或者其他字符)
        is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
    } 
} 

将🤺number变量赋值给req列表

if(is_numeric($_REQUEST['number'])) //is_numeric — 检测变量是否为数字或数字字符串 
{
 
   $info="sorry, you cann't input a number!";
 
}

number变量需要经过 is_number函数检测

elseif($req['number']!=strval(intval($req['number']))) //intval — 获取变量的整数值
{
 
     $info = "number must be equal to it's integer!! ";  
 
}

number变量的整数部分如果和变量本身的值不相等,则会报错

else
{
 
     $value1 = intval($req["number"]);
     $value2 = intval(strrev($req["number"]));  
 
     if($value1!=$value2){
          $info="no, this is not a palindrome number!";
     }

当number变量的正向输出和逆向输出不相同时,会报错误信息。

     else
     {
 
          if(is_palindrome_number($req["number"])){
              $info = "nice! {$value1} is a palindrome number!"; 
          }
          else
          {
             $info=$flag;
          }
     }
 
}
 
echo $info;

number变量需要如果通过is_palindrome_number函数的判断,就不会输出flag。
全篇代码分析之后发现number共经过4次检测。

  1. is_numeric 使它为假
  2. $req['number']!=strval(intval($req['number']))使它为假
  3. intval($req["number"])!= intval(strrev($req["number"]));使它为假
  4. is_palindrome_number($req["number"])使它为假
    第一,二个检测可以通过%00的方式绕过
    在这里插入图片描述
    在这里插入图片描述
    第三个需要num是回文数(例如131,12421),但第四个检测又需要他首尾不相同,测试发现当一个回文数字前面有+号时第三个检测也可以通过即+12321==12321判断为真。
    在这里插入图片描述
    而当数字前面加了+后又使得第4个自定义函数检测为假,从而输出flag。
    在这里插入图片描述
    而第1,2个检测会忽略+符号,所以不会影响结果
    在

最终的Poc即为%00%2b111
zzzz

过滤绕过2
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
  {
    echo '<p>You password must be alphanumeric</p>';
  }

这里需要password不能有特殊符号,比较简单,直接%00截断即可

  else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)

这里要求长度必须小于8,有要大于99999999,正常来讲是不可能的,这里通过科学计数法绕过1e5

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

这里需要password包含*-*,利用截断绕过
最终的Poc

password=1e9%00*-*

在这里插入图片描述

URL二次编码
<?php
if(eregi("hackerDJ",$_GET[id])) {
  echo("<p>not allowed!</p>");
  exit();
}

$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "hackerDJ")
{
  echo "<p>Access granted!</p>";
  echo "<p>flag: *****************} </p>";
}
?>

代码要求id参数不能等于hackerDJ,又需要id进行url解码后等于hackerDJ,我们只需要保证传入脚本的id参数等于hackerDJ的url编码值,但用户从浏览器传参时,会自动执行一次url解码,所以我们需要编码2次,才能使id传入脚本后还有一层url编码。
在这里插入图片描述

在这里插入图片描述

请求IP伪造
<?php
function GetIP(){
if(!empty($_SERVER["HTTP_CLIENT_IP"]))
    $cip = $_SERVER["HTTP_CLIENT_IP"];
else if(!empty($_SERVER["HTTP_X_FORWARDED_FOR"]))
    $cip = $_SERVER["HTTP_X_FORWARDED_FOR"];
else if(!empty($_SERVER["REMOTE_ADDR"]))
    $cip = $_SERVER["REMOTE_ADDR"];
else
    $cip = "0.0.0.0";
return $cip;
}
$GetIPs = GetIP();
if ($GetIPs=="1.1.1.1"){
echo "Great! Key is *********";
}
else{
echo "错误!你的IP不在访问列表之内!";
}
?>

|头部参数|
|–|–|
| CLIENT-IP |
|X-Forwarded-For|
|REMOTE-ADDR|

tip:前2个可以很简单的利用,但remote-addr的伪造比较麻烦,需要重新实现系统的 tcp协议栈,然后 自己改变自己的 ip
资料:https://yonghaowu.github.io/2018/11/23/get_reql_ip/
在这里插入图片描述
在这里插入图片描述

比较相等绕过

==对比的时候会进行数据转换,0eXXXXXXXXXX 转成0,如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换为数值并且比较按照数值来进行

md5
md5(‘240610708’)==md5(‘QNKCDZO’)
md5(‘aabg7XSs’)==md5(‘aabC9RqS’)
sha1
sha1(‘aaroZmOk’)==sha1(‘aaK1STfY’)
sha1(‘aaO8zKZF’)==sha1(‘aa3OFF9m’)
明文
‘0010e2’==‘1e3’
‘0x1234Ab’==‘1193131’
‘0xABCdef’==’ 0xABCdef’);

在这里插入图片描述

弱类型整数大小比较
<?php

error_reporting(0);
$flag = "flag{test}";

$temp = $_GET['password'];
is_numeric($temp)?die("no numeric"):NULL;    
if($temp>1336){
    echo $flag;
} 

?>

当一个整形和一个其他类型行比较的时候,会先把其他类型intval再比,例如12321a在比较时会变成12321

十六进制绕过
<?php

error_reporting(0);
function noother_says_correct($temp)
{
    $flag = 'flag{test}';
    $one = ord('1');  //ord — 返回字符的 ASCII 码值
    $nine = ord('9'); //ord — 返回字符的 ASCII 码值
    $number = '3735929054';
    for ($i = 0; $i < strlen($number); $i++)
    { 
        // Disallow all the digits!
        $digit = ord($temp{$i});
        if ( ($digit >= $one) && ($digit <= $nine) )
        {
            // Aha, digit not allowed!
            return "flase";
        }
    }
    if($number == $temp)
        return $flag;
}
$temp = $_GET['password'];
echo noother_says_correct($temp);

?>

这里的函数规定temp变量不允许出现1~9的内容,然后又要去和一个数字3735929054相等,这个数字是十进制的,可以令temp等于这个数字的16进制数0xdeadc0de,这样就temp就没有1~9的内容了,而10进制和16进制比较自然是相等的。
在这里插入图片描述

md5函数true注入绕过
$password = $_GET['password'];
$sql = "SELECT * FROM users WHERE password = '".md5($password,true)."'";

这里是一个单引号注入,难点在md5(pasword,true),这个函数当第二个参数值为true时,返回的内容从原来的32位md5变成了原始2进制流。

在这里插入图片描述
ffifdyop

在这里插入图片描述

动态函数

危险函数

strpos()

在这里插入图片描述

strpos()找的是字符串,那么传一个数组给它,strpos()出错返回null

在这里插入图片描述

sha1()

<?php

$flag = "flag";

if (isset($_GET['name']) and isset($_GET['password'])) 
{
    if ($_GET['name'] == $_GET['password'])
        echo '<p>Your password can not be your name!</p>';
    else if (sha1($_GET['name']) === sha1($_GET['password']))
      die('Flag: '.$flag);
    else
        echo '<p>Invalid password.</p>';
}
else
    echo '<p>Login first!</p>';
?>

这里需要构造一对哈希值相等但明文不同的字符串,这里sha1函数无法处理数组,当处理数组时会报错返回False,这样就使得2个参数的哈希值“相等”,这里传参2个不同的数组即可绕过所有if。

在这里插入图片描述

strcmp()

<?php
$flag = "flag";
if (isset($_GET['a'])) {  
    if (strcmp($_GET['a'], $flag) == 0) //如果 str1 小于 str2 返回 < 0; 如果 str1大于 str2返回 > 0;如果两者相等,返回 0。 

    //比较两个字符串(区分大小写) 
        die('Flag: '.$flag);  
    else  
        print 'No';  
}

?>

在这里插入图片描述

传入数据的类型是字符串类型,当传入的类型不是字符串类型 函数就会发生错误,显示报错信息后会return 0 ,即认为2个参数相等通过判断。

in_array()

在这里插入图片描述
查阅官方文档可以发现,in_array有3个参数,如果没有设置第三个参数,那么将不会检查 needle 的类型是否和 haystack 中的相同。

题目来源:
https://github.com/bowu678/php_bugs
https://github.com/CHYbeta/Code-Audit-Challenges
https://github.com/hongriSec/PHP-Audit-Labs
https://chybeta.gitbooks.io/code-audit-challenges/content/

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值