php 中的转义与正则
前言
今天机缘巧合审出国外某 cms 的一个 xss,其产生的主要原因是因为开发者对 php 中转义和正则的书写有误而导致的,这让我想起以前看的一篇文章。
参考:https://www.cdxy.me/?p=756
php 中的转义
很多人可能知道在 php 中,双引号中的内容会转义,变量会被解析成对应的值,单引号中却不会。实际上,这样说不是很正确。
对于变量来说,只有双引号中的变量能够被解析。
单引号中的$
没有特殊含义,即不会和后面的字符组合成变量并解析。
对于转义字符来说,会稍微复杂点,规则如下。
反斜线会根据其后面紧跟的字符是否需要转义和是否会导致解析出错而进行转义。
对于反斜线本身来说,因为它是转义字符的开始标志,所以其本身在任何情况下都需要转义,无论单引号还是双引号中。
接下来主要看解析出错时的转义。我们都知道在 php 中字符串以单引号或双引号开始。因为没有研究过 php 源码,所以我猜测引号中内容的解析过程如下。
php 看到第一个引号,开始解析字符串中的内容,一直解析到下一个没有被转义的对应引号结束。解析时,如果看到一个反斜线,会继续看反斜线后面紧跟的字符和该反斜线是否会组成一个转义字符,如果不会,则直接显示反斜线和该字符。而判断是否需要转义,主要有两点,第一是一些特殊字符,如制表符\t
、回车符\r
、换行符\n
等。第二是是否会导致解析出错。
在单引号中,第一个判断条件是不能用的,只有第二个条件才会影响反斜线的转义。
为什么这种情况下需要转义单引号呢?因为这个字符串是从单引号开始解析的,所以第一个单引号之后的单引号代表该字符串解析结束,若想在单引号中表示单引号就需要转义。最后的代码echo ''';
在脚本中会直接报错。
在双引号中,第一个判断条件可用于判断字符的转义情况。
总结一下,只有双引号可以解析被其包裹的变量,单引号不行。对于特殊字符,在双引号中要用转义字符表示(如\r
、\$
),$
也需要转义是因为,双引号中可以解析变量,而这是变量名开始的标志。对于导致解析出错的字符(单引号中的单引号,双引号中的双引号),单引号和双引号中都需要转义。
在上面单引号的示例的第一行代码中可以看到,当反斜线和后面紧跟的字符不会组成转义字符时(在双引号中\r
等可表示特殊字符,单引号中不解析),反斜线和该字符会直接输出。
1 | php > echo '\r\n\abc'; |
如下情况就会报错。
1 | php > echo '\\''; |
因为反斜线在单、双引号中都需要转义,所以第一个反斜线会转义其后紧跟的反斜线,而第二个反斜线紧跟的单引号就没有反斜线去转义,多了个单引号导致字符串解析出错。
题目解析
我们看一下参考文章的那道题目。
该代码逻辑就是,根据用户输入修改指定文件中的内容,将$option='default'
改为$option='xx'
。该场景可能出现在写配置信息时,如安装某 cms 时向文件写入数据库相关信息等。
参考文章中,使用了三种方法,分别为preg_replace
函数特性、%0a
、%00
( addslashes 处理后为\0
),这里就讲下第一种方法。
从输入到写入 file 变量的整个过程中,输入值的变化如下:
1 | \' --> \\\' --> \\' |
先是\'
经过 addslashes 函数处理后,变成\\\'
,然后因为 preg_replace 函数的特性,变成\\'
,可以理解为第一个反斜线转义了一个反斜线。为什么说是 preg_replace 的特性呢?我在中间用双引号的解析变量特性将\\\'
赋值给了变量$a
,打印后其值和输入相同,所以和 preg_replace 第二个参数处的双引号无关,我们也可以这样写。
直接模拟 str 变量进入第二个参数,输出的结果和上面一样,也会因为 preg_replace 的特性转义了反斜线。所以对于这题,输入\'
并利用 preg_replace 转义反斜线的特性即可逃逸单引号。
转义与正则
今天审到的类似如下代码,该过滤代码是为了防止 xss。
1 | $a = preg_replace("/[^a-zA-Z0-9.\ -_]+/", "", $a); |
结合上面知道,在 php 中,要在引号中表示反斜线有两种方式,反斜线转义反斜线\\
,或者反斜线和其后内容不会组成转义字符时,反斜线表示为\
字符。
可以看到,反斜线后内容需要转义时,会产生转义的效果,这样最后一行代码就少了闭合的双引号而导致报错了。
在正则表达式中,也用反斜线来转义有特殊意义的字符。看如下代码。
我们本义是将\
替换为空,可是 preg_replace 函数发生错误返回了 null。猜测下该函数的处理过程,首先 php 中双引号解析第一个参数内容,反斜线\
和其后跟的/
不组成转义字符,所以直接将/\/
传给正则解析器解析,pcre 正则表达式必须包含在一对分割符中,即第一个字符/
,所以该字符具有了特殊意义,而正则中也通过反斜线去转义字符,所以后面的表达式结束符/
被转义了,这样就没有表达式结束符而导致报错了。
再看一种写法。
这种情况下,php 解析第一个参数时,反斜线转义了反斜线,变成/\/
,到正则解析时还是和上面一样。
在正则中,\
的处理和 php 中有点不同。在 php 中,反斜线如果和紧跟字符不组成转义字符的话,反斜线没有转义功能,变成反斜线字符。而在正则中,反斜线如果和紧跟字符不组成转义字符或表示特殊含义时,就会有反斜线被去除一样的效果。
首先,\_abc
在 php 的双引号解析中,_
本身不需要转义,所以\
无转义功能表示为\
字符,送到正则解析的内容为\_abc
,正则中也没有转义字符\_
,这样\
没有起到转义效果,直接被去除,最后正则匹配到的内容为_abc
。
回到过滤 xss 的代码中。
1 | $a = preg_replace("/[^a-zA-Z0-9.\ -_]+/", "", $a); |
我猜这里本义应该是除了a-z
、A-Z
、0-9
、.
、\
、空格
、-
、_
外的字符全部替换为空,但实际效果却不是这样。
根据上面的思路,主要分析\ -_
这个部分,php 双引号解析完后变成\ -_
,因为空格本身就可以在引号中表示,所以\
没有转义效果时表示为原字符,到正则解析时,空格也可以正常表示,所以反斜线\
没有起到转义作用被去除,变成空格-_
,而在 pcre 正则表达式的方括号[]
中,-
是有特殊意义的,a-f
即从 a 对应的 ascii 码到 f 对应的 ascii 码范围的字符。因为这里的-
没有被转义,实际匹配的是空格对应的 ascii 码到下划线对应的 ascii 范围内的字符。
只有最右边中的字符才不在空格-_
的范围中。
要想达到原本的效果,只需要对\
和-
转义即可,为了方便,我去掉了[]
中的非^
字符。
转义过程如下。
1 | php: /[a-zA-Z0-9.\\ \-_]+/ |
总结
刚刚看到开发者回复我了,原来是漏了转义-
,看来我上面的修补方案写复杂了,可以看看简短的写法:https://github.com/pluck-cms/pluck/issues/77
开发还是要注意细节呀 : )