首页 / 教程资源

PHP弱类型

发布时间:2023-05-17 15:38:40
                     

学网安渗透

扫码加我吧

免费&进群


Track安全社区投稿~  

千元稿费!还有保底奖励~

PHP弱类型简介

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

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

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

<?php
$var = 1;
$var = array();
$var = "string";
?>

弱类型的语言对变量的数据类型没有限制,你可以在任何地时候将变量赋值给任意的其他类型的变量,同时变量也可以转换成任意地其他类型的数据。
比如在 $a == $b 的比较中

$a = null; $b = false; //为真
$a = ''; $b = 0; //同样为真

php虽然属于弱语言,但是里面也有一些强语言类型相关的强制定义数据类型的方法

比如php里的 == 和 === 之间的区别
== 为松散比较 只比较值,不比较数据类型

而 === 为严格比较,不仅比较值也比较类型,可以看作是强语言的强制数据类型要相同

然而,php 内核的开发者原本是想让程序员借由这种不需要声明的体系,更加高效的开发,所以在几乎所有内置函数以及基本结构中使用了很多松散的比较和转换,防止程序中的变量因为程序员的不规范而频繁的报错,然而这却带来了安全问题。

php 内核之 zval 结构

在 PHP 中声明的变量,在 ZE 中都是用结构体 zval 来保存的
php 内核中弱类型的封装

typedef struct _zval_struct zval;  

struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};

typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;

其中 php 通过 type 判断变量类型 存入 value。

类型转换问题

zval.type 决定了存储到 zval.value 的类型。当源代码进行一些未限制类型的比较,或数学运算的时候,可能会导致 zval.type 的改变,同时影响 zval.value 的内容改变。

比较操作符

=== 在进行比较的时候,会先判断两种字符串的类型是否相等,再比较
== 在进行比较的时候,会先将字符串类型转化成相同,再比较
如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换成数值并且比较按照数值来进行
比较简单,不过多举例。

  • Hash 比较缺陷

    "0e132456789"=="0e7124511451155" //true
    "0e123456abc"=="0e1dddada" //false
    "0e1abc"=="0" //true

    在进行比较运算时,如果遇到了 0e\d + 这种字符串,就会将这种字符串解析为科学计数法。如果不满足 0e\d + 这种模式,就会当作字符串进行比较,所以不会相等。

  • 十六进制转换

    "0x1e240"=="123456"     //true
    "0x1e240"==123456 //true
    "0x1e240"=="1e240" //false

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

    类型转换

    <?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。

    intval () 函数:intval () 转换的时候,会将从字符串的开始进行转换知道遇到一个非数字的字符。即使出现无法转换的字符串,intval () 不会报错而是返回 0。

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

bool 欺骗

当存在 json_decode 和 unserialize 的时候,部分结构会被解释成 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 unserialize'."\n");
}

运行结果:

bool(true) 
logined in 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() 进行注入。

PHP5.4.4 特殊情况

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

$ php -r 'var_dump("61529519452809720693702583126814" == "61529519452809720000000000000000");'
bool(true)

PHP弱类型危害

危害具体表现在比如像一些数据验证上,两个变量类型不匹配时进行类型转换时可能会将传递的参数类型转换,一些函数存在松散性的问题,调用时,给函数传递函数无法处理的参数类型但是没有报错,直接返回null,这些都有可能产生各种问题

这里以一个ctf题,举例:
示例代码:

<?php
$pass=@$_GET['pass'];
$pass2=@$_GET['pass2'];
@$pass1='asdasdasd';
if(strcmp($pass,$pass2)==0 && $pass != $pass2){
echo "$pass1";
}else{
echo "aaaa!";
}
?>

这里需要传入两个变量,对比两个参数要相同且两个字符串内容要不同
那么怎么才能让它相同呢,这里可以通过传入数组去让它等

分别传入不同的数组,结果为

传入参数相同时

通过数组就绕过了限制,具体为什么可以看后面关于该函数的介绍

函数分析

常见函数:md5(),sha1(),strcmp(),switch(),array_search(),in_array(),json_decode(),intval(),strpos(),is_numeric()

1.1:MD5(),sha1()

这个函数用于对字符串进行MD5加密
函数格式:md5(要加密的字符串,规定的输出格式)

绕过方法

通过传入数组去绕过,这两个函数不能处理数组数据,md5还有一个加密的问题

示例代码:

<?php

$flag = "1234";

if (isset($_GET['name']) and isset($_GET['passwd']))
{
if ($_GET['name'] == $_GET['passwd'])
echo '<p>error</p>';
else if (md5($_GET['name']) === md5($_GET['passwd']))
die('Flag: '.$flag);
else
echo '<p>Invalid passwd.</p>';
}
else
echo '<p>Login first!</p>';
?>

测试效果如下

问题在于该函数不能处理数组内容,处理数组会出错,返回结果都为false
这里的sha1() 也是同样道理
sha1函数用于把字符串转换为sha-1加密处理
函数格式:
sha1(需要处理的字符串,需要的输出格式,二进制或16进制)

测试代码:

<?php

