当前位置: 首页 > news >正文

PHP弱类型全面复盘

写在前面

连续碰了两次不同的PHP弱类型题,都卡了很久,还是总结一下吧

有几个点在网上不同师傅的文章意见不一样,本着实践出真知的想法,本次的所有内容都跑了代码,或者查阅了官方文档

本次的代码保留了老版本的测试结果,而我会基于新的版本进行测试,本文的注意点部分很多都是最新版本中的变动

至于考证每种绕过方式具体哪些版本可用,工作量实在太大,先写进Todo里吧

PHP版本:PHP Version 8.2.2

概念

  • 强类型是两个不同类型的变量不能用同一块内存存储
  • 弱类型是两个不同类型的变量可以用同一块内存存储

PHP 是弱类型语言

弱类型简单来说就是数据类型可以被忽视的语言,和强类型语言的强制数据类型定义不同,弱类型可以一个变量赋不同数据类型的值

PHP 类型比较表

弱比较/松散比较/==

注意PHP 8.0.0是一个分水岭,有些弱比较无法使用了

强比较/严格比较/===

类型转换问题

比较操作符

  • ===在进行比较的时候,会先判断两种字符串的类型是否相等,再比较
  • ==在进行比较的时候,会先将字符串类型转化成相同,再比较

如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换成数值并且比较按照数值来进行
比较简单,不过多举例

Hash 比较缺陷
"0e132456789"=="0e7124511451155" //true
"0e123456abc"=="0e1dddada"  //false
"0e1abc"=="0"     //true

在进行比较运算时,如果遇到了 0exxxxx(xxxxx为纯数字) 这种字符串,就会将这种字符串解析为科学计数法

如果不满足 0exxxxx(xxxxx为纯数字) 这种模式,就会当作字符串进行比较,所以不会相等

注意点

在最新版本中"0e1abc""0"弱比较也不相等

var_dump("0e1abc"=="0"); // false
十六进制转换
"0x1e240"=="123456"     //true
"0x1e240"==123456       //true
"0x1e240"=="1e240"      //false

当其中的一个字符串是 0x 开头的时候,PHP 会将此字符串解析成为十进制然后再进行比较

注意点

在最新版本中,弱比较不再将字符串解析成为十进制然后再进行比较

"0x1e240"=="123456"     //false
"0x1e240"==123456       //false

类型转换

<?php
  $test=1 + "10.5"; // $test=11.5(float)
  $test=1 + "-1.3e3"; //$test=-1299(float)
  $test=1 + "bob-1.3e3";//$test=1(int)
  $test=1 + "2admin";//$test=3(int)
  $test=1 + "admin2";//$test=1(int)
?>

PHP 手册:

当一个字符串当作一个数值来取值,其结果和类型如下:

  • 如果该字符串没有包含.,e,E,并且其数值在整型的范围之内,则该字符串被当作 int 来取值
  • 其他所有情况下都被作为 float 来取值

该字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值为 0

注意点

在PHP 8.0.0引入了数字字符串,PHP 8.0.0之后之前说的部分规则不适用了

<?php
  $foo = 1 + "10.5";                // $foo 是 float (11.5)
  $foo = 1 + "-1.3e3";              // $foo 是 float (-1299)
  $foo = 1 + "bob-1.3e3";           // PHP 8.0.0 起产生 TypeError;在此之前 $foo 是 integer (1)
  $foo = 1 + "bob3";                // PHP 8.0.0 起产生 TypeError;在此之前 $foo 是 integer (1)
  $foo = 1 + "10 Small Pigs";       // PHP 8.0.0 起,$foo 是 integer (11),并且产生 E_WARNING;在此之前产生 E_NOTICE
  $foo = 4 + "10.2 Little Piggies"; // PHP 8.0.0 起,$foo 是 float (14.2),并且产生 E_WARNING;在此之前产生 E_NOTICE
  $foo = "10.0 pigs " + 1;          // PHP 8.0.0 起,$foo 是 float (11),并且产生 E_WARNING;在此之前产生 E_NOTICE
  $foo = "10.0 pigs " + 1.0;        // PHP 8.0.0 起,$foo 是 float (11),并且产生 E_WARNING;在此之前产生 E_NOTICE
