云端网 - www.ydw.org
前段时间 密集发布了一系列补丁,修复几个笔者之前储备的 0day,本文就来介绍其中一个列比较有意思的,以及分享一下相关的挖掘思路。
背景
最近几个月笔者都在研究 Java Web 方向,一方面是工作职责的调整,另一方面也想挑战一下新的领域。对于漏洞挖掘而言,选择一个具体目标是非常重要的,经过一段时间供应链和生态的学习以及同事建议,兼顾漏洞挖掘难度和实战效果选择了 OA作为第一个漏洞挖掘的目标。
代码审计
虽然本文介绍的是 ,但之前也说过很多次,自动化漏洞挖掘只能作为一种辅助手段,是基于自身对代码结构的理解基础上的提效方式。
在真正开始挖漏洞之前,笔者花了好几周的时间去熟悉目标的代码,并且对一些不清晰的动态调用去进行运行时分析,最终才能在 20G 代码之中梳理出大致的鉴权和路由流程。
通过分析 应用注册的路由,注意到其中一个映射:
ServletMapping[url-pattern=/services/*, name=XFireServlet] ^/services(?=/)|^/services\z]
其对应的类为 org..xfire..http.vlet:
XFireServlet
XFire Servlet
org.codehaus.xfire.transport.http.XFireConfigurableServlet
XFireServlet
/services/*
XFire 考古
XFire[1] 并不是 自己的业务代码,而是一个 SOAP Web 服务框架,它是作为 Axis 的有效替代方案而开发的。除了通过使用 StAX 实现良好性能的目标外,XFire 的目标还包括通过各种插件机制实现灵活性,API 的直观操作以及与通用标准的兼容性。此外 XFire 非常适合集成到基于 的项目中。
值得一提的是,XFire 目前已经不再进行开发,其官方继任者是 CXF[2]。
XFire 的用法比较简单,首先在 META-INF/xfire/.xml 中定义需要导出的服务,比如:
WorkflowService
webservices.services.weaver.com.cn
weaver.workflow.webservices.WorkflowService
weaver.workflow.webservices.WorkflowServiceImpl
org.codehaus.xfire.annotations.AnnotationServiceFactory
这样 ... 就被认为是导出服务。
可以直接被客户端进行调用。调用方式主要是通过 SOAP 请求到 ,例如调用上述服务可以发送 POST 请求到 //:
evilpan
2333
表示以指定参数调用服务的 方法。
SQL 注入
接下来回到漏洞本身, 服务的具体实现为 ,例如其中的 就是服务导出的一个方法,其具体实现为:
@Override
public String getUserId(String var1, String var2) {
if (Util.null2String(var2).equals("")) {
return "-1";
} else if (Util.null2String(var1).equals("")) {
return "-2";
} else {
RecordSet var3 = new RecordSet();
var3.executeQuery("select id from HrmResource where " + var1 + "=? and status<4 ", var2);
return !var3.next() ? "0" : Util.null2s(var3.getString("id"), "0");
}
}
可以看到,一个教科书式的 SQL 注入就已经找到了。
鉴权
现在漏洞点找到了,触发路径也找到了,可实际测试的时候发现这个接口有些特殊的鉴权,其鉴权逻辑为判断请求地址是否是内网地址,如果是的话就放行。
考虑到很多系统是集群部署的,且前面有一层或者多层负载均衡,因此实际请求服务的可能是经过反向代理的请求,此时客户端的真实 IP 只能通过 X--For 等头部获取。
这本来无可厚非,但是 HTTP 请求头是可以被攻击者任意设置的,因此 在此基础上进行了复杂的过滤,精简后的伪代码如下:
private String getRemoteAddrProxy() {
String ip = null;
String tmpIp = null;
tmpIp = this.getRealIp(this.request.getHeaders("RemoteIp"), ipList);
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = tmpIp;
}
boolean isInternalIp = IpUtils.internalIp(ip);
if (isInternalIp) {
ipList.add(this.request.getRemoteAddr());
tmpIp = IpUtils.getRealIp(ipList);
if (!IpUtils.internalIp(tmpIp)) {
ip = tmpIp;
}
}
return ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip) ? ip : null;
}
# 的判断更为复杂,连 byte[] 都出来了:
public static boolean internalIp(String ip) {
if (ip != null && !ip.equals("127.0.0.1") && !ip.equals("::1") && !ip.equals("0:0:0:0:0:0:0:1")) {
if (ip.indexOf(":") != -1 && ip.indexOf(":") == ip.lastIndexOf(":")) {
ip = ip.substring(0, ip.indexOf(":"));
}
byte[] addr = (byte[])null;
if (isIpV4(ip)) {
addr = textToNumericFormatV4(ip.trim());
} else {
addr = textToNumericFormatV6(ip.trim());
}
return addr == null ? false : internalIp(addr);
} else {
return true;
}
}
public static boolean internalIp(byte[] addr) {
byte b0 = addr[0];
byte b1 = addr[1];
byte SECTION_1 = true;
byte SECTION_2 = true;
byte SECTION_3 = true;
byte SECTION_4 = true;
byte SECTION_5 = true;
byte SECTION_6 = true;
switch (b0) {
case -84:
if (b1 >= 16 && b1 <= 31) {
return true;
}
case -64:
switch (b1) {
case -88:
return true;
}
default:
return false;
case 10:
return true;
}
}
其逻辑是对 取出来的 IP,如果路径匹配 且 IP 匹配 前缀,就认为是内网地址的请求从而进行放过:
webserviceList = [
"/online/syncOnlineData.jsp",
"/services/",
"/system/MobileLicenseOperation.jsp",
"/system/PluginLicenseOperation.jsp",
"/system/InPluginLicense.jsp",
"/system/InMobileLicense.jsp"
];
webserviceIpList = [
"localhost",
"127.0.0.1",
"192.168.",
"10.",
"172.16.",
"172.17.",
"172.18.",
"172.19.",
"172.20.",
"172.21.",
"172.22.",
"172.23.",
"172.24.",
"172.25.",
"172.26.",
"172.27.",
"172.28.",
"172.29.",
"172.30.",
"172.31."
]
根据上面的代码,你能发现鉴权绕过的漏洞吗?
也许对代码比较敏感的审计人员可以通过上述鉴权代码很快发现问题,但说实话我一开始并没有找到漏洞。于是我想到这个鉴权逻辑是否能单独抽离出来使用 的思路去进行自动化测试。
之前发现 Java 也有一个基于 的模糊测试框架 [3],但是试用之后发现比较鸡肋,因为和二进制程序会自动 Crash 不同,Java 的 fuzz 需要自己指定 Sink,令其在触达的时候抛出异常来构造崩溃。
虽然说没法发现通用的漏洞,但是对于现在这个场景来说正好是绝配,我们可以将目标原始的鉴权代码抠出来,然后在未授权通过的时候抛出一个异常即可。构建的 Test 代码如下:
public static void fuzzerTestOneInput(FuzzedDataProvider data) {
String poc = data.consumeRemainingAsString();
fuzzIP(poc);
}
public static void fuzzIP(String poc) {
if (containsNonPrintable(poc)) return;
XssRequestWeblogic x = new XssRequestWeblogic();
String out = x.getRemoteAddr(poc);
boolean check2 = check2(out);
if (check2) {
throw new FuzzerSecurityIssueHigh("Found IP [" + poc + "]");
}
}
public static boolean check2(String ipstr) {
for (String ip: webserviceIpList) {
if (ipstr.startsWith(ip)) {
return true;
}
}
return false;
}
其中精简了一些 代码中读取配置相关的依赖,将无关的逻辑进行手动剔除。
编译好代码后,使用以下命令开始进行 fuzz:
$ ./jazzer --cp=target/Test-1.0-SNAPSHOT.jar --target_class=org.example.App
不多一会儿,就已经有了一个成功的结果!
fuzz.png
可以看到图中给出了 127.0.0.10 这个 ,可以触发 IP 鉴权的绕过!反过来分析这个 PoC,可以发现之所以能绕过是因为 只检查了前缀,而 127.0.0.10 可以在 返回 False,即认为不是内部 IP,但实际上 却认为是内部 IP,从而导致了绕过。
如果只是从代码上去分析的话,可能一时半会并不一定能发现这个问题,可是通过 在覆盖率反馈的加持下,却可以在几秒钟之内找到正解,这也是人工审计无法比拟的。
漏洞补丁
通过 IP 的鉴权绕过和 XFire 组件的 SQL 注入,笔者实现了多套前台的攻击路径,并且在 HW 中成功打入多个目标。因为当时提交的报告中带了漏洞细节,因此这个漏洞自然也就被官方修补了。如果没有公开的话这个洞短期也不太会被撞到。
漏洞修复的关键补丁如下:
diff --git a/src/weaver/security/webcontainer/IpUtils.java b/src/weaver/security/webcontainer/IpUtils.java
index 6b3d8efc..e7482511 100644
--- a/src/weaver/security/webcontainer/IpUtils.java
+++ b/src/weaver/security/webcontainer/IpUtils.java
@@ -48,12 +48,16 @@ public class IpUtils {
}
public static boolean internalIp(String ip) {
- if (ip != null && !ip.equals("127.0.0.1") && !ip.equals("::1") && !ip.equals("0:0:0:0:0:0:0:1")) {
+ if (ip == null || ip.equals("127.0.0.1") || ip.equals("::1") || ip.equals("0:0:0:0:0:0:0:1")) {
+ return true;
+ } else if (ip.startsWith("127.0.0.")) {
+ return true;
+ } else {
if (ip.indexOf(":") != -1 && ip.indexOf(":") == ip.lastIndexOf(":")) {
ip = ip.substring(0, ip.indexOf(":"));
}
其中把 换成了 ,并且还过滤了我们之前使用的 组件。当然还是沿袭 一贯的漏洞修复原则,不改业务代码,只增加安全校验,这也是对历史遗留问题的一种妥协吧。
总结引用链接
[1] XFire: ()
[2] CXF:
[3] :
from
www.ydw.org - 云端网