本次实验项目源码来源之前我写的Shiro-CTF的源码https://github.com/SummerSec/JavaLearnVulnerability/tree/master/shiro/shiro-ctf ,项目需要database文件上传到GitHub项目 learning-codeql上。
本文的漏洞分析文章一道shiro反序列化题目引发的思考 ,看本文之前看完这漏洞分析会更好的理解。但本文会从全新的角度去挖掘审计漏洞,但难免会有之前既定思维。如果你有兴趣和我一起交流学习CodeQL可以联系summersec#qq.com。
找到可以序列化的类
挖掘反序列化漏洞,首先得找到入口。可以反序列化的类首先肯定是实现了接口Serializable
,其次会有一个字段serialVersionUID
,所以我们可以从找字段或者找实现接口Serializable
入手进行代码分析。
-
TypeSerializable
类,在JDK中声明 -
instanceof
断言 -
fromSource
谓词判断来着项目代码排除JDK自带 -
getASupertype
递归,父类类型
import java
/*找到可以序列化类,实现了Serializable接口 */
from Class cl
where
cl.getASupertype() instanceof TypeSerializable
/* 递归判断类是不是实现Serializable接口*/
and
cl.fromSource()
/* 限制来源 */
select cl
/* 查询语句 */
点击查询出来的结果可以看到对应的查询结果源码
找User类实例化代码
使用
RefType.hasQualifiedName(string packageName, string className)
来识别具有给定包名和类名的类,这里使用一个类继承RefType
,使代码可读性更高点。例如下面两端QL代码是等效的:import javafrom RefType rwhere r.hasQualifiedName("com.summersec.shiroctf.bean", "User")select r
import java
/* 找到实例化User的类 */
class MyUser extends RefType{
MyUser(){
this.hasQualifiedName("com.summersec.shiroctf.bean", "User")
}
}
from ClassInstanceExpr clie
where
clie.getType() instanceof MyUser
select clie
可以发现在
IndexController
类59行处实例化User
类。
IndexController
:package com.summersec.shiroctf.controller;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import com.summersec.shiroctf.Tools.LogHandler;import com.summersec.shiroctf.Tools.Tools;import com.summersec.shiroctf.bean.User;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;@Controllerpublic class IndexController { public IndexController() {
} @GetMapping({"/"}) public String main() { return "redirect:login";
} @GetMapping({"/index/{name}"}) public String index(HttpServletRequest request, HttpServletResponse response, @PathVariable String name) throws Exception {
Cookie[] cookies = request.getCookies(); boolean exist = false;
Cookie cookie = null;
User user = null; if (cookies != null) {
Cookie[] var8 = cookies; int var9 = cookies.length; for(int var10 = 0; var10 < var9; ++var10) {
Cookie c = var8[var10]; if (c.getName().equals("hacker")) {
exist = true;
cookie = c; break;
}
}
} if (exist) { byte[] bytes = Tools.base64Decode(cookie.getValue());
user = (User)Tools.deserialize(bytes);
} else {
user = new User();
user.setID(1);
user.setUserName(name);
cookie = new Cookie("hacker", Tools.base64Encode(Tools.serialize(user)));
response.addCookie(cookie);
}
request.setAttribute("hacker", user);
request.setAttribute("logs", new LogHandler()); return "index";
}
}
查看Tools类源码是否存在问题
查看源码有
Base64
编码解码函数、序列化、反序列化以及exeCmd
方法,该函数可以执行命令对于
Tools#deserialize
方法可以编写规则:import javaclass Deserialize extends RefType{
Deserialize(){ this.hasQualifiedName("com.summersec.shiroctf.Tools", "Tools")
}
}class DeserializeTobytes extends Method{
DeserializeTobytes(){ this.getDeclaringType() instanceof Deserialize and
this.hasName("deserialize")
}
}from DeserializeTobytes des
select des
对于
Tools#exeCmd
方法的调用可以找到,可以发现LogHandler
类调用了两次exeCmd
方法。import java/* 找到调用exeCmd方法 */from MethodAccess exeCmd
where exeCmd.getMethod().hasName("exeCmd")select exeCmd 下面是 exeCmd
方法的源码,不能发现可以执行任何命令,传入的参数commandStr
即是将被执行的命令。public static String exeCmd(String commandStr) {
BufferedReader br = null; String OS = System.getProperty("os.name").toLowerCase(); try {
Process p = null; if (OS.startsWith("win")){
p = Runtime.getRuntime().exec(new String[]{"cmd", "/c", commandStr});
}else {
p = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", commandStr});
}
br = new BufferedReader(new InputStreamReader(p.getInputStream())); String line = null;
StringBuilder sb = new StringBuilder(); while((line = br.readLine()) != null) {
sb.append(line + "n");
} return sb.toString();
} catch (Exception var5) {
var5.printStackTrace(); return "error";
}
}
精简代码逻辑
对 IndexController
简单提炼处理逻辑,画出数据流流程图:HttpServletRequest request = null
Cookie[] cookies = request.getCookies();Cookie cookie = c
byte[] bytes = Tools.base64Decode(cookie.getValue());user = (User)Tools.deserialize(bytes);目前就有以下几点:
request
和bytes
是有联系的预期是将 request
作为source
,sink
是deserialize()#bytes
Tools#exeCmd
方法肯定是可以被利用的Loghandler
类的目的?
污点分析
污点分析简单介绍
现在已经确定了(a)程序中接收不受信任数据的地方和(b)程序中可能执行不安全的反序列化的地方。现在把这两个地方联系起来:未受信任的数据是否流向潜在的不安全的反序列化调用?在程序分析中,我们称之为 数据流
问题。数据流作用:这个表达式是否持有一个源程序中某一特定地方的值呢?污点分析是一种跟踪并分析污点信息在程序中流动的技术。在漏洞分析中,使用污点分析技术将所感兴趣的数据(通常来自程序的外部输入)标记为污点数据,然后通过跟踪和污点数据相关的信息的流向,可以知道它们是否会影响某些关键的程序操作,进而挖掘程序漏洞。 在CodeQL提供了数据流分析的模块,分为全局数据流、本地数据流、远程数据流。数据流分析有一般有以下几点:
-
source
定义污染数据即污点 -
sink
判断污点数据流出 -
sanitizers
对数据流判断无害处理(可选) -
additionalTaintStep
CodeQL增加判断污染额外步骤(可选)
int func(int tainted) { int x = tainted; if (someCondition) { int y = x;
callFoo(y);
} else { return x;
} return -1;
}
确定source和sink
source
和sink
,现在可以知道的是IndexController
类中的index
函数的参数request
是可以用户可控可以作为一个source
。然后现在目前已知可以反序列化函数点在Tools#deserialize
方法的传入参数bytes
,可以作为一个sink
。class Myindex extends RefType{
Myindex(){ this.hasQualifiedName("com.summersec.shiroctf.controller", "IndexController")
}
}class MyindexTomenthod extends Method{
MyindexTomenthod(){ this.getDeclaringType().getAnAncestor() instanceof Myindex and
this.hasName("index")
}
}
predicate isDes(Expr arg){
exists(MethodAccess des |
des.getMethod().hasName("deserialize")
and
arg = des.getArgument(0)
)
}
/**
* @name Unsafe shiro deserialization
* @kind problem
* @id java/unsafe-deserialization
*/import javaimport semmle.code.java.dataflow.DataFlow// TODO add previous class and predicate definitions hereclass ShiroUnsafeDeserializationConfig extends DataFlow::Configuration {
ShiroUnsafeDeserializationConfig() { this = "ShiroUnsafeDeserializationConfig" }
override predicate isSource(DataFlow::Node source) {
exists(/** TODO fill me in **/ |
source.asParameter() = /** TODO fill me in **/
)
}
override predicate isSink(DataFlow::Node sink) {
exists(/** TODO fill me in **/ | /** TODO fill me in **/
sink.asExpr() = /** TODO fill me in **/
)
}
}
from ShiroUnsafeDeserializationConfig config, DataFlow::Node source, DataFlow::Node sink
where config.hasFlow(source, sink)
select sink, "Unsafe Shiro deserialization"
-
isSource() 定义了数据可能从哪里流出。 -
isSink()定义了数据可能流向的地方。
第一次尝试
@kind
关键词将problem
转换为 path -problem
告诉CodeQL工具将这个查询的结果解释为路径结果。/**
* @name Unsafe shiro deserialization
* @kind path-problem
* @id java/unsafe-deserialization
*/import javaimport semmle.code.java.dataflow.DataFlowimport semmle.code.java.dataflow.TaintTracking
predicate isDes(Expr arg){
exists(MethodAccess des |
des.getMethod().hasName("deserialize")
and
arg = des.getArgument(0))
}class Myindex extends RefType{
Myindex(){ this.hasQualifiedName("com.summersec.shiroctf.controller", "IndexController")
}
}class MyindexTomenthod extends Method{
MyindexTomenthod(){ this.getDeclaringType().getAnAncestor() instanceof Myindex and
this.hasName("index")
}
}class ShiroUnsafeDeserializationConfig extends TaintTracking::Configuration {
ShiroUnsafeDeserializationConfig() {
this = "ShiroUnsafeDeserializationConfig"
}
override predicate isSource(DataFlow::Node source) {
exists(MyindexTomenthod m | // m.
source.asParameter() = m.getParameter(0)
)
}
override predicate isSink(DataFlow::Node sink) {
exists(Expr arg|
isDes(arg) and
sink.asExpr() = arg /* bytes */
)
}
}
from ShiroUnsafeDeserializationConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink, source, sink, "Unsafe Shiro deserialization"
Exception during results interpretation: Interpreting query results failed: A fatal error occurred: Could not process query metadata.
Error was: Expected result pattern(s) are not present for query kind "path-problem": Expected between two and four result patterns. [INVALID_RESULT_PATTERNS]
[2021-04-06 15:53:16] Exception caught at top level: Could not process query metadata.
Error was: Expected result pattern(s) are not present for query kind "path-problem": Expected between two and four result patterns. [INVALID_RESULT_PATTERNS]
com.semmle.cli2.bqrs.InterpretCommand.executeSubcommand(InterpretCommand.java:123)
com.semmle.cli2.picocli.SubcommandCommon.executeWithParent(SubcommandCommon.java:414)
com.semmle.cli2.execute.CliServerCommand.lambda$executeSubcommand$0(CliServerCommand.java:67)
com.semmle.cli2.picocli.SubcommandMaker.runMain(SubcommandMaker.java:201)
com.semmle.cli2.execute.CliServerCommand.executeSubcommand(CliServerCommand.java:67)
com.semmle.cli2.picocli.SubcommandCommon.call(SubcommandCommon.java:430)
com.semmle.cli2.picocli.SubcommandMaker.runMain(SubcommandMaker.java:201)
com.semmle.cli2.picocli.SubcommandMaker.runMain(SubcommandMaker.java:209)
com.semmle.cli2.CodeQL.main(CodeQL.java:91)
. Will show raw results instead.
import DataFlow::PathGraph
而不是import semmle.code.java.dataflow.TaintTracking
第二次尝试
/**
* @name Unsafe shiro deserialization
* @kind path-problem
* @id java/unsafe-deserialization
*/import javaimport semmle.code.java.dataflow.DataFlow//import semmle.code.java.dataflow.TaintTrackingimport DataFlow::PathGraph
predicate isDes(Expr arg){
exists(MethodAccess des |
des.getMethod().hasName("deserialize")
and
arg = des.getArgument(0))
}class Myindex extends RefType{
Myindex(){ this.hasQualifiedName("com.summersec.shiroctf.controller", "IndexController")
}
}class MyindexTomenthod extends Method{
MyindexTomenthod(){ this.getDeclaringType().getAnAncestor() instanceof Myindex and
this.hasName("index")
}
}class ShiroUnsafeDeserializationConfig extends TaintTracking::Configuration {
ShiroUnsafeDeserializationConfig() {
this = "ShiroUnsafeDeserializationConfig"
}
override predicate isSource(DataFlow::Node source) {
exists(MyindexTomenthod m | // m.
source.asParameter() = m.getParameter(0)
)
}
override predicate isSink(DataFlow::Node sink) {
exists(Expr arg|
isDes(arg) and
sink.asExpr() = arg /* bytes */
)
}
}
from ShiroUnsafeDeserializationConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink, source, sink, "Unsafe Shiro deserialization"
第三次尝试
RemoteFlowSource
,在翻开博客之后成功解决。后期大佬解释了RemoteFlowSource
的作用,该类考虑了很多种用户输入数据的情况。/**
* @name Unsafe shiro deserialization
* @kind path-problem
* @id java/unsafe-shiro-deserialization
*/import javaimport semmle.code.java.dataflow.FlowSourcesimport DataFlow::PathGraph
predicate isDes(Expr arg){
exists(MethodAccess des |
des.getMethod().hasName("deserialize")
and
arg = des.getArgument(0)
)
}class ShiroUnsafeDeserializationConfig extends TaintTracking::Configuration {
ShiroUnsafeDeserializationConfig() {
this = "StrutsUnsafeDeserializationConfig"
}
override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}
override predicate isSink(DataFlow::Node sink) {
exists(Expr arg|
isDes(arg) and
sink.asExpr() = arg /* bytes */
)
}
}
from ShiroUnsafeDeserializationConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Unsafe shiro deserialization" ,source.getNode(), "this user input"// select sink, source, sink, "Unsafe shiro deserialization" ,source, "this user input"
request->cookies->cookie->bytes
整个路径查询出来。于是我又去Discussion去提问了,Disuccsion334 ,起初我没看懂老外的意思,老外也没有懂我的意思,语言的障碍,下面是对话内容:isAdditionTaintStep
。LogHandler
类是调用了Tools#exeCmd
方法,利用调用该类此特性就可以完成Exploit的编写。利用方式参考一道shiro反序列化题目引发的思考 ,这里就不在赘述。LogHandler源码
:private Object target;private String readLog = "tail accessLog.txt";private String writeLog = "echo /test >> accessLog.txt";public LogHandler() {
}public LogHandler(Object target) { this.target = target;
}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Tools.exeCmd(this.writeLog.replaceAll("/test", (String)args[0])); return method.invoke(this.target, args);
}@Overridepublic String toString() { return Tools.exeCmd(this.readLog);
}
Source
和定位Sink
是本文的重点,在CodeQL规则中也是一个重点。但让规则更加完美处理中间额外污染步骤AdditionalTainStep
也很重要,本文对此并没有涉及。对于小白来说,可能这篇文章还是有点难度,我已经尽可能写小白化了。对于学过CodeQL入门的童鞋应该是刚刚好。https://xz.aliyun.com/t/7789#toc-0
https://summersec.github.io/2021/03/28/CodeQL%20workshop%20for%20Java%20Unsafe%20deserialization%20in%20Apache%20Struts/
https://xz.aliyun.com/t/7789#toc-8
https://0range228.github.io/%E6%B1%A1%E7%82%B9%E5%88%86%E6%9E%90%E7%AE%80%E5%8D%95%E4%BB%8B%E7%BB%8D/
https://github.com/github/securitylab/discussions/334
https://github.com/haby0/mark/blob/master/articles/2021/CodeQL-%E6%95%B0%E6%8D%AE%E6%B5%81%E5%9C%A8Java%E4%B8%AD%E7%9A%84%E4%BD%BF%E7%94%A8.md
- 结尾 - 精彩推荐 【技术分享】后门防御-Neural Cleanse分析及复现 【技术分享】NCTF/NJUPTCTF 2021 部分 WriteUp 【技术分享】2021 蓝帽杯初赛 PWN WriteUp 戳“阅读原文”查看更多内容 原文始发于微信公众号(安全客):【技术分享】从Java反序列化漏洞题看CodeQL数据流
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论