之前被几个人喷,且找实习过程中总是一面挂,对自己产生了很大的质疑,冲动之下做了些不应该的事,写了些不该写的文章。七月没有学习,八月底开始简单的学习,尝试继续挖洞,不过并没有什么太大成果。
这些挖洞过程还是值得总结下。
首先谈谈前两周擦肩而过的一个SQL注入吧,大致过程如下:
Apache Skywalking
报告Skywalking
主席确认了报告,无论是否可被利用都需要修复Skywalking
主席讨论三种可能的攻击链路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
方法
xxxxxxxxxx
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
方法没有采用预编译,判断是否使用预编译其实也简单:
executeQuery
方法是否有第三个参数:显然无之所以直接上报给Skywalking
官方,理由如下:
根据我Java
开发经验和直接,我觉得这里80%以上是存在漏洞的,然而Skywalking
的做法和普通的Web
项目并不类似,它的id并不完全由前端控制,会造进入DAO
层之前对id进行base64
编码。显然传入的Payload
在base64
编码过后就会失效。
参考Skywalking
主席的修复方案:https://github.com/apache/skywalking/pull/9561
xxxxxxxxxx
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
修复,不过需要fork
并clone
改了后再提pr
有点麻烦,所以我推给了官方
回复截图:
总结:
SQL
注入漏洞,总结了一些审计技巧CVE
但也算对Apache Skywalking
做出了贡献上个月向Spring Cloud
和Apache Flink
提交了多份Zip Slip
漏洞,由于一些原因最终官方选择了修复(防御性编程)但由于各种原因(无法在生产环境利用或无法提升权限)不发布CVE,这个漏洞其实很有必要来谈一谈
(1)什么是Zip Slip漏洞
该漏洞在18年左右由国外安全公司提出:https://github.com/snyk/zip-slip-vulnerability
简单来说,就是代码解压缩时,压缩子项的名可以是../../test
这样的文件,这时候如果有写文件的操作,将会导致任意文件写入漏洞。主流的解压缩软件其实会自动处理这种情况,想要制作这种恶意压缩包有两种方式:高端操作可以直接编辑二进制来修改已有压缩包中的子项文件名,简易方式可以用代码构造并通过ZipOuputStream
生成
(2)场景和利用
在Java
和Golang
基础库中,不存在高级的压缩文件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
报告了一些其他漏洞,有高危也有鸡肋,全部都被一样的原因拒绝了,测试框架的漏洞难道不应该算漏洞嘛)
最后官方还是在文档中明确写出了:您永远不应该下载来自不受信任位置的合约,参考链接
简单来看下代码,一些重点内容写在注释中:
xxxxxxxxxx
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);
}
}
修复代码如下,包含了..
抛出了异常:
xxxxxxxxxx
while ((entry = zis.getNextEntry()) != null) {
// 子项包含..则抛出异常
if (entry.getName().contains(".." + File.separatorChar)) {
throw new IOException(
"Zip entry contains illegal characters: " + entry.getName());
}
可以看下另一种修复方式,参考Tomcat
在解压时候的代码(并不是漏洞)
xxxxxxxxxx
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
的修复方式,会发现和这种方式类似
xxxxxxxxxx
before: \test\..\src\1.txt
after: \src\test1.txt
(4)如何挖掘该漏洞
简单来说一下Zip Slip
漏洞的挖掘思路:确定漏洞触发点并向上分析
对于确定漏洞触发点(sink)的方式有很多,我简单谈一谈。开源项目的角度可以直接搜索ZipInputStream
和JarInputStream
关键字,因为这是解压zip
和jar
包必须的类,但是实践发现JarInputStream
更多情况下并不实际写文件,而是遍历其中的压缩项并提取关键配置文件等信息。另外一个思路是搜索FileOutputStream
或者Files.write
等写文件的代码,实践中发现接近三分之一的情况中FileOutputStream
配合解压文件使用。非开源的Java项目一般是提供Jar
包,批量反编译是一种方案,进阶情况可以写字节码分析的工具。
基于Golang
的项目也存在大量类似的漏洞,不过起点是archive/zip
包,根据这个点寻找不难,比如之前对Apache OpenWhisk
简单分析后,得到三处潜在漏洞,但分析后都无法直接利用:
xxxxxxxxxx
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绕过时候顺便搞的东西,分析了Tomcat
和HTTPD
中是否存在这样的绕过可能。
啃Tomcat
好几次了,每次都没有什么收获。不过有一次,我将Tomcat
与Apache HTTPD
做对比的时候,发现了一处有趣的地方:介于漏洞和Bug
之间
注意到Apache HTTPD
和Nginx
等组件都存在rewrite
模块,主要用于处理重定向。以Apache HTTPD
为例:
https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html
假设我们配置这样的rewrite
规则:
xxxxxxxxxx
RewriteRule /foo/.* /bar
结果应该是所有/foo/
下的请求重定向到/bar
,其实实际上不这么用,也许会把/foo/(.*)
取出来作为变量传递到类似/bar?id=$1
这样的地方。之前有个Shiro
的垃圾绕过洞,就是绕过.*
这样的正则,通过%0a和%0d即可绕过没有设置DOTALL
选项的正则。
类似的原理,假设配置了以上的规则,可能存在一种方式使访问/foo/?
可以不被重定向到/bar
。按照漏洞的一种定义:产生意外行为的代码可以被定义的漏洞,这将是一个配置漏洞或者逻辑漏洞,虽然想不到太多的危害。除非/foo
下有什么受保护的资源,或者需要授权才能访问的东西,假设这种情况,也需要有restful
或者对url
有特殊处理的情况下才可以有意义地访问。
最先并没有分析Tomcat
而是从HTTPD
入手,在Apache HTTPD
的rewrite
中:对正则表达式进行编译的函数如下,其中cflags
是编译正则表达式的选项,在mod_rewrite
中该选项为newrule->flags & RULEFLAG_NOCASE)? AP_REG_ICASE : 0
,虽复杂,但可以发现仅与用户配置和大小写case
有关,暂时说明默认不配置DOTALL,存在绕过规则的可能。
xxxxxxxxxx
regexp = ap_pregcomp(cmd->pool, a1, AP_REG_EXTENDED |
((newrule->flags & RULEFLAG_NOCASE)
? AP_REG_ICASE : 0));
跟入util.c
中
xxxxxxxxxx
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
是什么东西。
xxxxxxxxxx
// 首先和默认规则进行&操作
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_DEFAULT
为0x400
AP_DOTALL
为0x40
default_cflags
为AP_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
解析核心代码节选如下:
xxxxxxxxxx
int flags = 0;
if (isNocase()) {
flags |= Pattern.CASE_INSENSITIVE;
}
Pattern.compile(patternString, flags);
发现仅设置了CASE_INSENSITIVE
的flag
然后直接Pattern.compile
操作,显然这里存在绕过。一点题外话,不难发现Java
代码的确比C
代码分析起来更简单,至少Tomcat
找核心代码一步到位,而HTTPD
跳了多步。
简单对Tomcat
进行配置:
(1)在server.xml
中开启RewirteValve
功能
xxxxxxxxxx
<Valve className="org.apache.catalina.valves.rewrite.RewriteValve" />
(2)在conf\Catalina\localhost
中新建rewrite.config
文件
xxxxxxxxxx
RewriteRule /foo/.* /bar
(3)启动Tomcat
测试绕过
xxxxxxxxxx
/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 HTTPD
和Tomcat
的对比分析过程和结果报告到Tomcat
官方,询问他们对于此问题的看法,以及是否认为这是安全漏洞。
Apache Tomcat
官方对于此问题的看法:
rewrite
用于重定向而不是保护资源等安全理由总结:
CVE
但也算对Apache Tomcat
做出了贡献之前有多篇文章来谈论DoS
拒绝服务攻击,这次再谈一个特殊的,解析和协议层面的拒绝服务
很多文件结构和协议都有table
的概念:
struct
结构常见的class
结构中有多种这样的表结构,在elf/pe
等文件结构中也类似。这样的结构在解析库中,大概率会存在内存拒绝服务漏洞,我用一段伪代码来解释:
xxxxxxxxxx
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等高危,后续文章再谈吧。