0x01 前言
前一段时间在p 师傅的小密圈中看到了他分享的定界符安全以及一则 Django 的安全修复公告,趁着有时间,对定界符相关的安全问题学习了一波。
0x02 定界符
定界符从其字面意义上来说就是限制界限的符号,假设我们设置定界符为//
,那么//panda//
的意思就是告诉计算机,从第一个//
开始,到panda
字符串,再到后一个//
结束。定界符在很多语言中都有不同的形式,有的时候定界符可以是分隔符、也有可能是注释符。如在 php 中使用<<<
作为定界符;在MySQL 中默认语句定界符为;
,在 python 中,定界符如下表:
( | ) | [ | ] | { | } |
---|---|---|---|---|---|
, | : | . | ` | = | ; |
+= | -= | *= | /= | //= | %= |
&= | |= | ^= | >>= | <<= | **= |
可见定界符的运用还是比较广泛的,但也正是如此,造成了相关的安全漏洞。
0x03 定界符引起相关安全问题的实例
1、绕过验证
假设有一段PHP代码如下:
SELECT * FROM Users WHERE ((Username ='$username') AND (Password=MD5('$password')))
通过这行代码我们很容易看出这是用来登陆判断的代码,但是如果这里没有对输入数据进行特殊字符检测,特别是定界符的检测,那么就可能导致绕过登陆验证。在上述实例中,我们令:
username = 1' or '1' = '1')) /*
password = test
因此传入到该 SQL语句 中应该是:
SELECT * FROM Users WHERE ((Username='1' or '1' = '1'))/*') AND (Password=MD5('$password')))
这样一来让语句始终保证为真,即可绕过登陆验证。
2、提升权限
可以参考漏洞:CVE-2003-1350 ,如果程序对于用户注册信息过滤不严格,并且可以使用该程序的定界数据库的符号,那么就可能导致权限的提升。在这个漏洞中,List Site Pro使用了 |
来定界数据库,并且没有对输入数据进行定界符检查,因此用户输入相关数据后,就可以修改任意账户的密码。
非独有偶。在Poster V2中也出现过类似漏洞,Poster V2有个文件叫mem.php
,专门用于存储用户字段(用户名,密码,电子邮件地址和特权),负责管理应用程序用户,如下是该文件的示例:
<? panda|12345678|[email protected]|admin| test|5211314|[email protected]|normal| ?>
panda 是管理员,test 为普通用户,当用户编辑其个人资料时,使用 index.php
页面中的“编辑帐户”选项并输入其登录信息即可。
从上述文件示例中我们可以知道该文件使用的定界符是|
,如果当我们编辑资料的时候,没有对编辑后的内容进行过滤,那么就可以通过编辑后的内容将其特权提升为管理员。如下:
Username: test Password: 5211314 Email: [email protected] |admin|
那么当该信息存储到mem.php
文件中时,就会变成:
test|5211314|[email protected]|admin|normal|
从而达到提升权限的目的。当然,这种将用户字段信息存储在文件中的程序基本上没了,但是这种思路还是可以借鉴。
3、SQL 注入
最典型的例子就是 Django 的 SQL 注入了(CVE-2020-7471),2020年2月3日Django 发布安全公告说django.contrib.postgres.aggregates.StringAgg
聚合函数存在漏洞,只要设计好定界符,那么就能进行SQL注入,找到该函数 :
class StringAgg(OrderableAggMixin, Aggregate):
function = 'STRING_AGG'
template = "%(function)s(%(distinct)s%(expressions)s, '%(delimiter)s'%(ordering)s)"
allow_distinct = True
def __init__(self, expression, delimiter, **extra):
super().__init__(expression, delimiter=delimiter, **extra)
def convert_value(self, value, expression, connection):
if not value:
return ''
return value
相关文档的对该函数的解释:
class StringAgg(expression, delimiter)
Returns the input values concatenated into a string, separated by the delimiter string.
返回连接到字符串中的输入值,该字符串由定界符字符串分隔。
delimiter
Required argument. Needs to be a string.
必填参数,且是一个字符串。
简单来说该函数就是由用户输入一个定界符,然后将查询出或者输入的值使用我们自定义的那个定界符连接起来。
比如我设置定界符为-
,数据表如下:
uid | username | private |
---|---|---|
1 | panda | admin |
2 | test | normal |
3 | hello | normal |
4 | world | normal |
SELECT "vul_app_info"."gender", STRING_AGG("vul_app_info"."name", '-') AS "mydefinedname" FROM "djsqltest_info" GROUP BY "djsqltest_info"."gender" LIMIT 1 OFFSET 1 --
若以 private 列查询,并将 username 列聚合,结果在 django 中显示为:
{'private':'admin','username':'panda'}
{'private':'normal','username':'test-hello-world'}
根据官方的说法,存在特定的定界符能够导致注入,经过 fuzzing 可以确定为单引号,当我们设置定界符为单引号时,会出现错误:
查询语句如下:
SELECT "test_sql_userinfo"."private", STRING_AGG("test_sql_userinfo"."username", \'\'\') AS "username" FROM "test_sql_userinfo" GROUP BY "test_sql_userinfo"."private"
可以看到我们传入的定界符被转义成了\'
,该段字符串传入到 postgres中为:
SELECT "test_sql_userinfo"."private", STRING_AGG("test_sql_userinfo"."username", ''') AS "username" FROM "test_sql_userinfo" GROUP BY "test_sql_userinfo"."private"
由于三个单引号的出现,导致 sql 语法出错,并且我们可以知道,我们设置的定界符是传入到了 SQL 语句中的,那么只要设置好定界符,就可能导致注入。
下面就可以演示此漏洞:
我们定义一个数据库为 django_sql
有表test_sql_userinfo
,内容如下:
有表sql_admin
,内容如下:
正常情况下,我们令定界符为:-
,返回结果如下:
但如果我们设置定界符为:') AS "uname" FROM "test_sql_admin" group by "test_sql_admin"."id"--
返回结果如下:
成功注入出其他数据
这种注入虽然可控的概率很小,但是依旧是个 SQ L 注入漏洞,而且是一个比较经典的由于定界符问题引起的注入
这个实际案例(CVE-2008-5185)是由于定界符的问题没有关闭标签,导致无限循环,形成了拒绝服务攻击。看核心代码:
function parse_code () {
...
$code = str_replace("\r\n", "\n", $this->source);
$code = str_replace("\r", "\n", $code);
// Add spaces for regular expression matching and line numbers
$code = "\n" . $code . "\n";
...
if ($this->strict_mode) {
// Break the source into bits. Each bit will be a portion of the code
// within script delimiters - for example, HTML between < and >
$parts = array(0 => array(0 => '', 1 => ''));
$k = 0;
for ($i = 0; $i < $length; ++$i) {
foreach ($this->language_data['SCRIPT_DELIMITERS'] as $delimiters) {
foreach ($delimiters as $open => $close) {
// Get the next little bit for this opening string
$open_strlen = strlen($open);
$check = substr($code, $i, $open_strlen);
// If it matches...
if ($check == $open) {
// We start a new block with the highlightable
// code in it
++$k;
$parts[$k][0] = $open;
$close_i = strpos($code, $close, $i + $open_strlen) + strlen($close);
if ($close_i === false) {
$close_i = $length - 1;
}
$parts[$k][1] = substr($code, $i, $close_i - $i);
$i = $close_i - 1;
++$k;
$parts[$k][0] = '';
$parts[$k][1] = '';
// No point going around again...
continue 3;
}
}
}
// only non-highlightable text reaches this point
$parts[$k][1] .= $code[$i];
}
}
...
主要从循环开始看,$this->language_data['SCRIPT_DELIMITERS']
是一个定义开始符(如:<
)和结束符(如:>
)的数组,然后把这些符号分配给$open
和$close
,然后取代码块第一个字符,查找是否有开始符,如果有,进入判断,看这一句:
$close_i = strpos($code, $close, $i + $open_strlen) + strlen($close);
$close
为结束符,如果在代码块中没有查到结束符,那么最终$close_i
的值为 1,传到下方的: $i = $close_i - 1;
时候,$i
的值为 0,循环再次从头开始,形成无限循环,导致拒绝服务。
0x04 总结
其实除了上述实例外,定界符还可能导致代码执行漏洞(CVE-2007-5178),但是由于时间比较久远,找不到源码,故不分析了。另外,非严格意义上比如说 DOM 型的 XSS、CRLF 注入等等也算是定界符漏洞,以上是个人理解,如有别的技巧,欢迎讨论交流