?>
intval() 函数

intval() 转换的时候,会将从字符串的开始进行转换,直到遇到一个非数字的字符

即使出现无法转换的字符串,intval() 不会报错而是返回 0

var_dump(intval('2'));   //2
var_dump(intval('3abcd'));   //3
var_dump(intval('abcd'));    //0

bool 欺骗

当存在json_decodeunserialize 的时候,部分结构会被解释成 bool 类型
json_decode 示例代码:

$json_str = '{"user":true,"pass":true}';
$data = json_decode($json_str,true);
if ($data['user'] == 'admin' && $data['pass']=='secirity')
{
    print_r('logined in as bool'."\n");
}

运行结果:logined in as bool

unserialize 示例代码:

$unserialize_str = 'a:2:{s:4:"user";b:1;s:4:"pass";b:1;}';
$data_unserialize = unserialize($unserialize_str);
var_dump($data_unserialize['user']);
if ($data_unserialize['user'] == 'admin' && $data_unserialize['pass']=='secirity')
{
  print_r('logined in as unserialize'."\n");
}

运行结果:

bool(true)

logined in as unserialize

数字转换问题

$user_id = ($_POST['user_id']);
if ($user_id == "1")
{
  $user_id = (int)($user_id);
  #$user_id = intval($user_id);
  $qry = "SELECT * FROM `users` WHERE user_id='$user_id';";
}
$result = mysql_query($qry) or die('<pre>' . mysql_error() . '</pre>' );

可以让 user_id=0.999999999999999999999即可绕过判断,并且最终执行查询的结果却是 user_id=0 的数据

<?php 
  var_dump("1" == 0.9999999999999999);//false
  var_dump("1" == 0.99999999999999999);//true
?>

int 和 intval 在转换数字的时候都是取不大于此数的最大整数

print((int)'0.9999999999999');//0
print((int)'1.1');//1

intval 还有个尽力模式,就是转换所有数字直到遇到非数字为止,如果采用:

if (intval($qq) === 123456)
{
  $db->query("select * from user where qq = $qq")
}

可以传入 123456 union select version()进行注入,实际执行

select * from user where qq = 123456 union select version()

PHP5.4.4 特殊情况

这个版本的 php 的一个修改导致两个数字型字符溢出导致比较相等

var_dump("61529519452809720693702583126814" == "61529519452809720000000000000000");

结果为

bool(true)

具体函数绕过

md5/sha1

用于对字符串进行 md5/sha1 加密

md5(string $string, bool $binary = false): string

如果可选的 binary 被设置为 true,那么 md5 摘要将以 16 字符长度的原始二进制格式返回

绕过方式1 - 数组

适用于强比较

通过传入数组去绕过,这两个函数不能处理数组数据

md5($_GET['name']) === md5($_GET['passwd'])

如果传入name[]=1&passwd[]=1,则两边的结果都会为false,相比较的结果就为true了

注意点

注意这里网上有些文章会说传入数组返回的是null,但其实更有可能是false,为什么是 false 而不是 null?

PHP 函数设计惯例:

  • 当函数因参数类型错误无法执行时,通常返回 false(如 md5(), strpos() 等)
  • 返回 null 的情况一般用于函数无返回值或未初始化变量,与参数错误无关

By the way,我没办法完全验证这一点,审计PHP源码审的有点头大,而官方文档也没有提及,如果有师傅可以给出证明,欢迎评论区指正

绕过方式2 - 科学计数法

仅适用于弱比较

php在处理0e开头,后接的字符都为数字的字符串的时候,会将整个字符串解析为科学计数法

当有另一个0e开头的字符串和这个进行对比时,php会都当作0来处理

