免责声明:本文章仅用于交流学习,因文章内容而产生的任何违法&未授权行为,与文章作者无关!!!
附:完整笔记目录~
ps:本人小白,笔记均在个人理解基础上整理,若有错误欢迎指正!
1.3 🐘松散比较(PHP)
-
引子:本章主要介绍一些由PHP自身语言特性可能产生的脆弱性,该内容往往被应用于PHP CTF入门题中,但在PHP Web开发时也可能被使用。
-
==
== 是php中的比较运算符,用于判断 == 左右两边的值是否相等。若两边的值类型不同,则会优先将两边的值类型转换为同一类型后再进行比较,也称这种比较方式为松散比较。
这其中使用最多也是最容易产生误解的就是数字、数字字符串、字符串之间的比较。这里给出官方规则:
If both operands are numeric strings, or one operand is a number and the other one is a numeric string, then the comparison is done numerically. These rules also apply to the switch statement.
简单来说,就是当数字与数字字符串,或者数字字符串与数字字符串之间进行比较时,都会被转换为数字后再比较,这些规则同样适用于switch。有没有感觉官方给出的内容有些含糊呢?这里举几个例子:// 在松散比较中,以下格式的字符串会被转换为什么数字呢?如: "sjjjer" "1999sj" "1999e9r" "1999.9er"
我们分别来测试一下:
<?php $data1 = "sjjjer"; $data2 = 0; echo $data1 == $data2 ? "true" : "false"; // true --> php版本为7.3 // false --> php版本为8.2 // 从官方给出的解释来看,在php8版本前字符串默认被转换为数字0参与比较,8版本后被修复
$data1 = "1999sj19"; $data2 = 1999; echo $data1 == $data2 ? "true" : "false"; // true // 可以看到,当字符串中同时出现字母与数字时,且数字在前, // 则在比较时会默认将字符串转换为出现第一个字母前的数字
$data1 = "1999e9r"; $data2 = 1999; echo $data1 == $data2 ? "true" : "false"; // false // 为什么此时松散比较结果为false呢? // 是由于1999e9在转换为数字参与比较时,其实际值为1999 * 10^9,e是科学计数法的标识。 // So 我们可以修改1999的值为1999000000000再尝试比较 $data3 = 1999000000000; echo $data3 == $data1 ? "true" : "false"; // true
$data1 = "1999.9er"; $data2 = 1999; echo $data1 == $data2 ? "true" : "false"; // false // 由于在比较时会将字符串转换为出现第一个字母前的数字 // 也就是 "1999.9er" --> 1999.9 (浮点数) // 当我们修改1999为1999.9时,再参与比较 $data3 = 1999.9; echo $data1 == $data3 ? "true" : "false"; // true
-
switch()
上述所提松散比较时的类型转换规则同样适用于switch分支结构,也就说明php中switch结构在进行数据比较时也采用松散比较,这里来测试一下。
demo:<?php echo "请输入数据:"; $data1 = fgets(STDIN); // 由于fgets()从键盘获取到的数据默认携带 \n, // 因此需使用trim()去除 $data1 = trim($data1); switch ($data1) { case 0: echo 0; break; case 19: echo 19; break; case "sj": echo "sj"; break; default: echo "数据未成功匹配!"; } // fgets()为获取用户所输入数据,默认返回值类型为字符串, // 也就是说data1的数据类型为字符串, // next 进入switch分支结构, // 该分支结构实质为字符串与数字、字符串与字符串的松散比较 /*请输入数据:sj --> php版本7.3 0 请输入数据:19er 19 请输入数据:sj --> php版本8.2 sj*/
-
md5()
已知md5加密后会生成固定32位字符串。而在php的身份验证中,常常会存在这样的逻辑:获取用户输入密码并将其转换为md5值,随后再与数据库所存储用户密码比较(数据库中用户密码往往加密存储),若相等则成功验证身份。
若开发者所使用比较运算符为 == ,也就是本文所提松散比较,则可能会出现以下情况:- 用户密码经md5加密后的结果为"0e656002536496242128899000718690"。
- 由上文可知 e 为科学计数法的标识,因此在php中"0e656002536496242128899000718690" 为一串数字字符串。且使用 == 进行松散比较。
- 由php松散比较规则可知,当数字串与数字串比较时,会被转换为数字进行比较。而0e后无论跟什么其结果均为0,因此仅需再寻找另一个字符串,使其满足经md5加密后的结果为0e开头的数字字符串,就能实现,即使所输入密码与用户原码密码不一致,但经md5加密后再进行松散比较,返回结果为true。
我们来验证一下:
<?php echo "请输入您的密码:"; $input = trim(fgets(STDIN)); $password = md5("000dSb96"); if (md5($input) == $password) { echo "恭喜您!密码输入正确"; } else { echo "Sorry,您的密码有误"; } // 请输入您的密码:001st0X5 // 恭喜您!密码输入正确
若想获取更多满足要求的字符串,可以写个脚本慢慢跑,这里给出我自己的脚本:
import hashlib import itertools import string # 定义函数,生成所有可能的8-16位字符串 def generate_strings(): # 所有可用的字符:数字和字母 chars = string.digits + string.ascii_lowercase + string.ascii_uppercase # 生成长度从8到16的字符串 for length in range(8, 17): # 生成当前长度的所有可能字符串 for s in itertools.product(chars, repeat=length): yield ''.join(s) # 定义函数,检查MD5值是否以0e开头且后续为数字 def check_md5(md5_hash): # 判断MD5值是否以"0e"开头,且后续部分全为数字 return md5_hash.startswith("0e") and md5_hash[2:].isdigit() # 主程序 def main(): # 打开result.txt文件以写入结果 with open("result.txt", "w") as result_file: # 符合条件的字符串数量 found_count = 0 # 遍历生成的所有字符串 for s in generate_strings(): # 对字符串进行MD5加密 md5_hash = hashlib.md5(s.encode('utf-8')).hexdigest() # 检查MD5是否满足条件 if check_md5(md5_hash): # 打印符合条件的字符串和MD5值 print(f"{s} {md5_hash}") # 写入到文件中 result_file.write(f"{s} {md5_hash}\n") found_count += 1 # 如果已经找到了10个符合条件的字符串,停止执行 if found_count >= 10: break if __name__ == "__main__": main()
10分钟就跑出来了五条,看来还有很大的优化空间。。。
# 结果展示 000dSb96 0e656002536496242128899000718690 001st0X5 0e632311920444249596217934156166 001uTksg 0e012344733002178183175029976663 001yLWsp 0e561475551818519587284167859133 001CKN1u 0e364973363767512736620399322212 0029F2eL 0e819654840882947331243228254063 002zDdF8 0e693440086514416381258215413583 0031LEiY 0e050990879989909849271876008654 003kjZ6a 0e813458376037199230741560480845 003mHOrg 0e186417107900720544959665557875
要是懒得用脚本跑,也可以参考:
https://blog.csdn.net/baidu_41871794/article/details/83750615
,这篇博文作者也给了一些例子。 -
strcmp()
php中的字符串比较函数,strcmp($str1, $str2)会逐个字符比较字符串str1,str2的ASCII值,若str1 < str2 则返回值<0,若str1 > str2 则返回值>0,若 str1 = str2 返回值为0。
而在php 8版本之前,若所比较值类型为非字符串时,此时strcmp()函数报错且return 0,也就是说虽然strcmp()所接收参数不为字符串类型,但最终仍判断其字符串值相同。
demo:<?php $str1 = "sjjjer"; $str2 = $_GET["str"]; if (strcmp($str1, $str2) == 0) { echo "两字符串相等"; } else { echo "两字符串不等"; } // url: http://192.168.2.106:81/simplectf/SimplectfDemo4.php?str[]=sj // Warning: strcmp() expects parameter 2 to be string, array given in SimplectfDemo4.php on line 4 // 两字符串相等
可以看到,虽然报错但仍执行了两字符串相等的逻辑。测试版本为php 7.3.4,php 8版本后修复。
-
in_array() & array_search()
in_array()用于查找指定值是否在数组中,返回true&false。array_search()用于查找指定值的键(索引),若找到返回索引值,若未找到返回false。同样的,这两个数组查找函数,若未指定其第三个值为true(默认为false),则采用松散比较。
demo:<?php $arr = [0, 19, "sj"]; var_dump(in_array("sjjjer", $arr)); var_dump(array_search("sj", $arr)); var_dump(array_search("sj", $arr, true)); // bool(true) // int(0) // int(2)
-
bool比较
上文案例所产生的有趣结果,主要基于数字、数字字符串、字符串之间的松散比较规则而成。
而接下来则对bool值与字符串的比较规则做一介绍:- ture 与 字符串比较结果为true,这里的字符串指:数字字符串(“19”),字符串(“sj”)。而false 与 字符串的比较结果为false,这里的字符串类型同true。
- true 与 空字符串比较结果为false,这里的空字符串指:“”,“0”。相反的,false 与 空字符串的比较结果为true。
注:这里的比较均为 ==
若想了解其余类型数据的比较结构,可参考官方比较表:
https://www.php.net/manual/en/types.comparisons.php
demo:<?php // 这里以json_decode() 与 unserialize()函数为例 $str = '{"username": true, "password": true}'; $data = json_decode($str); var_dump($data->username); if ($data->username == "admin" && $data->password == "sjjjer") { echo "身份认证成功!"; } else { echo "身份认证失败!"; } // bool(true) // 身份认证成功! // true与字符串松散比较结果为true。
// 正确值为:$str = 'a:2:{s:8:"username";s:5:"admin";s:8:"password";s:6:"sjjjer";}'; $str = 'a:2:{s:8:"username";b:1;s:8:"password";b:1;}'; // 其中b表示bool类型,1为true $data = unserialize($str); if ($data['username'] == 'admin' && $data['password'] == 'sjjjer') { echo "身份认证成功!"; } else { echo "身份认证失败!"; } // 身份认证成功!
-
===
以上内容均为开发者采用php松散比较时可能产生的结果,那有没有更为严谨的比较方式呢?采用===
比较,===
在比较时会先比较两边值类型,再比较值,若两边值类型不同也会返回false,也称这种比较方式为严格比较。
demo:<?php $data1 = "sj"; $data2 = 0; var_dump($data1 === $data2); // bool(false)
-
实验
好了,你现在已经大致了解php的松散比较规则,以及使用松散比较可能会产生的问题了。接下来做几道ctf入门题吧:-
第一题
<?php $num = $_GET['num']; // 判断$num的值是否为数字或数字字符串 if (!is_numeric($num)) { echo $num; if ($num == 1) { echo ' flag!'; } }
答案:
http://192.168.2.106:81/simplectf/SimplectfDemo8.php?num=1sj
-
第二题
<?php $md51 = md5('QNKCDZO'); $a = $_GET['a']; $md52 = md5($a); // 判断$a是否有值且值不为NULL if (isset($a)) { if ($a != 'QNKCDZO' && $md51 == $md52) { echo "flag"; } else { echo "false!!!"; } } else { echo "please input a"; }
答案:
http://192.168.2.106:81/simplectf/SimplectfDemo9.php?a=000dSb96
至此,本章内容结束!
-