PHP特性绕过技巧

弱类型

参考:php 弱类型总结

一直到碰到弱类型比较的题目,但是总感觉很零碎,看到一片总结的文章,就想着跟着学一学,总结一下:

强类型,弱类型

首先我们来区分一下强类型语言和弱类型语言的区别:

  • 强类型 语言,就是在定义一个变量的时候,就需要 指定变量的类型, 如果不经转换(手动或者自动)就使用就会报错,例如Java,C都是强类型的语言,我们都需要先定义类型才能进行赋值;
  • 弱类型 语言,就是在定义一个变量的时候, 无需定义类型, 想怎么使用,用什么赋值都可以,语言的解析器回自动转换,例如php,python都是弱类型的语言,我们在使用的时候就能明显感受;

PHP中的比较是否相等

参考: Comparison Operators:php.net

php中比较两个变量是否相等有两种运算符:

$a == $b, Equal 相等,返回true只要 $a 在类型转换之后等于 $b 即可(因此会先转换成相同的类型,再比较值); $a === $b, Identical 一致,返回true只有 $a 与 $b 完全相等,即 值一样 ,同时 类型也相等, 因此会 先判断类型再比较值, !== 亦是如此;

<?php
var_dump(0 == "a"); // true
var_dump("1" == "01"); // true
var_dump("10" == "1e1"); // true
var_dump(100 == "1e2"); // true

var_dump("admin"==0); // true
var_dump("1admin"==1); // true
var_dump("admin1"==1); // flase
var_dump("admin1"==0); // true
var_dump("0e123456"=="0e4456789"); // true
?>

这是 PHP8.0.0前 的特性,即在比较字符串和数字的时候,string会先被转换成数字,然后再进行比较:

因此我们可以看到:

0 == “a”, var_dump(“admin”==0); 都会先将字符串先强制转换成数值,变成0,然后再进行比较,自然就相等了;

同时,当一个字符串被强制转换类型变成Int的时候,php文档给出的规则是:

If the string is numeric or leading numeric then it will resolve to the corresponding integer value, otherwise it is converted to zero (0).

即如果string能够被解析成为int,或者string的开头是数字,那么就会被转换成对应的整型值,否则都转换成0;

那么什么情况下string会被解析成int呢:

官网给出的正则表达式:https://www.php.net/manual/en/language.types.numeric-strings.php

WHITESPACES      \s* // 若干空格
LNUM             [0-9]+ // 普通的数字
DNUM             ([0-9]*)[\.]{LNUM}) | ({LNUM}[\.][0-9]*) // float
EXPONENT_DNUM    (({LNUM} | {DNUM}) [eE][+-]? {LNUM})  //科学计数法
INT_NUM_STRING   {WHITESPACES} [+-]? {LNUM} {WHITESPACES}
FLOAT_NUM_STRING {WHITESPACES} [+-]? ({DNUM} | {EXPONENT_DNUM}) {WHITESPACES}
NUM_STRING       ({INT_NUM_STRING} | {FLOAT_NUM_STRING})

我们接着看下面的例子:

<?php
$foo = 1 + "10.5";                // $foo is float (11.5)
$foo = 1 + "-1.3e3";              // $foo is float (-1299)
$foo = 1 + "bob-1.3e3";           // TypeError as of PHP 8.0.0, $foo is integer (1) previously
$foo = 1 + "bob3";                // TypeError as of PHP 8.0.0, $foo is integer (1) previously
$foo = 1 + "10 Small Pigs";       // $foo is integer (11) and an E_WARNING is raised in PHP 8.0.0, E_NOTICE previously
$foo = 4 + "10.2 Little Piggies"; // $foo is float (14.2) and an E_WARNING is raised in PHP 8.0.0, E_NOTICE previously
$foo = "10.0 pigs " + 1;          // $foo is float (11) and an E_WARNING is raised in PHP 8.0.0, E_NOTICE previously
$foo = "10.0 pigs " + 1.0;        // $foo is float (11) and an E_WARNING is raised in PHP 8.0.0, E_NOTICE previously
?>

通过以上的知识我们也能明白为什么 “admin1”==1 =>False 的原因,因为其会被转换成0而不是1,

而"0e123456"==“0e4456789"则是因为两边都是科学计数法,结果都为0

实例

接下来我们跟着Mrsm1th师傅来看看几个实例加强一下印象:

