数据库漏洞扫描-啊d注入工具扫描网站后台漏洞
图 1.1 CodeQL 代码审计流程阶段
为什么我们最终会遇到安全风险而不是漏洞? 这是因为 CodeQL 不是万能的。 它只能帮助我们发现可能存在的安全隐患,并不一定能确定漏洞的存在。 这将在以后的文章中解释。
目前的自动化代码审计工具将使用python3开发,目标语言为java。 如果有时间,会陆续支持其他语言。 目前代码不是特别完善,后面会继续优化代码,见github地址:
0x02 工具设计
基于CodeQL的自动化代码审计工具的过程其实和传统的漏洞扫描工具是类似的,所以我们还是按照传统的漏扫思维来设计工具。 关于第一阶段将源码转入数据库的部分,将在下一篇文章中详细介绍。 这里我们只关注第二阶段数据库查询的内容,如图2.1所示。
图 2.1 自动化工具设计流程
其实从流程可以看出,该工具的主要功能是基于ql插件的遍历,以及插件结果的格式化输出。首先需要解决的问题是源码ql 插件。 在上一篇文章中,我们提到了CodeQL官方给我们提供了很多demo例子来进行测试。
.
据CWE介绍,官方提供了几种不同类型的ql插件。 有些插件可以直接使用,但有些插件涉及到自定义的qll库,需要改造一下才能使用,如图2.2所示。 因此,FilePathInjection.ql 脚本只是具有自定义库的典型脚本。
图2.2 使用qll自定义库的ql脚本
在我们设计的自动化工具中,为了方便只能查询单个ql脚本,需要转换ql脚本中调用的qll库。 转换方式是直接将qll库中定义的类和谓词定义到ql脚本中。 官方提供的脚本我都转换了,后面会把完整的代码分享到github上。
为了便于结果的统一格式输出,我们期望每个ql文件最终返回的结果都是统一的格式,所以也需要对每个ql文件的最终返回结果进行约束。 一个典型的演示如下所示。 select之后的值就是ql脚本最终返回的数据。
from DataFlow::PathNode source, DataFlow::PathNode sink, BeanShellInjectionConfig conf
where conf.hasFlowPath(source, sink)
select source.toString(),source.getNode().getEnclosingCallable(),source.getNode().getEnclosingCallable().getFile().getAbsolutePath(),
sink.toString(),source.getNode().getEnclosingCallable(), sink.getNode().getEnclosingCallable().getFile().getAbsolutePath(), "BeanShell injection"
表 2.1 ql 脚本输出规范约束
由于CodeQL官方没有开源引擎,我们只能直接使用官方编译好的版本。 官方编译引擎不支持python等语言,只能通过命令行调用,如图2.3所示。 -d参数用于表示要查询的数据库路径,后面是要查询的ql脚本路径。
图2.3 通过命令行调用codeql查询
由于CodeQL每次查询都需要使用ql脚本文件路径,如果每次查询都需要先生成一个文件,查询完成后再删除该文件,代码看起来很奇怪。 好在python为我们提供了tempfile库,可以更优雅的解决这个问题,如图2.4所示。 这是我项目中的一段代码,用于检查环境是否准备就绪。 通过tempfile生成一个临时的ql脚本,运行后临时脚本会自动删除。
图2.4 使用tempfile生成临时文件查询codeql
本来想自己封装一个类调用CodeQL运行,突然看到网上有个大佬写了对应的类。 实现思路和我之前的思路差不多,本质上就是命令行调用CodeQL。 但是,我直接运行大哥的代码失败了。 主要原因是生成的临时文件必须在ql sdk所在的test路径下,并且该路径下必须有正确配置的qlpack.yml文件。 所以我在原有代码的基础上进行了修改,主要是将sdk路径固定为配置的路径。
之后,我们的一个简单的基于CodeQL的自动化代码审计工具的原型已经基本准备就绪,我们将继续基于这个框架进行功能优化。
0x03 插件优化
虽然官方提供了大约59个java ql查询插件,但是远远不能满足我们的需求。 我们希望更多的白帽子参与提供更多的ql查询插件。 现阶段,我根据自己日常的漏洞挖掘过程,添加了一些ql查询插件,如下图,相关插件都在plugins/java_ext目录下。
表3.1 新增Java常见漏洞查询ql脚本
这个新增内容只是一个开始,并不能涵盖所有内容。 我知道还有很长的路要走。 但是不断的优化总会有好的结果。 由于有些朋友对CodeQL的语法了解不多,我们用一个简单的脚本Unserialze.ql来说明一个完整的CodeQL脚本的编写。
反序列化漏洞是java中常见的漏洞,典型的漏洞代码编写如下。 这是从应用程序中提取的真实漏洞的部分代码。
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
ObjectInputStream objin = new ObjectInputStream(request.getInputStream()); //这里是获取用户输入
response.setContentType("application/x-download");
ServletOutputStream out = response.getOutputStream();
try {
String dsName = (String)objin.readObject(); //这里是反序列化的点
System.out.println(dsName);
} catch (Exception var11) {
var11.printStackTrace();
}
out.close();
objin.close();
}
最关键的一点是,用户可控的源点是request.getInputStream(),而最终的危险操作汇点是objin.readObject()。 也就是说,直接反序列化从外部传入的postdata,可能会导致反序列化漏洞。 对于CodeQL,可以如下编写相应的查询脚本。
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.dataflow.DataFlow
class UnserializeSink extends DataFlow::Node {
UnserializeSink(){
exists(MethodAccess ma,Class c | ma.getMethod().hasName("readObject") and
ma.getQualifier().getType() = c and
c.getASupertype*().hasQualifiedName("java.io", "InputStream") and
this.asExpr() = ma
)
}
}
class UnserializeSanitizer extends DataFlow::Node {
UnserializeSanitizer() {
this.getType() instanceof BoxedType or this.getType() instanceof PrimitiveType
}
}
class JavaUnserialize extends TaintTracking::Configuration {
JavaUnserialize() { this = "Java Unsearialize" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof UnserializeSink }
override predicate isSanitizer(DataFlow::Node node) { node instanceof UnserializeSanitizer }
}
from DataFlow::PathNode source, DataFlow::PathNode sink, JavaUnserialize conf
where
conf.hasFlowPath(source, sink)
select source.toString(),source.getNode().getEnclosingCallable(),source.getNode().getEnclosingCallable().getFile().getAbsolutePath(),
sink.toString(),source.getNode().getEnclosingCallable(), sink.getNode().getEnclosingCallable().getFile().getAbsolutePath(), "Potential JAVA Unserialize Vulnerability"
源点直接使用CodeQL预定义类RemoteFlowSource,而汇点则通过如下代码实现。 判断逻辑是有一个方法叫readObject,调用主体继承自java.io.InputStream类。 注意这些是反序列化漏洞,不涉及使用链数据库漏洞扫描,不一定会导致RCE效果。
exists(MethodAccess ma,Class c | ma.getMethod().hasName("readObject") and
ma.getQualifier().getType() = c and
c.getASupertype*().hasQualifiedName("java.io", "InputStream") and
this.asExpr() = ma
)
单独执行相应的脚本,可以发现程序中可能存在的反序列化漏洞数据库漏洞扫描,如图3.1所示。
图3.1 单独运行Unserialize.ql脚本的效果
其他脚本不再依次说明。 如果有小伙伴感兴趣的话,期待小伙伴可以给我们提供插件。 如果你不会写CodeQL脚本,可以私信我漏洞代码逻辑,我会转成CodeQL插件。
0x04 工具使用
回到工具本身,目前完整的代码我已经放在了github上,使用方法如下。
在使用之前,您应该先安装 CodeQL 并配置 config/config.ini。 最重要的是配置临时生成的ql脚本保存的路径qlpath,如图4.1所示。 确保qlpath当前目录下有配置文件qlpack.yml。 如果在使用过程中出现问题,建议将debug配置为on。
图 4.1 项目配置文件
运行python3 main.py -h,如图4.2所示。
参数-d代表数据库文件的地址,必填。
参数-s代表是否跳过环境检查。 如果不填,默认为false。 建议首次使用不要跳过环境检查。
图 4.2 项目支持的参数列表
运行python3 main.py -d /Users/xxxx/CodeQL/databases/RuoYI/,靠源码掩盖效果。
图 4.3 使用该工具得到的扫描结果
最终的扫描结果以csv文件的形式保存在out/result/目录下,打开对应的结果,如图4.4所示。
图 4.4 将结果格式化为 CSV 文件
关于结果的分析,我们在之前的文章中已经谈到了一些,这里就不分析结论了。
0x05 工具不足
在图4.4的结果中,FilePathInjection插件扫描的结果很多,缺点也很相似。 以其中一个为例,我们根据文件的source和sink来定位问题。
找到源文件和方法,com.ruoyi.web.controller.system.SysProfileController类的updateAvatar方法,如图5.1所示。
图 5.1 源类和方法
继续跟踪上传方式,可以到汇点,如图5.2所示。
图 5.2 Sink 的类和方法
事实上,若一已经对上传文件的文件扩展名进行了限制,CodeQL 仍然认为这是一个漏洞。 这是典型的误报行为,也是CodeQL的代码审计工具中最难解决的问题。
CodeQL可以跟踪Source和Sink流向,但它毕竟只是静态代码审计工具,无法自动分析代码中的一些过滤操作,导致误报。 而这也是为什么文章开头提到的CodeQL只能作为发现安全隐患的辅助工具,无法判断是否一定存在漏洞。
0x06 结论
在自动化代码审计工具方面,我们还有很长的路要走。 如果您能提供一些可用的ql插件或者提供有漏洞的代码示例供我们编写ql插件,我们将不胜感激。
未来我们会继续丰富工具的功能,特别是解决前一阶段生成数据库的问题。