<?php
  var_dump('0e45511255' == '0e1455155'); //bool(true) 
  var_dump('0e45511255' == '0');	//bool(true) 
?>

md5编码时,部分被加密的字符也会是0e开头的,比如网上常见的

QNKCDZO
0e830400451993494058024219903391

240610708
0e462097431906509019562988736854

s878926199a
0e545993274517709034328855841020
  
s155964671a
0e342768416822451524974117254469
  
s214587387a
0e848240448830537924465865611904
  
s214587387a
0e848240448830537924465865611904
  
s878926199a
0e545993274517709034328855841020
  
s1091221200a
0e940624217856561557816327384675
  
s1885207154a
0e509367213418206700842008763514
注意点

要记住核心原理,而不是看到md5加弱比较就直接套QNKCDZO240610708

例如如果实际的比较是md5(md5($a)."SALT") == "0e545993274517709034328855841020",直接套QNKCDZO240610708是没用的,这种情况只能写脚本爆破

以这种加密为例,爆破出来62778807294428677是可行的

绕过方式3 - md5碰撞

两个参数都可控,且是强比较的时候适用,比如md5($a) === md5($b)

这里就不记录Payload了,网上有很多

想自己研究玩玩可以看MD5 Collision Demo

strcmp

比较两个字符串并且区分大小写

  • 如果两个字符串相等,返回0
  • 如果string1 < string2,返回 <0 的值
  • 如果string1 > string2,返回 >0 的值
8.2.0 版本更新

当字符串长度不相等时,此函数不再保证返回 strlen($string1) - strlen($string2), 而可能返回 -1 或 1

绕过方式 - 数组

通过传入数组去绕过,该函数处理数组时会发生错误,strcmp会返回结果0,导致绕过

strcmp($_GET['pass'],$pass1)==0

如果传入pass[]=1,无论pass1是什么,strcmp返回结果都为0

注意点

在PHP的一些版本可能返回NULL或抛出错误,而不是0,差异主要源自 Zend 引擎在处理非预期类型输入时的改进和严格化处理

  • 当返回的是0时,可以绕过强比较 strcmp($_GET['pass'],$pass1) === 0
  • 当返回的是null是,可以绕过弱比较 strcmp($_GET['pass'],$pass1) == 0
  • 当仅抛出错误而中断执行时,无法绕过

json_decode

用于解码json格式的字符串

绕过方式 - 传入值为0的json

仅适用于弱比较

当有不同类型的数据时,该函数会转换为同一类型比较,这里会把原有的数据的数据类型转换为和传入数据的数据类型相同

<?php
  if (isset($_GET['m'])) {
      $m = json_decode($_GET['m']);
      $flag ="dfdfgdg";
      if ($m->flag == $flag) 
          echo "get flag";
  }
?>

传入一个json格式的字符,字符要用json格式去编写,这里设定名称为flag,传入json格式字符为{"flag":0}

为什么这里传入的值要是0呢?

当我们传入数字时,它会转化为同一类型进行比较,这里字符被转为0,我们传入的参数为0,所以相等

注意点

弱比较(松散比较)在PHP 8.0.0 之后,对于字符与0的比较会返回false而不是true(见文章最开头的表格)

switch

switch 语句类似于具有同一个表达式的一系列 if 语句

很多场合下需要把同一个变量(或表达式)与很多不同的值比较,并根据它等于哪个值来执行不同的代码

switch/case 作的是松散比较

绕过方式

switch选择的时候,处理的变量会被强转为int类型

当我们输入字符串4abcdesdf的时候,就会被强转为int类型,返回4的结果

注意点

最新版本中已失效,核心问题在于

var_dump("1aa" == 1); // false

in_array

用于检查数组中是否存在某个值

in_array(mixed $needle, array $haystack, bool $strict = false): bool

大海捞针,在大海(haystack)中搜索针( needle),如果没有设置 strict 则使用宽松的比较

绕过方式

