一个Python正则绕过技巧

  • Comments Off on 一个Python正则绕过技巧
  • 15 views
  • A+

译文来源: https://www.secjuice.com/python-re-match-bypass-technique/。受个人了解知识与个人偏见所限,部分内容可能存在过分曲解或误解,望各位包含并提出建议,十分感谢。

检查用户输入最常见的方法之一就是对照正则表达式进行测试。Python的RE模块提供了简单而又非常强大的函数来检查一个特定的字符串与一个给定的正则表达式是否相匹配(或者给定的正则表达式是否与特定字符串相匹配,总之说的是一个意思)。但有些时候,Python RE中包含的函数要么被误用,要么就是没有被开发人员很好地理解的情况,当你发现存在类似情况时,就有可能绕过这些脆弱的输入验证函数。

摘要:使用pythonre.match()函数来验证用户的输入时可能会导致绕过,因为它只对目标字符串的开头进行匹配,而不是对每一行的开头进行匹配。因此,可以通过将一个payload转换为多行形式,这样第二行的内容将会被该函数忽略。这就意味着如果存在这样一个防止在参数值中使用特殊字符的脆弱的验证函数(比方说验证参数为id=123),那么就可以将其转换为id=123\n'+OR+1=1--进行绕过。

在本文中,我将会向各位演示几种re.match()函数的错误用法。[search()与match()] Python提供了这两种基于正则表达式的不同的原始操作方法:re.match()只对字符串的开头进行匹配检查,而re.search()则是对整个字符串的内容进行匹配检查(这也是在Perl中的默认做法)。

举例说明:

```python

re.match("c", "abcdef") # 不匹配
re.search("c", "abcdef") # 匹配

```

'^'开头的正则表达式可以与search()搭配使用,以限制指定对字符串的开头进行匹配:

```python

re.match("c", "abcdef") # 不匹配
re.search("^c", "abcdef") # 不匹配
re.search("^a", "abcdef") # 匹配

```

正如大家看到的那样,第一条re.match语句并没有匹配成功,因为它存在一个隐形的锚点,只匹配字符串的开头。而这一锚点并没有匹配到任何字符。其实,它们匹配的是字符之前、之后或之间的一个位置。它们可以用来在正则匹配中“锚定”准确位置的内容(https://www.regular-expressions.info/anchors.html)。

使用re.match()的输入验证

假设我这里有一个存在SQL注入漏洞的Python flask web应用。如果我通过参数id传递文章id编号,通过category传递种类名称,向/news发送一个HTTP请求,它就会返回给我关于该文章的内容。举例说明:

```python
from flask import Flask
from flask import request
import re

app = Flask(name)

def is_valid_input(input):
m = re.match(r'.(["\';=]|select|union|from|where).', input, re.IGNORECASE)
if m is not None:
return False
return True

@app.route('/news', methods=['GET', 'POST'])
def news():
if request.method == 'POST':
if "id" in request.form:
if "category" in request.form:
if is_valid_input(request.form["id"]) and is_valid_input(request.form["category"]):
return f"OK: {request.form['category']}/{request.form['id']}"
else:
return f"Invalid value: {request.form['category']}/{request.form['id']}", 403
else:
return "No category parameter sent."
else:
return "No id parameter sent."
```

wKg0C2DR1mAGDI6AAB4cTQAbA759.png

通过发送带有参数id=123category=financial的请求,应用以“200 OK”状态码回复响应并且响应主体为”OK: financial/123“。正如我之前所说,id参数处存在SQL注入漏洞,所以开发者需要通过针对所有参数(id和category)创建一个用户输入验证函数来阻止用户发送像是单双引号或是类似于”select“”union“这样的字符串。

正如你所看见的那样,该web应用通过第7行的is_valid_input函数对用户输入进行验证:

python
def is_valid_input(input):
m = re.match(r'.*(["\';=]|select|union|from|where).*', input, re.IGNORECASE)
if m is not None:
return False
return True

上述代码含义为:”如果任何输入内容中的值包含双引号、单引号、分好、等于号或者包含’select‘、’union‘、’from‘、’where‘中的任意一个的话,该请求就会被丢弃“。我们来试试看:

wKg0C2DSxGAAgyBAACCwlxW9yc244.png

通过向id参数的值中中插入SQL语句,web应用返回了403 Forbidden,并且响应体内容为“Invalid value”。这主要是由于验证函数匹配到了payload中像是单引号和等于号这样的无效字符。

输入验证绕过

从RE模块的官方文档中可以得知,关于re.match()函数:“即便是在MULTILINE(多行)模式下,re.match()函数仍然只会匹配字符串的开头部分而并非每一行的开头部分。如果你想要对字符串进行全匹配的话,还是用search()吧(详细信息请参照search()与match()

所以说,要想绕过这一类型的输入验证的话,我们只需要通过在数值与SQL语句之间添加一个\n,将SQL注入payload从单行转换为多行就可以了。举例说明:

wKg0C2DUAAbvehAABgbCmm4g907.png

如果你的问题是“SQL在SELECT语句中允许换行吗?”,答案是肯定的。假设SQL语句变成了以下内容执行效果是一样的:

wKg0C2DUaSAa0oKAAAyrFv3qHk491.png

让我们再在存在漏洞的web应用上试一下:

wKg0C2DUhqAWkeqAACONqg83I998.png

正如截图所示,我只是在id数值后插入了一个\n(并不是CRLF字符的\r\n),然后开始进行SQL注入。由于验证函数只对第一行进行了验证,所以我成功实现了绕过。

利用curl的话是这样的:

sh
curl -s -d "id=123%0a'+OR+1=1--&category=test" 'http://localhost:5000/news'
OK: test/123%

自己试一下

首先,从这里下载存在漏洞的flask web应用源码:https://gist.github.com/theMiddleBlue/bd06e56aac9885266fb916200514d534#file-app-py

```python
from flask import Flask
from flask import request
import re

app = Flask(name)

def is_valid_input(input):
m = re.match(r'.(["\';=]|select|union|from|where).', input, re.IGNORECASE)
if m is not None:
return False
return True

@app.route('/news', methods=['GET', 'POST'])
def news():
if request.method == 'POST':
if "id" in request.form:
if "category" in request.form:
if is_valid_input(request.form["id"]) and is_valid_input(request.form["category"]):
return f"OK: {request.form['category']}/{request.form['id']}"
else:
return f"Invalid value: {request.form['category']}/{request.form['id']}", 403
else:
return "No category parameter sent."
else:
return "No id parameter sent."
```

然后使用该命令启动flask web服务器:flask run

修复措施

第一个选择就是要做积极的验证而并非是消极的验证(进行白名单验证而不是黑名单验证)。不要创建一个“禁用字符”或是“禁用单词”的禁止列表,而是要检查预定值的格式。比方说id=123就可以通过^[0-9]+$的匹配验证。

第二个措施就是使用re.search()而不是re.match()对整个值的内容进行检查而不是只检查第一行。

第三个就是不要选择自己创建一个输入验证的函数,最好是根据你自身需求找一个被广泛应用和维护的库使用。