谈谈最近失败的挖洞经历

谈谈最近失败的挖洞经历

开始

之前被几个人喷,且找实习过程中总是一面挂,对自己产生了很大的质疑,冲动之下做了些不应该的事,写了些不该写的文章。七月没有学习,八月底开始简单的学习,尝试继续挖洞,不过并没有什么太大成果,有些事情不是努力就可以做到的,没有天赋错过时机,再多的付出也是白费。

那件事之后有师傅看不惯我,说到:你TM是不是想做世界第一?也有说:如果你算垃圾那我们去si好了?

其实我只想追求一个平均,只想不脱离大学好友的圈子,但拼了一切也跟不上。和初学者比,我当然不是垃圾,但在做安全研究的师傅里,我什么都不是,只是实话实说我是垃圾,立场不同。

闲话不多说。

这些挖洞过程还是值得总结下,本篇文章先谈一谈失败和不被认可的漏洞,至于得到确认和修复的漏洞,后续再写。

一次失败的SQL注入

首先谈谈前两周擦肩而过的一个SQL注入吧,大致过程如下:

  • 9月4日凌晨向Apache Skywalking报告
  • 9月4日早晨Skywalking主席确认了报告,无论是否可被利用都需要修复
  • 9月4日中午我与Skywalking主席讨论三种可能的攻击链路
  • 9月5日早晨得到Skywalking主席的确认,这是增强提案而不是CVE

历史上爆出过几个SQL注入漏洞,由于是H2所以比较容易从注入到RCE,打算从这方面下手看看

简单审计后发现下面的代码(节选关键部分)

// ids是String数组
// build方法见下文
String param = ArrayParamBuilder.build(ids);
// 一眼即可看出有问题
try (ResultSet rs = h2Client.executeQuery(connection, "SELECT * FROM " + modelName + " WHERE id in (" + param + ")")) {
    // ...
}

涉及到的build方法

public static String build(String[] values) {
    StringBuilder param = new StringBuilder();
    // 将字符串数组按照逗号拼接
    for (int i = 0; i < values.length; i++) {
        param.append("'").append(values[i]).append("'");
        if (i < values.length - 1) {
            param.append(",");
        }
    }
    return param.toString();
}

可以发现其中executeQuery方法没有采用预编译,判断是否使用预编译其实也简单:

  • SQL语句中是否有问号占位:显然无
  • executeQuery方法是否有第三个参数:显然无

之所以直接上报给Skywalking官方,理由如下:

  • 简单分析后,发现这个ids是可控的,也就是入口到触发点的链路没有问题
  • 通常开发中,传入id数组进行批量操作很常见,而这个id数组大概率是前端可控的

根据我Java开发经验和直接,我觉得这里80%以上是存在漏洞的,然而Skywalking的做法和普通的Web项目并不类似,它的id并不完全由前端控制,会造进入DAO层之前对id进行base64编码。显然传入的Payloadbase64编码过后就会失效。

参考Skywalking主席的修复方案:https://github.com/apache/skywalking/pull/9561