md5绕过(Hash比较缺陷)

<?php
if (isset($_GET['Username']) && isset($_GET['password'])) {
    $logined = true;
    $Username = $_GET['Username'];
    $password = $_GET['password'];

     if (!ctype_alpha($Username)) {$logined = false;} // 检查$Username中的所有字符是否都是字母
     if (!is_numeric($password) ) {$logined = false;}
     if (md5($Username) != md5($password)) {$logined = false;}
     if ($logined){
    echo "successful";
      }else{
           echo "login failed!";
        }
    }
?>

这里要求 $Username 为字母组成的字符串,要求 $password 为数字,同时要求他们的md5结果一致;

那么我们的目的,就是让两个md5后的值,用 != 比较,也就是在强制转换为数字之后再进行比较,最后得到结果一致;

这里给出一组md5加密之后结果为"0e"开头的字符串,0e开头意味着会被当作是科学计数法来使用,并且计算的结果都为0,自然也就都相等;

我们就可以利用 QNKCDZO 作为用户名,利用 240610708 作为密码,即可绕过前面两个检查,最后他们在md5加密之后,两个String被当作科学计数法,比较为相等,登陆成功;

QNKCDZO
0e830400451993494058024219903391

240610708
0e462097431906509019562988736854

s878926199a
0e545993274517709034328855841020

s155964671a
0e342768416822451524974117254469

s214587387a
0e848240448830537924465865611904

s214587387a
0e848240448830537924465865611904

s878926199a
0e545993274517709034328855841020

s1091221200a
0e940624217856561557816327384675

s1885207154a
0e509367213418206700842008763514

s1502113478a
0e861580163291561247404381396064

s1885207154a
0e509367213418206700842008763514

s1836677006a
0e481036490867661113260034900752

s155964671a
0e342768416822451524974117254469

s1184209335a
0e072485820392773389523109082030

s1665632922a
0e731198061491163073197128363787

s1502113478a
0e861580163291561247404381396064

s1836677006a
0e481036490867661113260034900752

s1091221200a
0e940624217856561557816327384675

s155964671a
0e342768416822451524974117254469

s1502113478a
0e861580163291561247404381396064

s155964671a
0e342768416822451524974117254469

s1665632922a
0e731198061491163073197128363787

s155964671a
0e342768416822451524974117254469

s1091221200a
0e940624217856561557816327384675

s1836677006a
0e481036490867661113260034900752

s1885207154a
0e509367213418206700842008763514

s532378020a
0e220463095855511507588041205815

s878926199a
0e545993274517709034328855841020

s1091221200a
0e940624217856561557816327384675

s214587387a
0e848240448830537924465865611904

s1502113478a
0e861580163291561247404381396064

s1091221200a
0e940624217856561557816327384675

s1665632922a
0e731198061491163073197128363787

s1885207154a
0e509367213418206700842008763514

s1836677006a
0e481036490867661113260034900752

s1665632922a
0e731198061491163073197128363787

s878926199a
0e545993274517709034328855841020

来源: https://www.jianshu.com/p/d3a9e7de0b12

json绕过

<?php
if (isset($_POST['message'])) {
    $message = json_decode($_POST['message']);
    $key ="*********";
    if ($message->key == $key) {
        echo "flag";
    } 
    else {
        echo "fail";
    }
 }
 else{
     echo "~~~~";
 }
?>

在这个情况下 $key 的值我们不知道,无法给出准确的输入,但是却可以利用 0==‘admin’ 的形式来进行绕过,即我们输入0;

这样就会使在相等判断时的key的值被强制转换为0来进行比较,从而就可以绕过检查;

payload: message={"key":0}

array_search(), is_array()绕过

<?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]);
}
if(array_search("admin",$test)===0){
    echo "flag";
}
else{
    echo "false";
}
?>

脚本要求GET输入的值 test 是一个数组,并且数组中的每一个元素都不能等于 admin ,但是又要求在 array_search 的搜索中需要有 admin;

array_search(mixed $needle, array $haystack, bool $strict = false): int|string|false

其中:

  • $needle代表查询的字符串;
  • $haystack是被查询的数组;

$strict 参数的设置也决定了是否能够进行绕过,即是否进行“===”比较:

  • 如果没有设置,那么如果我们的字符串中有0,在进行比较的时候就会使 $needle 被转换成为0来进行比较;

