作者:天融信阿爾法實驗室
公眾號:https://mp.weixin.qq.com/s/NRNnhmtOVuIKxfKar5ydXw

一、起因

在復現分析Wordpress-5.0.0 RCE 的時候,因為在寫圖片的過程中,根據圖片的dirname創建目錄,而后根據basename寫入圖片。在目錄創建成功的前提下,應該是可以寫入文件的。但是情況卻不是如此,過程中我要在寫目標圖片前,必須還要再寫一個輔助圖片。其實這個輔助圖片不是很重要,而重要的是這個輔助圖片的目錄創建。

過程中例如需要寫入目標文件為:

首先需要先寫一張

為什么會這樣?假設直接寫目標文件,過程中會首先創建目錄:

其實這個過程是沒有創建任何目錄的,因為判斷是directory already ,到下一步寫入圖片這里是Imagick::writeImage,在這里就會出問題。invaild file path.報錯。因為這里不存在 /var/www/html/wordpress/wp-content/uploads/2019/03/1.jpg?這個目錄,這涉及到系統調用,因系統的不同相對于的系統處理函數處理的方式也不同。

例如在kali 下 Imagick::writeImage寫入 ./1?/../1.png , ./1? 這個目錄是會報錯的。具體系統調用如下

首先判斷了這個文件的狀態,而后調用openat 打開這個文件并不存在。AT_FDCWD表示打開的文件位置相對于當前目錄。這是我在做的時候遇到的情況。

但是在 WORDPRESS IMAGE 遠程代碼執行漏洞分析一文中,甚至其他另一篇。都沒提到兩次寫圖片。難道因為window和linux的不同嗎?就這個問題我進行了一次對mkdir的探究。發現其實有很有趣。

二、PHP內核 && 系統差異 之mkdir()

2.1 Linux && PHP 7.3.2-3

mkdir(‘./1?/../1′,777,true);

mkdir(‘./1?/../1′,777,false);

當第三參數為$recursivetrue 時可以寫目錄,先說一下這個參數的含義$recursive用來循環創建目錄。什么意思呢,當false時只能創建1級目錄,即目錄連接符最后的一個目錄。而當true時是可以創建多級目錄至到最后一個目錄。列如./a/b/c當abc都不存在時,會通過系統函數mkdir循環創建目錄,abc都會被創建,但若為false會因為走到a處目錄不存在,則不回去創建最后一個c。

但是第一個mkdir即使為true卻也沒有創建1?目錄 ,這里我們從php內部mkdir執行情況 和 系統 mkdir 執行情況來探究。

2.1.1 PHP_FUNTCION(mkdir)

PHP內調過程如下圖:

我們在出現分支的地方細分/php-src/main/streams/plain_wrapper.c

2.1

2.1.1.1 $recursive = fasle

其中出現的分支的地方在判斷$recursive 若是不需要循環創建則直接進入php_mkdir

/php-src/ext/standard/file.c

跟進php_mkdir_ex

首先會檢查open_basedir,接著會進入VCWD_MKDIR,VCWD_MKDIR是個宏命令,有三種不同定義:

在這里我剛開始并沒有考慮太多,跟著gdb的流程走,直接執行mkdir(),會直接調用系統的_mkdir().mkdir(“./1?/../1″, 01411) = -1 ENOENT (No such file or directory)

會直接報錯。在預料之類,linux系統下mkdir是不允許這樣創建目錄的,會效驗每一層目錄的有效性。回到第一次出現分叉的時候。

2.1.1.2 $recursive = true

這里會進入expand_filepath_with_mode,這里其實很熟悉,之前也是在看路徑處理的時候看到過這個函數,它是一個展開函數,會通過遞歸的方式展開需要被創建的目錄。在其過程會先把相對目錄和當前腳本執行目錄評價起來,若是絕對目錄則忽略.

其中我們的相對目錄為 ./1?/../1會變成/var/www/html/WordPress/wp-content/themes/4/5/6/./1?/../1當前我所在的目錄為/var/www/html/WordPress/wp-content/themes/4/5/6然后通過遞歸的方式 去掉 ../, ./ ,//.并且對應目錄前移,會變成/var/www/html/WordPress/wp-content/themes/4/5/6/1然后在傳遞給系統的mkdir函數。在這個函數里面存在win32 和 linux的不同分支,但在具體處理之前win32判斷了目錄名不能存在 *

注意一下此處!

附上strace ,也是驗證上訴分析過程:mkdir(“/var/www/html/WordPress/wp-content/themes/4/5/6/1″, 01411) = 0

2.1.2 MKDIR IN LINUX

在linux中單純的mkdir是會層層驗證目錄,而后在創建一級目錄。mkdir 也可以帶參 -p,代表系統層面循環的創建目錄。

當執行mkdir -p 時 :

  1. strace -f -e trace=mkdir mkdir -p ./1?/../1
  2. mkdir(“1?”, 0777) = 0
  3. mkdir(“1″, 0777) = 0

我們能看到它并不像php內部那樣,展開而后處理 。它會層層按照輸入的目錄創建。

2.2 window && PHP 7.0.12

這里是我為什么要探究的一個重要問題點所在,在前面我提到的那篇文章中作者在window下實驗當$recursivefalse才能創建成功,正好是反著的。作者的解釋的false的時候不會去層層判斷,但是真的是這樣嗎?

而后我也做了一個驗證性的實驗,在window 上用 php 5.6做了這個測試,但是結果讓我疑惑了,無論在false還是 true的情況都不會創建目錄.而且報錯也很有意思,在false的情況下報錯 no error 但是就是無法創建。在true的情況下報錯 invaild path

難道是php-cli 問題?我又用cgi測了一遍,發現同樣是這樣。有意思,而后我通過郵件聯系了那篇文章作者,詢問其版本號。很快,得到了他的答復,php-7.0.12