如果第三个参数 strict 不设置或为false ,则 in_array() 函数不会检查 needle 的类型是否和 haystack 中的相同,导致弱类型,传入不同数据类型来绕过

<?php
    $array = array(
        'egg' => true,
        'cheese' => false,
        'hair' => 765,
        'goblins' => null,
        'ogres' => 'no ogres allowed in this array'
    );

    var_dump(in_array(null, $array)); // true
    var_dump(in_array(false, $array)); // true
    var_dump(in_array(765, $array)); // true
    var_dump(in_array(763, $array)); // true
    var_dump(in_array('egg', $array)); // true
    var_dump(in_array('hhh', $array)); // true
    var_dump(in_array(array(), $array)); // true 
    
    echo "<br>";

    var_dump(in_array(null, $array, true)); // true
    var_dump(in_array(false, $array, true)); // true
    var_dump(in_array(765, $array, true)); // true
    var_dump(in_array(763, $array, true)); // false
    var_dump(in_array('egg', $array, true)); // false
    var_dump(in_array('hhh', $array, true)); // false
    var_dump(in_array(array(), $array, true)); // false
?>
注意点

在 PHP 8.0.0 之前,string needle 在非严格模式下将会匹配数组中的值 0,反之亦然

在 PHP 8.0.0 之后已修正,与之前json_decode中提到的一样,其核心在于:

var_dump(0 == "aaa"); // false
var_dump("aaa" == 0); // false
var_dump(1 == "1aa"); // false
var_dump("1aa" == 1); // false

array_search

在数组中搜索给定的值,如果成功则返回首个相应的键名

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

在 haystack 中搜索 needle

绕过方式

如果可选的第三个参数 strict 不设置或为false,则 array_search() 将在 haystack 中检查相同的元素时不考虑类型

跟之前一样,使用"aaa"来类型转换匹配0,使用"1aa"来类型转换匹配1

也同样的,在PHP 8.0.0 之后已修正

$array=[0,1,2];
var_dump(array_search('aaa', $array)); 	// false
var_dump(array_search('1aa', $array));	// false

intval

用于获取变量的整数值

intval(mixed $value, int $base = 10): int

通过使用指定的进制 base 转换(默认是十进制),返回变量 value 的 int 数值

intval() 不能用于 object,否则会产生 E_WARNING 错误并返回 1

绕过方式

该函数处理本身不能处理的字符串时并不会报错,直接返回0

当函数内有其他的操作时,会把字符串这些转为数字类型再操作,而且在处理一些特殊数据的时候会有不同的处理结果

在 64 位系统上

echo intval(42) . "<br>";                      // 42
echo intval(4.2) . "<br>";                     // 4
echo intval('42') . "<br>";                    // 42
echo intval('42.1') . "<br>";                  // 42
echo intval('42.9') . "<br>";                  // 42
echo intval('42.0') . "<br>";                  // 42
echo intval('+42') . "<br>";                   // 42
echo intval('-42') . "<br>";                   // -42
echo intval(042) . "<br>";                     // 34
echo intval('042') . "<br>";                   // 42
echo intval(1e10) . "<br>";                    // 10000000000
echo intval('1e10') . "<br>";                  // 10000000000
echo intval(0x1A) . "<br>";                    // 26
echo intval('0x1A') . "<br>";                  // 0
echo intval('0x1A', 0) . "<br>";               // 26
echo intval(42000000) . "<br>";                // 42000000
echo intval(420000000000000000000) . "<br>";   // -4275113695319687168
echo intval('420000000000000000000') . "<br>"; // 9223372036854775807
echo intval(42, 8) . "<br>";                   // 42
echo intval('42', 8) . "<br>";                 // 34
echo intval(array()) . "<br>";                 // 0
echo intval(array('foo', 'bar')) . "<br>";     // 1
echo intval(false) . "<br>";                   // 0
echo intval(true) . "<br>";                    // 1
echo intval(null) . "<br>";                    // 0

测试一道ctf题的代码