try (Connection connection = h2Client.getConnection()) {
    SQLBuilder sql = new SQLBuilder("SELECT * FROM " + modelName + " WHERE id in (");
    List<Object> parameters = new ArrayList<>();
    // 问号占位
    for (int i = 0; i < ids.length; i++) {
        if (i == 0) {
            sql.append("?");
        } else {
            sql.append(",?");
        }
        parameters.add(ids[i]);
    }
    sql.append(")");
    // 使用预编译:三个参数
    try (ResultSet rs = h2Client.executeQuery(connection, sql.toString(), parameters)) {
        //...
    }

其实官方的想法是让我提一个PR修复,不过需要forkclone改了后再提pr有点麻烦,所以我推给了官方

回复截图:

总结:

  • 有时候直觉90%有洞也未必有洞,除非真正地复现出来
  • 从庞大的开源项目中逐行审计SQL注入漏洞,总结了一些审计技巧
  • 虽然最终没有给出CVE但也算对Apache Skywalking做出了贡献

深入谈谈Zip Slip漏洞

上个月向Spring CloudApache Flink提交了多份Zip Slip漏洞,由于一些原因最终官方选择了修复(防御性编程)但由于各种原因(无法在生产环境利用或无法提升权限)不发布CVE,这个漏洞其实很有必要来谈一谈

(1)什么是Zip Slip漏洞

该漏洞在18年左右由国外安全公司提出:https://github.com/snyk/zip-slip-vulnerability

简单来说,就是代码解压缩时,压缩子项的名可以是../../test这样的文件,这时候如果有写文件的操作,将会导致任意文件写入漏洞。主流的解压缩软件其实会自动处理这种情况,想要制作这种恶意压缩包有两种方式:高端操作可以直接编辑二进制来修改已有压缩包中的子项文件名,简易方式可以用代码构造并通过ZipOuputStream生成

(2)场景和利用

JavaGolang基础库中,不存在高级的压缩文件API。但是在Python等语言中,基础库已经考虑到并修复了这种情况。值得一说的话,在Apache Commons库中,也没有高级API所以导致这种漏洞在Java项目中尤其常见

在陈师傅(chybeta)星球有两篇文章提到该攻击:

(1)利用Zip Slip漏洞RCE:https://t.zsxq.com/05m2NJYbY

(2)陈师傅挖洞中遇到的:https://t.zsxq.com/05eUBY3vB

可能有师傅没有加星球,所以我简单总结下陈师傅分享的两种利用:服务端做更新操作的时候会拉取远程的更新压缩包并解压和更新,假设这个压缩包可控,将可以写入SSH Key或者PHP/JSP等后门以此达到RCE效果;另外各种CMS/OA系统的后台大概率存在上传解压文件的功能,例如备份和恢复功能,在解压的时候类似的思路实现RCE效果

(3)一些尝试

之前向Apache Flink提交了一份Zip Slip漏洞:https://issues.apache.org/jira/browse/FLINK-29122

其实提交的时候已经觉得不会认了,因为无法做到超越当前权限的事情,官方回复印证了我想法:有权访问 Flink 集群的用户无论如何都可以在那里执行任意代码,所以这更像是一个错误(因为意外行为)而不是漏洞

我向Spring Cloud Contract提交的Zip Slip漏洞,的确可以利用,但最终被特殊原因拒绝了,给出修复链接

这个漏洞在Spring Cloud Contract中应该如何利用就不多说了,鸡肋且不认可所以没必要研究。官方的理由是:这仅是一个测试框架,不会在生产环境中使用(之前给Spring Cloud Contract报告了一些其他漏洞,有高危也有鸡肋,全部都被一样的原因拒绝了,测试框架的漏洞难道不应该算漏洞嘛)

最后官方还是在文档中明确写出了:您永远不应该下载来自不受信任位置的合约,参考链接

简单来看下代码,一些重点内容写在注释中:

try (ZipInputStream zis = new ZipInputStream(sourceFs.open(file))) {
    ZipEntry entry;
    // 遍历压缩包
    while ((entry = zis.getNextEntry()) != null) {
        Path relativePath = new Path(entry.getName());
        // 拼接路径(将解压目的和压缩项名进行拼接)
        Path newFile = new Path(targetDirectory, relativePath);
        // 创建该文件
        try (FSDataOutputStream fileStream =
             targetFs.create(newFile, FileSystem.WriteMode.NO_OVERWRITE)) {
            // 写入文件
            IOUtils.copyBytes(zis, fileStream, false);
        }
    }

修复代码如下,包含了..抛出了异常:

while ((entry = zis.getNextEntry()) != null) {
    // 子项包含..则抛出异常
    if (entry.getName().contains(".." + File.separatorChar)) {
        throw new IOException(
            "Zip entry contains illegal characters: " + entry.getName());
    }

可以看下另一种修复方式,参考Tomcat在解压时候的代码(并不是漏洞)

Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
    JarEntry jarEntry = jarEntries.nextElement();
    String name = jarEntry.getName();
    // 拼接目标和压缩项的名称
    File expandedFile = new File(docBase, name);
    // 处理后的路径如何开头非法则抛出异常
    if (!expandedFile.getCanonicalFile().toPath().startsWith(canonicalDocBasePath)) {
        throw new IllegalArgumentException(...);
    }

这里的getCanonicalFile效果如下,如果绝对路径开头不匹配目标,那么抛出异常

其实如果分析了上文Spring Cloud Contract的修复方式,会发现和这种方式类似

before: \test\..\src\1.txt
after:  \src\test1.txt

(4)如何挖掘该漏洞

简单来说一下Zip Slip漏洞的挖掘思路:确定漏洞触发点并向上分析

对于确定漏洞触发点(sink)的方式有很多,我简单谈一谈。开源项目的角度可以直接搜索ZipInputStreamJarInputStream关键字,因为这是解压zipjar包必须的类,但是实践发现JarInputStream更多情况下并不实际写文件,而是遍历其中的压缩项并提取关键配置文件等信息。另外一个思路是搜索FileOutputStream或者Files.write等写文件的代码,实践中发现接近三分之一的情况中FileOutputStream配合解压文件使用。非开源的Java项目一般是提供Jar包,批量反编译是一种方案,进阶情况可以写字节码分析的工具。

基于Golang的项目也存在大量类似的漏洞,不过起点是archive/zip包,根据这个点寻找不难,比如之前对Apache OpenWhisk简单分析后,得到三处潜在漏洞,但分析后都无法直接利用:

openwhisk-wskdeploy/dependencies/gitreader.go#CloneDependency
openwhisk-runtime-go/openwhisk/zip.go#Unzip
openwhisk-cli/commands/util.go#unpackZip

进一步的分析类似其他漏洞,主要是找到每一条调用链:A->B->C->D->解压文件操作,选择codeql或肉眼分析都是办法。不过我之前是自己写工具来做的,因为有时候需要做一件小事,为了小事去学一个大领域有点杀鸡用牛刀感觉,当然从长久角度来看,学好静态分析和codeql之类的总没错。

(5)总结

由于Java语言标准库和Apache Commons库都不存在高级API导致这类漏洞在Java项目中极其常见,但绝大多数情况下都无法利用,由于各种各样奇特的理由:比如无法提升任何权限,或者项目仅用于测试不用于生产环境等理由。所以将思路放在自动工具上是不妥的,终究还得结合实际情况来人工调试分析。

其实我挖到的Zip Slip洞并不是全部都被拒绝的,还是有几个项目认可并可能在未来发布CVE和安全公告,这里就不多说了。无论如何,也算是对多个知名项目的代码做出了安全方面的贡献。

再谈正则DOTALL绕过

这其实是之前搞正则DOTALL绕过时候顺便搞的东西,分析了TomcatHTTPD中是否存在这样的绕过可能。

Tomcat好几次了,每次都没有什么收获。不过有一次,我将TomcatApache HTTPD做对比的时候,发现了一处有趣的地方:介于漏洞和Bug之间

注意到Apache HTTPDNginx等组件都存在rewrite模块,主要用于处理重定向。以Apache HTTPD为例:
https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html

假设我们配置这样的rewrite规则:

RewriteRule /foo/.* /bar

结果应该是所有/foo/下的请求重定向到/bar,其实实际上不这么用,也许会把/foo/(.*)取出来作为变量传递到类似/bar?id=$1这样的地方。之前有个Shiro的垃圾绕过洞,就是绕过.*这样的正则,通过%0a和%0d即可绕过没有设置DOTALL选项的正则。

类似的原理,假设配置了以上的规则,可能存在一种方式使访问/foo/?可以不被重定向到/bar。按照漏洞的一种定义:产生意外行为的代码可以被定义的漏洞,这将是一个配置漏洞或者逻辑漏洞,虽然想不到太多的危害。除非/foo下有什么受保护的资源,或者需要授权才能访问的东西,假设这种情况,也需要有restful或者对url有特殊处理的情况下才可以有意义地访问。

最先并没有分析Tomcat而是从HTTPD入手,在Apache HTTPDrewrite中:对正则表达式进行编译的函数如下,其中cflags是编译正则表达式的选项,在mod_rewrite中该选项为newrule->flags & RULEFLAG_NOCASE)? AP_REG_ICASE : 0,虽复杂,但可以发现仅与用户配置和大小写case有关,暂时说明默认不配置DOTALL,存在绕过规则的可能。

regexp = ap_pregcomp(cmd->pool, a1, AP_REG_EXTENDED |
                                    ((newrule->flags & RULEFLAG_NOCASE)
                                        ? AP_REG_ICASE : 0));

跟入util.c

AP_DECLARE(ap_regex_t *) ap_pregcomp(apr_pool_t *p, const char *pattern, int cflags)
{
    // 跟入
    int err = ap_regcomp(preg, pattern, cflags);
    // ...
}

跟入发现下面这样的代码:不难发现,首先和默认规则进行某种操作,然后根据cflags设置参数。在其中找到了DOTALL选项,因此分析AP_REG_NO_DEFAULT是什么东西。

// 首先和默认规则进行&操作
if ((cflags & AP_REG_NO_DEFAULT) == 0)
    // 结果为0则变成默认
    cflags |= default_cflags;
// 根据cflags进行设置(省略)
if ((cflags & AP_REG_DOTALL) != 0)
    options |= PCREn(DOTALL);

ap_regex.h头文件中发现:

  • AP_REG_NO_DEFAULT0x400
  • AP_DOTALL0x40
  • default_cflagsAP_REG_DOTALL|AP_REG_DOLLAR_ENDONLY用二进制表示为:0x240

分析得出的结论是: 从mod_rewrite传入的cflags会变成默认的0x240数值,强制设置了DOTALL选项和另一个选项,之后的&运算一定为1。

Apache Tomcat中同样存在rewrite模块,不过不像HTTPD一样以mod命名而是一个Valve:https://tomcat.apache.org/tomcat-9.0-doc/rewrite.html

分析Tomcat代码之后,其中rewrite rule解析核心代码节选如下:

int flags = 0;
if (isNocase()) {
    flags |= Pattern.CASE_INSENSITIVE;
}
Pattern.compile(patternString, flags);

发现仅设置了CASE_INSENSITIVEflag然后直接Pattern.compile操作,显然这里存在绕过。一点题外话,不难发现Java代码的确比C代码分析起来更简单,至少Tomcat找核心代码一步到位,而HTTPD跳了多步。

简单对Tomcat进行配置:
(1)在server.xml中开启RewirteValve功能

<Valve className="org.apache.catalina.valves.rewrite.RewriteValve" />

(2)在conf\Catalina\localhost中新建rewrite.config文件

RewriteRule /foo/.* /bar

(3)启动Tomcat测试绕过

/foo/bar -> /bar OK
/foo/123 -> /bar OK
/foo/1%0a23 -> /foo/1%0a23 BYPASS
/foo/1%0b23 -> /bar OK
/foo/1%0d23 -> /foo/1%0d23 BYPASS

我将自己对于Apache HTTPDTomcat的对比分析过程和结果报告到Tomcat官方,询问他们对于此问题的看法,以及是否认为这是安全漏洞。

Apache Tomcat官方对于此问题的看法:

  • 官方认为我报告的问题是一个BUG而不是漏洞
  • 因为rewrite用于重定向而不是保护资源等安全理由
  • 我报告的问题将在未来被修复

总结:

  • 漏洞(Vulnerability)和错误(Bug)的区别是什么?
  • 虽然最终没有给出CVE但也算对Apache Tomcat做出了贡献

解析库中的通用DoS

之前有多篇文章来谈论DoS拒绝服务攻击,这次再谈一个特殊的,解析和协议层面的拒绝服务

很多文件结构和协议都有table的概念:

  • 四个字节(int)表示某个属性的总长度
  • 随后是每个该属性的项目,通常是struct结构

常见的class结构中有多种这样的表结构,在elf/pe等文件结构中也类似。这样的结构在解析库中,大概率会存在内存拒绝服务漏洞,我用一段伪代码来解释:

public readAny(byte[] data){
    // 表长度
    int tableLen = data[0];
    // 初始化表数组
    Struct[] tableArray = new Struct[tableLen];
    // 依次读入所有表项
    // ...
}

如果没有对表长度进行验证,将会导致数组可能使用2G以上内存(由于四字节int类型最大0xffffffff)

当目标系统的某程序内存超过限制时,将会导致某程序被被kill(OOM-Killer)

垃圾DoS没有太多必要进行分析,所以简单提一下实践的情况:

  • Apache项目解析库中的DoS但认为是上层应用应该解决的问题(主要是不好修)
  • Golang标准解析库中的DoS但官方说已经有人提前报告了(卷)
  • 某知名开源项目的DoS已被确认并致谢,正在修复中(拿了点赏金)

之所以说这种DoS是通用的,因为绝大多数解析库中不会对表长度进行验证。但也应该注意一个问题,只有四字节int和八字节long类型的表长是可能存在DoS的,两字节short类型能够支持最大值也不过65535,仅分配64K的内存如何DoS呢?

结束

并非一直失败,总有成功的例子;并非都是垃圾鸡肋洞,也有RCE等高危,后续文章再谈吧。