無字母數字webshell之提高篇
前幾天【程式碼審計知識星球】裡有同學提出了一個問題,大概程式碼如下:
<?php if(isset($_GET['code'])){ $code = $_GET['code']; if(strlen($code)>35){ die("Long."); } if(preg_match("/[A-Za-z0-9_$]+/",$code)){ die("NO."); } eval($code); }else{ highlight_file(__FILE__); }
這個程式碼如果要getshell,怎樣利用?
這題可能來自是我曾寫過的一篇文章:《 ION/webshell-without-alphanum.html" rel="nofollow,noindex" target="_blank">一些不包含數字和字母的webshell 》,裡面介紹瞭如何構造無字母數字的webshell。其中有兩個主要的思路:
- 利用位運算
- 利用自增運算子
當然,這道題多了兩個限制:
- webshell長度不超過35位
- 除了不包含字母數字,還不能包含
$
和_
難點呼之欲出了,我前面文章中給出的所有方法,都用到了PHP中的變數,需要對變數進行變形、異或、取反等操作,最後動態執行函式。但現在,因為 $
不能使用了,所以我們無法構造PHP中的變數。
所以,如何解決這個問題?
我們將上述程式碼放在index.php中,然後執行 docker run --rm -p 9090:80 -v `pwd`:/var/www/html php:7.2-apache
,啟動一個php 7.2的伺服器。
php7中修改了表示式執行的順序: http://php.net/manual/zh/migration70.incompatible.php :
PHP7前是不允許用 ($a)();
這樣的方法來執行動態函式的,但PHP7中增加了對此的支援。所以,我們可以通過 ('phpinfo')();
來執行函式,第一個括號中可以是任意PHP表示式。
所以很簡單了,構造一個可以生成 phpinfo
這個字串的PHP表示式即可。payload如下(不可見字元用url編碼表示):
(~%8F%97%8F%96%91%99%90)();
我們使用 docker run --rm -p 9090:80 -v `pwd`:/var/www/html php:5.6-apach
來執行一個php5.6的web環境。
此時,我們嘗試用PHP7的payload,將會得到一個錯誤:
原因就是php5並不支援這種表達方式。
在我在知識星球裡發出帖子的時候,其實還沒想到如何用PHP5解決問題,但我有自信解決它,所以先發了這個小挑戰。後來關上電腦仔細想想,發現當思路禁錮在一個點的時候,你將會鑽進牛角尖;當你用大局觀來看待問題,問題就迎刃而解。
當然,我覺得我的方法應該不是唯一的,不過一直沒人出來公佈答案,我就先拋鑽引玉了。
大部分語言都不會是單純的邏輯語言,一門全功能的語言必然需要和作業系統進行互動。作業系統裡包含的最重要的兩個功能就是“shell(系統命令)”和“檔案系統”,很多木馬與遠控其實也只實現了這兩個功能。
PHP自然也能夠和作業系統進行互動,“反引號”就是PHP中最簡單的執行shell的方法。那麼,在使用PHP無法解決問題的情況下,為何不考慮用“反引號”+“shell”的方式來getshell呢?
因為反引號不屬於“字母”、“數字”,所以我們可以執行系統命令,但問題來了:如何利用無字母、數字、 $
的系統命令來getshell?
好像問題又回到了原點:無字母、數字、 $
,在shell中仍然是一個難題。
此時我想到了兩個有趣的Linux shell知識點:
.
第一點曾在《 小密圈裡的那些奇技淫巧 》露出過一角,但我沒細講。 .
或者叫period,它的作用和source一樣,就是用當前的shell執行一個檔案中的命令。比如,當前執行的shell是bash,則 . file
的意思就是用bash執行file檔案中的命令。
用 . file
執行檔案,是不需要file有x許可權的。那麼,如果目標伺服器上有一個我們可控的檔案,那不就可以利用 .
來執行它了嗎?
這個檔案也很好得到,我們可以傳送一個上傳檔案的POST包,此時PHP會將我們上傳的檔案儲存在臨時資料夾下,預設的檔名是 /tmp/phpXXXXXX
,檔名最後6個字元是隨機的大小寫字母。
第二個難題接踵而至,執行 . /tmp/phpXXXXXX
,也是有字母的。此時就可以用到Linux下的glob萬用字元:
* ?
那麼, /tmp/phpXXXXXX
就可以表示為 /*/?????????
或 /???/?????????
。
但我們嘗試執行 . /???/?????????
,卻得到如下錯誤:
這是因為,能夠匹配上 /???/?????????
這個萬用字元的檔案有很多,我們可以列出來:
可見,我們要執行的 /tmp/phpcjggLC
排在倒數第二位。然而,在執行第一個匹配上的檔案(即 /bin/run-parts
)的時候就已經出現了錯誤,導致整個流程停止,根本不會執行到我們上傳的檔案。
思路又陷入了僵局,雖然方向沒錯。
大部分同學對於萬用字元,可能知道的都只有 *
和 ?
。但實際上,閱讀Linux的文件( http://man7.org/linux/man-pages/man7/glob.7.html ),可以學到更多有趣的知識點。
其中,glob支援用 [^x]
的方法來構造“這個位置不是字元x”。那麼,我們用這個姿勢幹掉 /bin/run-parts
:
排除了第4個字元是 -
的檔案,同樣我們可以排除包含 .
的檔案:
現在就剩最後三個檔案了。但我們要執行的檔案仍然排在最後,但我發現這三個檔名中都不包含特殊字元,那麼這個方法似乎行不通了。
繼續閱讀glob的幫助,我發現另一個有趣的用法:
就跟正則表示式類似,glob支援利用 [0-9]
來表示一個範圍。
我們再來看看之前列出可能干擾我們的檔案:
所有檔名都是小寫,只有PHP生成的臨時檔案包含大寫字母。那麼答案就呼之欲出了,我們只要找到一個可以表示“大寫字母”的glob萬用字元,就能精準找到我們要執行的檔案。
翻開ascii碼錶,可見大寫字母位於 @
與 [
之間:
那麼,我們可以利用 [@-[]
來表示大寫字母:
顯然這一招是管用的。
構造POC,執行任意命令
當然,php生成臨時檔名是隨機的,最後一個字元不一定是大寫字母,不過多嘗試幾次也就行了。
最後,我傳入的code為 ?><?=`. /???/????????[@-[]`;?>
,傳送資料包如下:
成功執行任意命令。