由定界符引发的一些安全问题

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,内容如下:

3

正常情况下,我们令定界符为:-,返回结果如下:

但如果我们设置定界符为:') 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 注入等等也算是定界符漏洞,以上是个人理解,如有别的技巧,欢迎讨论交流

0x05 参考

1 个赞