$flag = "flag{mkluiop}";

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

测试效果如下

这里处理数组的结果都返回了false 都相等,导致绕过

1.2 md5加密相等问题

关于md5还有个科学计数法的问题

绕过方式

php在处理0e开头,后接的字符都为数字的的字符串的时候,会将整个字符串解析为科学计数法,当有另一个0e开头的字符串和这个进行对比时,php会都当作0来处理

示例如下
<?php
var_dump(‘0e45511255’ == ‘0e1455155’);
var_dump(‘0e45511255’ == ‘0’);
?>
得到结果为

md5编码时,部分被加密的字符也会是0e开头的

示例代码

<?php
$m1 = md5('s1091221200a');
$a = @$_GET['a'];
$m2 = @md5($a);
if(isset($a)) {
if ($a != 's1091221200a' && $m1 == $m2) {
echo "good";
} else {
echo "hahaha";
}
}
?>

这里我们传入一个md5编码后开头是0e的字符串,网上找了一个,aabC9RqS,其他的还有很多,网址 https://www.cnblogs.com/R4v3n/articles/7716671.html

得到结果为

1.3 strcmp()

这个函数的作用是比较两个字符串并且区分大小写
当字符串1大于字符串2就返回>0,当字符串1小于字符串2就返回<0,相等则返回0
函数格式: strcmp(第一个需要比较的字符串,第二个需要比较的字符串)

绕过方式

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

测试代码

<?php
$pass1="ASDSADSADAD";
if(isset($_GET['pass'])){
if(strcmp($_GET['pass'],$pass1)==0){
echo "233";
}else{
echo " ";
}
}
?>

测试效果如下

1.4 json_decode()

这个函数用于解码json格式的字符串

函数格式 json_decode(需要解码的字符串)

绕过方式

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

测试代码

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

这里要传入一个json格式的字符,字符要用json格式去编写,这里设定名称为flag,传入json格式字符为{“flag”:0},为什么这里传入的值要是0呢?这里当我们传入数字时,它会转化为同一类型进行比较(这里关键点在于,要把字符转换为数字有效,因为字符转换为数字会出问题,字符会被转为0),这里字符被转为0,我们传入的参数为0,所以相等

测试如下

1.5 switch函数

绕过方式

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

这个函数一般用于根据多个条件选择执行不同动作,和if…. else相似

函数格式
switch(表达式,通常是变量)
{
case xxxx1:
当条件符合xxxx1时执行的代码
break;
case xxxx2:
当条件符合xxxx2时执行的代码
break;
….
}
这里的问题在于switch选择的时候,处理的变量会被强转为int类型

测试代码

<?php
$a=$_GET['$a'];
switch ($a) {
case 1:
echo "error";
break;
case 2:
echo "error2";
break;
case 3:
echo "error3";
break;
case 4:
echo "233";
break;
}
?>

这里当我们输入字符串时,会被强转为int类型的
当我们输入字符串4abcdesdf的时候,这个$a就会被强转为int类型,使$a的值为4

测试效果如下

1.6 in_array函数

函数格式 in_array(需要在数组内搜索的数值,被搜索的数组,一个可选参数,设置TURE检查数据和数组值类型是否相同)

绕过方式

这个函数的作用是检查数组中是否存在某个值,当没有最后的检测参数为true时,默认为松散比较,导致弱类型,传入不同数据类型来绕过

关键就在于这个最后的检查的参数,如果没有这个参数的话,就会使用松散比较来判断

示例代码

<?php
$array=[1,2,3];
var_dump(in_array('aaa', $array));
var_dump(in_array('1aa', $array));
?>

测试结果如下

这里如果没有设置检查参数为ture的话,会进行松散比较,数据类型不同的话,会进行适当的类型转换,这里aaa被转换为int类型,被转换为0,匹配失败,但是1aa 在转换时被转换为1,匹配成功

1.7 array_search()函数

绕过方式

1.传入不同数据类型来绕过,没有最后的检测参数为true时,默认为松散比较,导致弱类型
2.当数据类型不同会先把原有数据的数据类型的进行转换,导致弱类型的产生

该函数的作用是在数组中搜索某个键值,并返回键名
函数格式 array_search(要搜索的键值,被搜索的数组,设置TURE检查数据和数组值类型是否相同)

这个函数有两个问题
第一个
测试代码

<?php
$array=[1,2];
var_dump(array_search('aaa', $array));
var_dump(array_search('2aa', $array));
?>

测试效果如下

和前面in_array一样,当没有设定检测参数为true时,会进行松散比较,会把数据类型进行转换,然后执行

第二个问题

测试代码:

<?php
if(!is_array($_GET['a'])){exit();}
$a=$_GET['a'];
for($b=0;$b<count($a);$b++){
if($a[$b]==="admin"){
echo "error";
exit();
}
$a[$b]=intval($a[$b]);
}
print_r($a);
if(array_search("admin",$a)===0){
echo "233";
}
else{
echo "false";
}
?>

测试效果如下