因此我们可以使用payload: test[]=0 来进行绕过:

  1. 首先是一个数组,绕过 is_array();
  2. 接着其中的0与"admin"使用 = 进行比较,绕过
  3. 最后 array_search() 并未设置 strict 参数, admin 会先进行类型转换为0进行比较,成功绕过;
<?php
$a=array(0,1);
var_dump(array_search("admin",$a)); // int(0) => 返回键值0
var_dump(array_seach("1admin",$a));  // int(1) ==>返回键值1
?>

strcmp漏洞绕过 php -v <5.3

<?php
    $password="***************"
     if(isset($_POST['password'])){

        if (strcmp($_POST['password'], $password) == 0) {
            echo "Right!!!login success";n
            exit();
        } else {
            echo "Wrong password..";
        }
?>

我们在上面的strcmp函数的绕过中也有过讲解,strcmp是用来比较两个字符串的长度,既然这里出现了”== 0" 那我们就应该想办法往弱类型比较绕过上去靠,那么当strcmp所接受的是数组的时候,将发生错误,并返回0

payload: password[]=xxx

switch绕过

<?php
$a="4admin";
switch ($a) {
    case 1:
        echo "fail1";
        break;
    case 2:
        echo "fail2";
        break;
    case 3:
        echo "fail3";
        break;
    case 4:
        echo "sucess";  //结果输出success;
        break;
    default:
        echo "failall";
        break;
}
?>

switch本质上还是进行弱类型比较,我们可以用同样的方法进行绕过;

create_function() 绕过

参考: [代码审计]PHP代码审计之create_function()函数

适用范围: PHP 4> = 4.0.1,PHP 5,PHP 7

create_function 的目的是为了构建一个匿名函数,并可以赋值给一个变量

create_function(string $args,string $code)

- string $args 声明的函数变量部分
- string $code 执行的方法代码部分

基本使用

<?php
$q = $_GET('query');
$newfunc = create_function('$a', 'echo $a');
echo "function name: $newfunc\n";
$newfunc($q);
?>

Figure 1: Output value

Figure 1: Output value

我们要关注的是这个匿名函数实际上做了一件什么事情, create_function() 会创建一个匿名函数(lambda格式),用lambda命名,

<?php
function lambda_2($a){
    echo $a;
}
?>

记住这个函数是真的需要构造的,然后我们来看看 create_function 注入的例子

注入实例

<?php
//sorry , here is true last level
//^_^
error_reporting(0);
include "str.php";

$a = $_GET['a'];
$b = $_GET['b'];
if(preg_match('/^[a-z0-9_]*$/isD',$a)){
    show_source(__FILE__);
}
else{
    $a('',$b);
}

我们不关注这个正则的细节,只需要知道我们可以用反斜杠来表示全局空间Global space来绕过就可以了

直接看payload

?a=\create_function&b=;}readfile('/flag'); //

我一开始对于看到很多人使用花括号很不理解,感觉都无法闭合括号是怎么回事,但是记得刚才我们说的匿名函数的构造吗,我们来看一下这里构造了一个怎么样的匿名函数

create_function('', ';}readfile('/flag'); /*')
<?php
function lambda_1(){
;    
}readfile('/flag'); /* 注释
}
?>

这里只是创造了一个匿名函数,但是没有任何的赋值,因此是无法直接执行的,因此我们需要先用 } 来闭合匿名函数的定义,然后在外面执行我们想要注入的代码即可

is_numberic() 绕过

参考:[极客大挑战 2019]BuyFlag 1 对于空字符 %00, 无论放在前后都可以判断为非数值 %20 放在数值后面也可以判定为非数值

strcmp 绕过

参考:

[极客大挑战 2019]BuyFlag 1

strcmp:php.net

strcmp(string $string1, string $string2): int

Return < 0; string1的长度小于string2 Return > 0; string1的长度大于string2 Return = 0; string1的长度等于string2

穿入非字符串类型的数据,函数发生错误在 5.3 版本之前,在显示了报错的警告信息之后,将return 0,作用与长度相等一致

因此我们可以通过穿入数组类型来完成绕过

Licensed under CC BY-NC-SA 4.0
Last updated on Nov 03, 2022 12:23 CST
comments powered by Disqus
Cogito, ergo sum
Built with Hugo
Theme Stack designed by Jimmy