@$a=$_GET['a'];
if(intval($a) < 2000 && intval($a +1) >2050){
    echo "get flag";
}

需要你传入$a的值小于2000,且加1后大于2050

可以传入参数0x2000intval('0x2000')的值为0,而intval('0x2000' + 1)则会将'0x2000'转化为16进制数字后完成计算

注意点

'0x2000' + 1在最新版本已不会自动转换16进制数字然后计算

var_dump('0x2000'+1); // int(1)
var_dump(intval('0x2000' + 1)); // int(1)

is_numeric

检测变量是否是数字或数字字符串

绕过方式

当传入数字开头,字母在后的字符串时,参数可以绕过某些检测,从而进行到下一步,而php处理不同字符串时又会把两个字符串转换到同一类型去处理

  $a="get flag";
  $temp = $_GET['m'];
  if(is_numeric($temp)){
      die("not allowed");
  }
  else if($temp>2010) {
      echo $a;
  }

这个代码里需要传入一个参数,参数要求大于2010又不能是数字,如果数字就会中途中断从而无法输出变量a里的内容

输入一个2020a的字符串,由于不是纯数字,执行到下一步,又因为php在处理数据对比时会将两个数据转换为同一类型的,从而达到绕过效果

strpos

查找字符串首次出现的位置

strpos(string $haystack, string $needle, int $offset = 0): int|false

返回 needle 在 haystack 中首次出现的数字位置

绕过方式 - 数组

strpos在处理数组时直接返回null,而没有返回false,导致问题发生

if (strpos($_GET['a'], '#asdafdasdadas') !== FALSE)
    die("get flag"); 
else
    echo 'can not get flag';

传入a[]=1,在strpos检测时,直接返回null,从而绕过

注意点

在最新版本已经失效,strpos在处理数组时直接返回的仍然为false

$a = $_GET['a']; //传入a[]=1
var_dump(strpos($a, '#asdafdasdadas') !== FALSE); // false

strlen+intval

让你传一个值,这个值长度要小于4,又要比500000大,你要如何处理?

$num  = @$_GET['num'];
if(isset($num) && strlen($num) <= 4 && intval($num + 1) > 500000)
{
    echo "get flag";
}
绕过方式

num=5e5

如果有错误或者遗漏的地方,欢迎各位师傅评论区指出~

  • PHP弱类型 - FreeBuf网络安全行业门户
  • 一文了解PHP的各类漏洞和绕过姿势-腾讯云开发者社区-腾讯云

相关文章:

  • Java 大视界 -- 基于 Java 的大数据隐私计算在医疗影像数据共享中的实践探索(158)
  • 【MinIO】Bucket的生命周期管理
  • python多态、静态方法和类方法
  • Open webui的使用
  • Docker Compose 启动jar包项目
  • dubbo http流量接入dubbo后端服务
  • GRS认证是什么?GRS认证有什么意义?对企业发展的好处
  • 剑指Offer49 -- DP_贪心
  • 高中数学联赛模拟试题第9套几何题
  • 使用YOLOv5训练自定义数据集
  • 阿里云云效 Maven
  • 前端技术有哪些
  • Canvas渲染管线解析:从API调用到像素落地的全过程
  • 蓝桥杯省模拟赛 阶乘求值
  • QEMU源码全解析 —— 块设备虚拟化(12)
  • 线性回归 + 基础优化算法
  • docker - compose up - d`命令解释,重复运行会覆盖原有容器吗
  • 滚珠花键的预压调整怎么做?
  • 附录C SLAC匹配过程命令定义与实际抓包
  • Go 语言标准库中math模块详细功能介绍与示例
  • 长治网站建设龙采科技技术支持/百度服务中心投诉
  • 寿阳网站建设/郑州网站建设制作
  • wordpress+资源站模板/百度助手app下载
  • 产品展示类网站/外贸网站推广费用
  • 找代理做网站多少钱/网络推广外包公司干什么的
  • 医院网站规划方案/win10系统优化软件