Fuzzing 在 Java 漏洞挖掘中的应用

前段时间泛微发布了 10.58.6 补丁,修复好几个笔者之前储备的 0day,本文就来介绍其中一系列比较有意思的漏洞,以及分享一下相关的挖掘思路。

meme

背景

最近几个月笔者都在研究 Java Web 方向,一方面是工作职责的调整,另一方面也想挑战一下新的领域。对于漏洞挖掘而言,选择一个具体目标是非常重要的,经过一段时间供应链和生态的学习以及同事建议,兼顾漏洞挖掘难度和实战效果选择了泛微OA作为第一个漏洞挖掘的目标。

代码审计

虽然本文介绍的是 Fuzzing,但之前也说过很多次,自动化漏洞挖掘只能作为一种辅助手段,是基于自身对代码结构的理解基础上的提效方式。

在真正开始挖漏洞之前,笔者花了好几周的时间去熟悉目标的代码,并且对一些不清晰的动态调用去进行运行时分析,最终才能在 20G 代码之中梳理出大致的鉴权和路由流程。

draft.png

本文介绍的就是其中红框中的漏洞

通过分析 JavaEE 应用注册的路由,注意到其中一个映射:

ServletMapping[url-pattern=/services/*, name=XFireServlet] ^/services(?=/)|^/services\z]

其对应的类为 org.codehaus.xfire.transport.http.XFireConfigurableServlet:

<servlet>
    <servlet-name>XFireServlet</servlet-name>
    <display-name>XFire Servlet</display-name>
    <servlet-class>org.codehaus.xfire.transport.http.XFireConfigurableServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>XFireServlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
</servlet-mapping>

XFire 考古

XFire 并不是泛微自己的业务代码,而是一个 SOAP Web 服务框架,它是作为 Apache Axis 的有效替代方案而开发的。除了通过使用 StAX 实现良好性能的目标外,XFire 的目标还包括通过各种插件机制实现灵活性,API 的直观操作以及与通用标准的兼容性。此外 XFire 非常适合集成到基于 Spring Framework 的项目中。

值得一提的是,XFire 目前已经不再进行开发,其官方继任者是 Apache CXF

XFire 的用法比较简单,首先在 META-INF/xfire/services.xml 中定义需要导出的服务,比如:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://xfire.codehaus.org/config/1.0">
    <service>
        <name>WorkflowService</name>
        <namespace>webservices.services.weaver.com.cn</namespace>
        <serviceClass>weaver.workflow.webservices.WorkflowService</serviceClass>
        <implementationClass>weaver.workflow.webservices.WorkflowServiceImpl</implementationClass>
        <serviceFactory>org.codehaus.xfire.annotations.AnnotationServiceFactory</serviceFactory>
    </service>
</beans>

这样 weaver.workflow.webservices.WorkflowService 就被认为是导出服务。

可以直接被客户端进行调用。调用方式主要是通过 SOAP 请求到 XFireServlet,例如调用上述服务可以发送 POST 请求到 /services/WorkflowService:

<?xml version="1.0" encoding="UTF-8"?>
  <Body>
    <getUserId>
<p>evilpan</p>
<p>2333</p>
    </getUserId>
  </Body>

表示以指定参数调用服务的 getUserId 方法。

SQL 注入

接下来回到漏洞本身,WorkflowService 服务的具体实现为 WorkflowServiceImpl,例如其中的 getUserId 就是服务导出的一个方法,其具体实现为:

@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 注入就已经找到了。

Service 鉴权

现在漏洞点找到了,触发路径也找到了,可实际测试的时候发现这个接口有些特殊的鉴权,其鉴权逻辑为判断请求地址是否是内网地址,如果是的话就放行。

考虑到很多系统是集群部署的,且前面有一层或者多层负载均衡,因此实际请求服务的可能是经过反向代理的请求,此时客户端的真实 IP 只能通过 X-Forward-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;
}

IpUtils#internalIp 的判断更为复杂,连 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;
  }
}

其逻辑是对 getRemoteAddrProxy 取出来的 IP,如果路径匹配 webserviceList 且 IP 匹配 webserviceIpList 前缀,就认为是内网地址的请求从而进行放过:

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."
]

根据上面的代码,你能发现鉴权绕过的漏洞吗?

Fuzzing

也许对代码比较敏感的审计人员可以通过上述鉴权代码很快发现问题,但说实话我一开始并没有找到漏洞。于是我想到这个鉴权逻辑是否能单独抽离出来使用 Fuzzing 的思路去进行自动化测试。

之前发现 Java 也有一个基于 libFuzzer 的模糊测试框架 Jazzer,但是试用之后发现比较鸡肋,因为和二进制程序会自动 Crash 不同,Java 的 fuzz 需要自己指定 Sink,令其在触达的时候抛出异常来构造崩溃。

虽然说没法发现通用的漏洞,但是对于现在这个场景来说正好是绝配,我们可以将目标原始的鉴权代码抠出来,然后在未授权通过的时候抛出一个异常即可。构建的 Test Harness 代码如下:

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 这个 payload,可以触发 IP 鉴权的绕过!反过来分析这个 PoC,可以发现之所以能绕过是因为 webserviceIpList 只检查了前缀,而 127.0.0.10 可以在 internalIp 返回 False,即认为不是内部 IP,但实际上 webserviceIpList 却认为是内部 IP,从而导致了绕过。

如果只是从代码上去分析的话,可能一时半会并不一定能发现这个问题,可是通过 Fuzzing 在覆盖率反馈的加持下,却可以在几秒钟之内找到正解,这也是人工审计无法比拟的。

漏洞补丁

通过 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(":"));
          }

其中把 equals 换成了 startsWith,并且还过滤了我们之前使用的 WorkflowService 组件。当然还是沿袭泛微一贯的漏洞修复原则,不改业务代码,只增加安全校验,这也是对历史遗留问题的一种妥协吧。

总结

  1. 对于 Java 这样的内存安全编程语言也是可以 fuzz 的,只不过目的是找出逻辑漏洞而不是内存破坏;
  2. 漏洞挖掘初期花时间投入到代码审计中是有必要的,有助于理解项目整体结构并在后期进行针对性覆盖;
  3. 漏洞挖掘的时候重点关注边界的系统和服务,处于信任边界之外的组件更有可能过于信任外部输入导致安全问题;
  4. 对于看起来很复杂的数据处理模块,可以充分利用 Fuzzing 的优势,帮助我们快速找出畸形的 payload;
  5. 模块化 Fuzzing 的难点在于抽离代码并构建可编译或者可以独立运行的程序,即构建 Test Harness,跑起来测试用例你就已经成功了 90%;
  6. 软件开发和漏洞挖掘正好相反。开发者会出于厌恶情绪刻意避开复杂的历史遗留代码,而这些代码却是更可能出现问题的地方。因此安全研究员要学会克服自己的厌恶情绪,做到 —— “明知山有屎,偏向屎山行”。

版权声明: 自由转载-非商用-非衍生-保持署名 (CC 4.0 BY-SA)
原文地址: https://evilpan.com/2023/09/09/java-fuzzing/
微信订阅: 有价值炮灰
TO BE CONTINUED.