于是下載php-7.0.12源碼 重新編譯加debug,此處省略1000字…

在編譯完成后我迫不及待的試了一下,同樣如此和我的php5.6 一摸一樣,無論在cli 模式 或者 cgi 模式下都是無法復現作者文中的情況。這到底問題出在哪呢?

先調了再說,VS調試php 網上基本上沒有詳細的介紹,有的都是Vscode。我不知道如何啟動并調試,只好想了個attach的辦法。在mkdir前面寫上sleep(10),但是這樣做,其實是有一點雞肋的,php內核初始化過程你其實抓不到的,但是用在這里夠了,還是在php_plain_files_mkdir這個地方下斷,刷新頁面,attach到啟動的php-cgi 上。

2.2.1 PHP_FUNCTION(MKDIR)

2.2.1.1 $recursive == false

還是先分析false的情況,前面都一樣,不同的是在php_mkdir_exVCWD_MKDIR調用的函數不一樣

這次走到不一樣的調用上

跟進virtual_mkdir

同樣調用了virtual_file_ex(),前面有一點沒提到,在expand展開路徑的過程中最后其實也是進入的這個函數,前面說過在處理的過程中若是win32的情況會判斷路徑存不存在 *, ?.若是存在則會直接返回1,不會進入后面寫路徑。為什么那篇文章的作者會在false的情況下寫成功呢?

2.2.1.1 $recursive == true

這里前面說過這里會進行expand過程,但是同樣會判斷路徑名中存不存在*, ?,會報錯 Invaild Path。

2.2.2 MKDIR IN WINDOW

這里因為沒有都沒有執行到寫目錄。此處我們還無法探究window系統mkdir 函數是如何執行的。

三、線程安全與非線程安全

重新梳理一下,現在是三種不一樣的情況:

linux /true 可寫

window/7.0.12 :

  1. false 可寫
  2. true/false 都不可寫

window 出現了兩種情況。仔細在走一遍window/false的情況,現在我唯一沒有考慮到是VCWD_MKDIR 選擇情況。前面都是跟著調試流程走的,這是唯一可能出現分叉的地方,重新看一下它的兩種種宏定義:

若非那片文章作者,是走的第二個define,于是我把第一個define先注釋掉了,換上了第二個define,再重新編譯一邊,結果竟然出現了和那篇作者一樣的情況。但是這里有一個小小不同,寫入的目錄是相對于php-cgi.exe解釋器的,不是相對于WWW的網站根目錄下的,當你看了下面的分析以后,應該會給你一個答案,那么很顯然問題現在出現在 VIRTUAL_DIR 定義的情況,在它沒有定義的情況下,才會走到第二個define,我看看VIRTUAL_DIR是在哪被定義的/php-src/Zend/zend_virtual_cwd.h

熟悉php內核的朋友不會陌生ZTS,這是php 線程安全的標志。用來應對那些使用線程來處理并發請求的Web服務器,列如window下的IIS,worker_mpm模式下的apahce,生活在線程里面的php需要考慮線程間的讀寫同時也要保證線程間是安全,所以php需要自己提供ZTS層來管理線程間的操作。當定義了ZTS時候,就也同時定義了虛擬目錄(VIRTUAL_DIR)。

為什么會存在虛擬目錄這一說法呢,其實很簡單你通過對應virtual_file_ex()可以看出來,這個函數的目的在于針對相對路徑替換出完整的絕對路徑。舉很簡單的例子,php腳本中寫的相對路徑,其相對路徑一定是針對于該腳本的。在執行腳本的過程中,會進入相應的php 內核里面的php_execute_script(),其中有一步是VCWD_CHDIR_FILE(filename),這是用來根據要執行的腳本位置去切換當前目錄,同樣這個宏定義有兩個不同的函數,一個是在虛擬目錄下切換目錄,一個是非線程安全環境下單線程切換目錄,不同是在線程安全下切換目錄,并不是直接調用系統的_chdir(),而是將執行腳本的目錄存儲在TSRMG中,并給定一個cwd_globals_id,要用的時候再去取,比如創建目錄,寫文件。因為在多線程環境不能直接修改當前進程的目錄,只能預定義一個變量保存各線程的當前目錄。

可以看到在線程安全的模式下,若是給的相對路徑,都會出現當前目錄和相對目錄的拼接。且都在win32的環境都會檢測目錄是否包含* ,?.

四、結論匯總

我有注意到那篇的文章作者是在window 上用的phpstudy,我也去看了一下phpstudy的是否有7.0.12的版本,存在一個 php-7.0.12-nts+Apache 確實也是非線程安全。也印證上面我修改php 7.0.12 重新編譯的結果,但是一個很有趣的東西是,window的系統調用API _mkdir() 是存在和php內部一樣的路徑展開功能,即他是允許這樣寫的./1?/../1 可以在當前目錄下寫入文件夾1的,這和linux不一樣,linux的系統函數是逐層判斷。在php7.1之后,改變了系統創建目錄的API,從_mkdir 變成了CreateDirectoryW,但是不變的是還是可以存在路徑展開的功能。即便你這樣寫:@@#@$@#$^%$&&**/@!#@!$!%/../../evil也是可以創建目錄evil的,可以算是一個小技巧。

但是條件是在windowphp非線程安全模式和**PHP_FUNCTION(mkdir)**第三個參數為false的情況下是可以這樣寫目錄的。可以算是一個小tips吧。結合相應的應用特點,是可以用到的,而且php版本一般都是非線程安全的,在nginx下都是多進程處理php,即非線程安全。apache只有在worker_mpm才是多線程的,一般也不常用。一般都是prefork_mpm + php_mod,即便是fastcgi也是多進程。利用環境還是比較常見的。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/863/