首先,这里是判断数组的,所以要首先弄个数组,这里设置数组值为0,然后判断字符串admin的值和这个0是否相等,这里数据类型不同会先把admin这个字符串类型的进行转换,结果为0,所以相等

1.8 intval() 函数

该函数用于获取变量的整数值

函数格式 intval(要转换的参数,指定转换需要的进制)

绕过方法:

该函数处理本身不能处理的字符串时并不会报错,直接返回0,当函数内有其他的操作时,会把字符串这些转为数字类型再操作,而且在处理一些特殊数据的时候会有不同的处理结果

找了网上常用的intval函数处理不同数据的结果的测试代码,改了一下

<?php
echo intval(25); //25
echo '<br/>';
echo intval(2.5); //2
echo '<br/>';
echo intval('25'); //25
echo '<br/>';
echo intval('+25'); //25
echo '<br/>';
echo intval('-25');//-25
echo '<br/>';
echo intval(025);//21
echo '<br/>';
echo intval('025');//25
echo '<br/>';
echo intval(1e10);//1410065408
echo '<br/>';
echo intval('1e10');//1
echo '<br/>';
echo intval(0x1A);//26
echo '<br/>';
echo intval(25000000);//25000000
echo '<br/>';
echo intval(250000000000000000000);//2007498752
echo '<br/>';
echo intval('420000000000000000000');//2147483647
echo '<br/>';
echo intval(25, 8);//25
echo '<br/>';
echo intval('0x42');//0
echo '<br/>';
echo intval(array());//0
echo '<br/>';
echo intval(array('aaa', 'abb'));//1
echo '<br/>';
echo intval('aaa', 8);//0
echo '<br/>';
echo intval('fgh');//0
echo '<br/>';
echo intval('1e10'+8);//1410065416
echo '<br/>';
echo intval('0x2000'+8);//8200
?>

测试结果如下

这里网上找了一个ctf题的代码,测试一下

<?php
@$a=$_GET['a'];
if(intval($a) < 2000 && intval($a +1) >2050){
echo "hahahah";
}
else
{
echo "233";
}
?>

这里需要你传入$a的值小于2000,且加1后大于2050,那么怎么样才能绕过呢?
回看上面的,看这个函数是怎么处理不同类型的字符串的,看最后一条和倒数第七条,当我们只是传入一个单引号闭合的0x2000时,这里当作一个字符串去处理该参数,返回结果为1,当后面在该函数内进行加操作时,该参数为16进制的字符串,被解析为十进制数字去处理,可以在电脑上打开计算器试下

后续进行了加8操作,得到8200,本题就可以通过此方式来完成

传入参数0x2000
得到结果

1.9 is_numeric函数

这个函数用于检测变量是否只由数字组成,否则返回false
函数格式is_numeric(目标字符)

绕过方法

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

测试代码

<?php
@$a="hehehehehe";
@$temp = $_GET['p'];
if(is_numeric($temp)){
die("hahha");
}
else if($temp>2010) {
echo $a;
}
?>

这个代码里需要传入一个参数,参数要求大于2010又不能是数字,如果数字就会中途中断从而无法输出变量a里的内容,这里我们输入一个2020a的字符串,输入后由于不是纯数字,执行到下一步,又因为php在处理数据对比时会将两个数据转换为同一类型的,从而达到绕过效果

结果如下

2.0 strpos函数

strpos函数用于查找目标字符在字符串中出现的第一次的位置

函数格式:strpos(字符串,目标字符)

绕过方法

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

测试代码

<?php
if (isset ($_GET['a'])) {
if(@ereg("^[1-9]+$", $_GET['a']) === FALSE)
echo '必须输入数字才行';
else if (strpos ($_GET['a'], '#asdafdasdadas') !== FALSE)
die("bababab");
else
echo 'hoho';
}
?>

这里通过数组的形式去传入一个数字,首先绕过第一道检测,然后在第二处strpos检测时,通过数组去绕过,直接返回null,从而绕过

2.1 strlen函数 + intval函数

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

测试代码:

<?php
highlight_file(__FILE__);
$num = @$_GET['num'];
if(isset($num) && strlen($num) <= 4 && intval($num + 1) > 500000)
{
echo file_get_contents('flag');
}

绕过方法:5e5

因为在strlen()函数的判断中,5e5被认为是字符串,长度为3,但是当他跟数字比较或者进行数学运算的时候,会被当成科学计数法,转成数字,也就是5乘以10的5次方,也就是500000,500000+1,自然是比500000大的

申明:本公众号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,

所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.


没看够~?欢迎关注!

分享本文到朋友圈,可以凭截图找老师领取

上千教程+工具+交流群+靶场账号

 

 分享后扫码加我


回顾往期内容

Xray挂机刷漏洞

零基础学黑客,该怎么学?

网络安全人员必考的几本证书!

文库|内网神器cs4.0使用说明书

代码审计 | 这个CNVD证书拿的有点轻松

【精选】SRC快速入门+上分小秘籍+实战指南

    代理池工具撰写 | 只有无尽的跳转,没有封禁的IP!

点赞+在看支持一下吧~感谢看官老爷~ 

你的点赞是我更新的动力

相关推荐