华体会在线平台:【技术分享】从内核层面分析部分PHP函数

来源:华体会app官方下载 作者:hth华体会体育app 时间:2022-09-29 09:29:05

  对一些函数缺陷的分析,从PHP内核层面来分析一些函数的可利用的地方,标题所说的函数缺陷并不一定是函数本身的缺陷,也可能是函数在使用过程中存在某些问题,造成了漏洞,以下是对部分函数的分析

  三个$id分别对应die、输出语句、输出语句,这就是因为in_array函数的第三个参数导致的,先贴出函数原型:

  通过$id=1也可以很明显的知道,在这里之所以能通过in_array肯定是仅是进行了弱类型的比较,因为此处$whiteList的键值的int型,如果进行强比较是无法通过的,下面对该函数进行分析:

  /*参数定义,需要2-3个参数,前两个参数是必须的,后面参数是可选的,而可选参数默认是0*/

  由于使用默认strict,因此这里进行省略,默认可选参数下会进入后面的else分支,当我们传入的value是字符型时进入else if (Z_TYPE_P(value) == IS_STRING) {中,这里先获得数组的值,然后进入关键函数fast_equal_check_string中,可以看到当是默认strict时都是用该函数进行判断,跟进:

  这里op2是整形,因此会直接使用compare_function进行比较,该函数实在太长,主要是现根据两个参数的数据类型来进行逻辑处理,这里op1 = IS_STRING,op2 = IS_LONG,但是IS_LONG跟IS_STRING的情况不存在,就进入转换类型的分支,所以这里直接将对应的处理部分贴下:

  if(!converted)判断了左右操作数还没有转换为number时候的处理,接着调用zendi_convert_scalar_to_number直接将其转换为number.因此这部分代码当操作数不是数字数据类型时,它们将被转换为数字,然后使得转换标志converted = 1,不妨跟踪下这个宏函数:

  这里就是字符转数字的关键步骤了,将字符串字符一个一个的处理,如果当前字符是数字就进行tmp_lval*10 + (*ptr) - 0运算,直到遇到第一个不是数字的字符,并且最终会return 0

  整个流程分析完成,也就是in_array如果第三个参数为0或者默认时,仅是对数组中的值和查询的值进行一个弱类型比较

  上面这个例子中可以知道,如果没有对data协议进行过滤,我们可以使用data协议来进行绕过,无论怎么绕过,第一层绕过的始终是filter_var($url,FILTER_VALIDATE_URL)对这个函数进行分析,先将其函数原型贴出:

  FILTER_VALIDATE_URL是一种过滤器,用来判断是否是一个合法的url,不过该判断是一个非常弱的判断,可以从下面的例子中发现:

  这些都能够通过filter_var,既然如此看似不合法的url都能通过,这里还是从源码进行分析:

  根据filter来寻找过滤的函数,将value转化成字符串后调用寻找到的过滤函数来进行处理,URL过滤器对应的函数:

  这里还对Ipv6地址进行了判断,以及后面对域名的判断,我们重点还是看php_url_parse_ex函数:

  如果p指向的值是字母或者数字或者是+,-,.则指针指向下一位,这就代表冒号前面的值其实是任意的字母、数字、+、-、.

  如果冒号所在位置小于str,且?#在冒号后面(如果有的话),就跳转到port解析部分

  如果str的长度大于1且str的前两个字符是//,s指向//后面的一个字符,e变为0,跳转到host解析

  当p小于str且p指向的为数字字符,p一直指向后一位,直到p指向str末尾或者p指向的字符为/,同时冒号后面的数字位数小于6位,跳转到port解析

  如果冒号后面不是纯数字或数字后面有一个/,那么冒号前面的内容就当作scheme,放在ret的scheme参数中,s指向冒号后一位,跳转到path解析

  如果冒号后面是/,那么冒号前面的内容就当作scheme,放在ret的scheme参数中。如果下面一位也是/,那么s指向//后面一位,如果scheme为file,那么判断接下来一位是不是/,如果是,判断冒号后是否有五个字符,如果有那么第五个字符是不是冒号(为了处理file:///c:),s指向///后的一位字符,跳转到path解析

  因此只要是该url满足其要求,都能够通过filter_var的url过滤器,所以在这里强调其实一个非常弱的过滤器,其实在parse_url的处理中底层也使用了该函数,不过关于parse_url的函数缺陷,之后在进行分析,因此在此题中如果没有过滤data协议,完全可以使用data协议进行绕过,但是此时由于过滤data://,因此使用的是另外一个协议compress.zlib

  根据上述内容也就解释了为什么形如xx://xx;google.com也能通过parse_url,因为在底层处理中,完全没有涉及分号;的对应处理,在解析时也只是将其视为一个简单的字符串,但如果是http或者是https协议则会进行_php_filter_validate_domain处理,导致无法使用该种形式进行绕过

  可以看到curl和parse_url对于解析host的相对位置是不同的,在PHP中parse_url倾向于解析较后面,其实在源码中有着相应的答案:

  看到这一段,对于user和password的解析过程,先对整个字符串判断是否出现@,并将指针指向p,如果有则判断@前面是否出现冒号,如果有则指向pp,将冒号前的字符串赋值给$ret-user,就是user,将冒号后一直到@符号前的字符串赋值给$ret-pass,否则若是字符串没有出现冒号,则将@符号前面的整个字符串赋值为$ret-user,这一段的关键在于如何定位@符号,也就是zend_memrchr函数是如何实现的?

  可以很明显的知道,该函数的搜索过程是自后向前的,因此他会从最后一个匹配@的字符串进行返回,因此在parse_url中,host匹配最后一个@后面符合格式的host

  发现这里居然调用的是php_url_parse_ex,那不是和parse_url调用的一个底层函数,但是我们仔细看,php_url_parse_ex这个函数在这里的作用就是解析这个url使用了什么协议,再根据解析出来的协议$uri-scheme对比是否是file协议,真正调用过程中对url的解析处理在libcurl中,因为phpcurl也相当于是调用libcurl的动态链接库,不过貌似很早已经被修复了,因此到这里不在继续,能够从源码角度了解parse_url解析时,host匹配的是最后一个@后面符合格式的host就行[+]strpos函数利用

  一般在匹配某段输入中存在黑名单中的关键词,可能会使用strpos进行比较,该函数是用来查找字符串首次出现的位置,下面贴一下其函数原型:

  这里的业务是通过格式化字符串的方式,使用xml结构存储用户的登录信息,并且判断$user或者$pass不能含有尖括号以避免注入,但是使用这种方式真的可以防止注入吗?

  成功进行了注入,但是在我们的输入中是存在尖括号,为何strpos会失效呢?

  strpos函数返回查找到的子字符串的下标。如果字符串开头就是我们要搜索的目标,则返回下标0;如果搜索不到,则返回false这里没有对strpos有深刻的理解,导致在这里直接将结果进行取反,而我们知道false和0取反后的效果是等价的,均为真,因此这里针对尖括号的过滤是有权限的,但事实上并非strpos的缺陷,只是在使用时存在缺陷导致了绕过,因此如果用strpos来判断字符串中是否存在某个字符时必须使用===false

  其中第三个参数offset指从字符串的第几位开始,默认是0,主要是调用php_memnstr进行搜素匹配,其为函数zend_memnstr的宏定义:

  }if(p ==NULL){//如果连头一个字符都没找着,则停止查找最合适

  也是比较容易理解,此处理解strpos在未匹配到时返回False,在第一个位置匹配到将会返回0,并且正确使用strpos即可

  功能 :parse_str的作用就是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否存在,所以会直接覆盖掉当前作用域中原有的变量。

  如果 encoded_string 是 URL 传入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result 则会设置到该数组里 )。

  也就是说,该函数将字符串赋值式中的字符串解析为变量,值为该变量的值,并且不会检验变量是否已经存在,如果存在也会将其覆盖,变量覆盖的相关漏洞也很常见了,这里不再谈论变量覆盖的利用问题,而是先从底层开始分析,再看从底层能够发掘出什么样的问题,注意从PHP7.2开始不使用第二个参数会出现警告,但是不影响程序执行

  在请求初始化时,通过调用 php_hash_environment 函数初始化以上的六个预定义的变量。如下所示为php_hash_environment函数的部分代码(考虑到篇幅问题)

  可以看到对于GET型的处理是第一种,而对于parse_str也就是PARSE_STRING是第三种,继续向下跟进:

  会对变量的空格、点变成下划线,而当解析变量中出现形如a[b中会将is_array=1

  并且在此之前前文代码中已经提到,会先对传入变量进行urldecode处理,因此总结起来为:

  PHP需要将所有参数转换为有效的变量名,在解析查询字符串时,它会做两件事:

  因此我们基于此可以将填充数据后最终解析成相同变量的所有不同填充形式的数据都进行fuzz,这里贴下已有的脚本

  我们知道命令注入是很常见的一个问题,原因就是开发者对于用户的输入没有进行过滤而直接拼接到语句中进行执行,因此在PHP中提供了若干对输入进行过滤的函数以避免命令注入,这里最常见的便是escapeshellcmd和escapeshellarg

  功能 :escapeshellarg 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec,system 执行运算符(反引号)

  escapeshellcmd 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。此函数保证用户输入的数据在传送到 exec 或 system 函数,或者 执行操作符 之前进行转义。

  反斜线(\)会在以下字符之前插入:`*?~^[]{}$\, \x0A 和 \xFF。‘ 和 “ 仅在不配对儿的时候被转义。在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。

  escapeshellcmd允许执行多个参数意味着如果该可执行程序中本身就有能够执行命令或者读取文件的参数,最典型的例子就是find,示例如上,escapeshellarg的处理是如果字符串中出现了则先将其转义后,再将每一部分用单引号括起来,但是这里如果将escapeshellarg和escapeshellcmd混用将会导致参数的注入.

  这里我们借助BUUCTF 2018 Online Tool这个题来分析这两个函数的源码以及利用的过程:

  可以看到这里使用escapeshellarg和escapeshellcmd对$host进行过滤后拼接到了nmap语句中,这样两重过滤应该显得更严格,但是事实并非如此,namp中有一个参数-oG可以实现将命令和结果写到文件,所以我们可以控制自己的输入写入文件。但是如何进行绕过上面两个函数的过滤呢?

  可以很明显的看到,如果字符串中存在单引号,则先在其前面一次填充单引号,斜杠,单引号,再将这个原本的单引号处理

  因此总结起来就是首先将字符串用单引号包围,然后遇到单引号则在其前面加上单引号,反斜杠,单引号,各位有兴趣也可以自己验证一下看是否和源码一致

  和输出是一样的,这样原来的exp经过过滤后变成了如上的形状,接着再经过escapeshellcmd的处理,同样在底层调用php_escape_shell_cmd函数,继续分析:

  可以看到,如果是有引号存在,会在该引号后面的字符串中寻找是否还出现引号,如果出现则不处理,但如果没有则会加入反斜杠进行转义,同时会判断

  而在linux中是支持空白连接符的,并且\这里视为将反斜杠进行转义,linux解析时双反斜杠也不会影响其他语句的执行:

  关于$_REQUEST这个全局变量应该都不陌生,在官方文档中是如下描述的:

  也就是说虽然$_REQUEST默认情况下包含了$_GET,$_POST和$_COOKIE,但是对一方的处理完全不会影响另一方,这里如果我们想要对输入进行过滤,而代码是这样的

  本意是想对输入全部进行转义,这样能够避免注入,但事实上对$_GET和$_POST的操作并不会影响$_REQUEST这个预定义变量,下面我们从内核角度来看$_REQUEST是如何实现的

  在请求初始化时,通过调用 php_hash_environment 函数初始化以上的六个预定义的变量。

  这里只是将$_GET, $_POST, $_COOKIEmerge起来,调用php_autoglobal_merge将相关变量的值写到符号表,最终调用zend_hash_update,将相关变量的值赋值给&EG(symbol_table),因此在这里$_REQUEST可以理解为一个全新的预定义变量,因其对$_POST、$_GET并不是引用,而知相当于将其重新赋值后写到符号表中,是一个崭新的变量(个人是如此理解的,如有不当,还请谅解)

  而variables_order这其实是用来控制PHP是否生成某个大变量以及大变量的生成顺序,该顺序在php.ini中已经定义

  因此整个预定义变量$_REQUEST的原理也就至此,该种特性之前也被用来在CTF中进行考察

  总的来说,从内核层面来分析函数能够对函数有更深层次的了解,在此过程中需要结合动态调试来试着分析整个执行过程,并且需要对PHP的内核有一定的了解,对函数的参数类型和个数要有一定的敏感度,并且多去查阅官方文档,对函数参数以及官方文档中标注意的地方多关注。