松散比较(PHP)(小迪网络安全笔记~

免责声明:本文章仅用于交流学习,因文章内容而产生的任何违法&未授权行为,与文章作者无关!!!
附:完整笔记目录~
ps:本人小白,笔记均在个人理解基础上整理,若有错误欢迎指正!

1.3 🐘松散比较(PHP)

  1. 引子:本章主要介绍一些由PHP自身语言特性可能产生的脆弱性,该内容往往被应用于PHP CTF入门题中,但在PHP Web开发时也可能被使用。

  2. ==

    == 是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。有没有感觉官方给出的内容有些含糊呢?这里举几个例子:

    php 复制代码
    // 在松散比较中,以下格式的字符串会被转换为什么数字呢?如:
    "sjjjer"
    "1999sj"
    "1999e9r"
    "1999.9er"

    我们分别来测试一下:

    php 复制代码
    <?php
    $data1 = "sjjjer";
    $data2 = 0;
    echo $data1 == $data2 ? "true" : "false";
    // true --> php版本为7.3
    // false --> php版本为8.2
    // 从官方给出的解释来看,在php8版本前字符串默认被转换为数字0参与比较,8版本后被修复
    php 复制代码
    $data1 = "1999sj19";
    $data2 = 1999;
    echo $data1 == $data2 ? "true" : "false";
    // true
    // 可以看到,当字符串中同时出现字母与数字时,且数字在前,
    // 则在比较时会默认将字符串转换为出现第一个字母前的数字
    php 复制代码
    $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
    php 复制代码
    $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
  3. switch()

    上述所提松散比较时的类型转换规则同样适用于switch分支结构,也就说明php中switch结构在进行数据比较时也采用松散比较,这里来测试一下。

    demo:

    php 复制代码
    <?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*/
  4. md5()

    已知md5加密后会生成固定32位字符串。而在php的身份验证中,常常会存在这样的逻辑:获取用户输入密码并将其转换为md5值,随后再与数据库所存储用户密码比较(数据库中用户密码往往加密存储),若相等则成功验证身份。

    若开发者所使用比较运算符为 == ,也就是本文所提松散比较,则可能会出现以下情况:

    1. 用户密码经md5加密后的结果为"0e656002536496242128899000718690"。
    2. 由上文可知 e 为科学计数法的标识,因此在php中"0e656002536496242128899000718690" 为一串数字字符串。且使用 == 进行松散比较。
    3. 由php松散比较规则可知,当数字串与数字串比较时,会被转换为数字进行比较。而0e后无论跟什么其结果均为0,因此仅需再寻找另一个字符串,使其满足经md5加密后的结果为0e开头的数字字符串,就能实现,即使所输入密码与用户原码密码不一致,但经md5加密后再进行松散比较,返回结果为true。

    我们来验证一下:

    php 复制代码
    <?php
    echo "请输入您的密码:";
    $input = trim(fgets(STDIN));
    $password = md5("000dSb96");
    if (md5($input) == $password) {
        echo "恭喜您!密码输入正确";
    } else {
        echo "Sorry,您的密码有误";
    }
    
    // 请输入您的密码:001st0X5
    // 恭喜您!密码输入正确

    若想获取更多满足要求的字符串,可以写个脚本慢慢跑,这里给出我自己的脚本:

    python 复制代码
    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分钟就跑出来了五条,看来还有很大的优化空间。。。

    bash 复制代码
    # 结果展示
    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,这篇博文作者也给了一些例子。

  5. 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 复制代码
    <?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版本后修复。

  6. in_array() & array_search()

    in_array()用于查找指定值是否在数组中,返回true&false。array_search()用于查找指定值的键(索引),若找到返回索引值,若未找到返回false。同样的,这两个数组查找函数,若未指定其第三个值为true(默认为false),则采用松散比较。

    demo:

    php 复制代码
    <?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)
  7. bool比较

    上文案例所产生的有趣结果,主要基于数字、数字字符串、字符串之间的松散比较规则而成。

    而接下来则对bool值与字符串的比较规则做一介绍:

    1. ture 与 字符串比较结果为true,这里的字符串指:数字字符串("19"),字符串("sj")。而false 与 字符串的比较结果为false,这里的字符串类型同true。
    2. true 与 空字符串比较结果为false,这里的空字符串指:"","0"。相反的,false 与 空字符串的比较结果为true。
      注:这里的比较均为 ==

    若想了解其余类型数据的比较结构,可参考官方比较表:https://www.php.net/manual/en/types.comparisons.php

    demo:

    php 复制代码
    <?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。
    php 复制代码
    // 正确值为:$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 "身份认证失败!";
    }
    
    // 身份认证成功!
  8. ===

    以上内容均为开发者采用php松散比较时可能产生的结果,那有没有更为严谨的比较方式呢?采用===比较,===在比较时会先比较两边值类型,再比较值,若两边值类型不同也会返回false,也称这种比较方式为严格比较。

    demo:

    php 复制代码
    <?php
    $data1 = "sj";
    $data2 = 0;
    var_dump($data1 === $data2);
    
    // bool(false)
  9. 实验

    好了,你现在已经大致了解php的松散比较规则,以及使用松散比较可能会产生的问题了。接下来做几道ctf入门题吧:

    1. 第一题

      php 复制代码
      <?php
      $num = $_GET['num'];
      // 判断$num的值是否为数字或数字字符串
      if (!is_numeric($num)) {
          echo $num;
          if ($num == 1) {
              echo ' flag!';
          }
      }

      答案:

      bash 复制代码
      http://192.168.2.106:81/simplectf/SimplectfDemo8.php?num=1sj
    2. 第二题

      php 复制代码
      <?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";
      }

      答案:

      bash 复制代码
      http://192.168.2.106:81/simplectf/SimplectfDemo9.php?a=000dSb96

    至此,本章内容结束!

相关推荐
BingoGo1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack3 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理3 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
一次旅行4 天前
网络安全总结
安全·web安全
西岸行者4 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
red1giant_star4 天前
手把手教你用Vulhub复现ecshop collection_list-sqli漏洞(附完整POC)
安全
starlaky4 天前
Django入门笔记
笔记·django