前言
学习一下SummerSec师傅的项目:https://github.com/SummerSec/learning-codeql
结合项目中的database,前置:需要了解一下shiro反序列化漏洞的利用链。
本文是学习的第一篇——CodeQL分析Shiro利用链
寻找重点:source & sink
SummerSec师傅的文章中提到,在寻找反序列化漏洞的时候需要先找入口,而入口肯定是可以实现反序列化功能的,那么就说明这个入口类,一定实现了Serializable接口,而且会存在serialVersionUID字段,所以现在需要找的就是一个接口需要实现反序列化接口就行。
结果成功查询出来,找到一个实现了反序列化接口的Class
/*** @id java/example/vuldemo* @name shiro-ctf* @description Shiro-vul* @kind path-problem* @problem.severity warning*/import javafrom Class cls/*条件选择,需要判断是否实现了反序列化接口,还要筛选是否是来自项目源码中的,去除JDK中自带的反序列化接口类的干扰*/where cls.getASupertype() instanceof TypeSerializable andcls.fromSource()select cls
![😶[Java]CodeQL with Shiro-CTF - 图1](/uploads/projects/m0re@demosec/91b231feadfd93135118efb0b013c010.png)
这只是一个接口类,并不是一个实现类,所以需要找到哪里将User实例化了。
/*** @id java/example/vuldemo* @name shiro-ctf* @description Shiro-vul* @kind path-problem* @problem.severity warning*/import javaclass FindUser extends RefType{FindUser(){this.hasQualifiedName("com.summersec.shiroctf.bean", "User")}}from ClassInstanceExpr ciewhere cie.getType() instanceof FindUserselect cie
查询结果如下:
![😶[Java]CodeQL with Shiro-CTF - 图2](/uploads/projects/m0re@demosec/5cae46d8d43a13e2efdf57ac996f9bf4.png)
在IndexController.java中的59行,对User类进行了实例化。
@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";}
关键代码中,如果cookies是null,那么不执行下面的逻辑判断的操作。如果不为null,而且满足c.getName().equals("hacker")就可以执行到cookie.getValue(),进而执行反序列化的操作。如果没有满足c.getName().equals("hacker")条件,那么就会执行序列化的操作。
Tools类中的base64Encode()和serialize()以及deserialize都是Tools中比较关键的方法。接下来就是需要去看Tools的源码是怎么写的这几个方法。
// serialize()public static byte[] serialize(final Object obj) throws Exception {ByteArrayOutputStream btout = new ByteArrayOutputStream();ObjectOutputStream objOut = new ObjectOutputStream(btout);objOut.writeObject(obj);return btout.toByteArray();}// Base64Encode(byte[] xxxx)public static String base64Encode(byte[] bytes) {Base64.Encoder encoder = Base64.getEncoder();return encoder.encodeToString(bytes);}// exeCmd() 执行系统命令// deserialize()public static Object deserialize(final byte[] serialized) throws Exception {ByteArrayInputStream btin = new ByteArrayInputStream(serialized);ObjectInputStream objIn = new ObjectInputStream(btin);return objIn.readObject();}
比如反序列化的方法可以这么来找
class Deserialize extends RefType{Deserialize(){this.hasQualifiedName("com.summersec.shiroctf.Tools", "Tools")}}class DeserializeMethod extends Method{DeserializeMethod(){this.getDeclaringType() instanceof Deserializeand this.hasName("deserialize")}}from DeserializeMethod dmselect dm
![😶[Java]CodeQL with Shiro-CTF - 图3](/uploads/projects/m0re@demosec/ac3ac98045b4b85da79f89f87da4e316.png)
然后找到exeCmd,这个可以执行命令的点,可以放一下,待会重新计划规则来找链子
from MethodAccess methodwhere method.getMethod().hasName("exeCmd")select method
可以发现在LogHandler.java两处方法调用了exeCmd()
![😶[Java]CodeQL with Shiro-CTF - 图4](/uploads/projects/m0re@demosec/81e454e63c07ec55593e36dc00ee60ca.png)
寻找利用链
完整规则
/*** @id java/example/vuldemo* @name shiro-ctf* @description Shiro-vul* @kind path-problem* @problem.severity warning*/import javaimport semmle.code.java.dataflow.FlowSourcesimport semmle.code.java.security.QueryInjectionimport DataFlow::PathGraphclass ShiroCtf extends TaintTracking::Configuration{ShiroCtf(){this="Shiro-CTF"}override predicate isSource(DataFlow::Node source) {source instanceof RemoteFlowSource}override predicate isSink(DataFlow::Node sink) {exists(Expr arg|isDes(arg) andsink.asExpr() = arg)}}predicate isDes(Expr arg){exists(MethodAccess des |des.getMethod().hasName("deserialize")andarg = des.getArgument(0))}from ShiroCtf config, DataFlow::PathNode source, DataFlow::PathNode sinkwhere config.hasFlowPath(source, sink)select sink.getNode(), source, sink, "Unsafe shiro deserialization" ,source.getNode(), "this user input"
执行结果
![😶[Java]CodeQL with Shiro-CTF - 图5](/uploads/projects/m0re@demosec/ecc874c6ddaa59d817f6ac1be44e8759.png)
source是这里,这里是SummerSec师傅在GitHub Securitylab discussions提出的问题之一,想要让source的点从request开始显示
![😶[Java]CodeQL with Shiro-CTF - 图6](/uploads/projects/m0re@demosec/8148012d978fc3e067ba5bf9769869a1.png)
实际上师傅的意思也就是在这里
![😶[Java]CodeQL with Shiro-CTF - 图7](/uploads/projects/m0re@demosec/0bfa6f6be185ba92f40dd668adeed509.png)
规则中isSource使用了RemoteFlowSource,其实这个已经包含了大多数情况中的用户输入。
直接在isSource点击"Quick Evaluation",就可以看到所有的用户输入内容。
![😶[Java]CodeQL with Shiro-CTF - 图8](/uploads/projects/m0re@demosec/bf13366c37539d0476c6628f6cf35bc6.png)
这里本想截图的,可是窗口太小了,无法放下,就直接把这部分代码贴过来吧。
@GetMapping({"/index/{name}"})public String index(HttpServletRequest request, HttpServletResponse response, @PathVariable String name)
这里的name才是真正的用户输入,而不是request,所以想要直接获取到request--cookies这个阶段的数据流应该是不行了。因为使用了RemoteFlowSource不会把request当作一个用户输入的。
SummerSec师傅最后也得到了答案,也就是需要增加一个谓词isAdditionTaintStep来连接这个中断的数据流。
不过确实,没什么必要,只不过是为了更好看而已。codeql可以直接判断getValue()为用户输入。
最后说一下exeCmd的这个利用方式,这个好像不太行的,因为执行命令中间涉及到了第三方包,在一开始创建数据库的时候是没有加载进来的。所以codeql无法跟踪到其中的一些调用关系。
Gadget:Tools.base64Decode()Tools.deserialize()ObjectInputStream.readObject()BadAttributeValueExpException.readObject()LogHandler.toSting()Tools.exeCmd()
我没去debug,这个链子是直接拉取SummerSec师傅的。从中可以看出,污点在经过这两个节点的时候已经超出了项目源码的范畴。ObjectInputStream、BadAttributeValueExpException
所以是无法这样来分析这条链子的。这样的链子要去挖掘的话,是需要将第三方包进行反编译并生成ql数据库的,再去寻找。有些繁琐,本人技术有限,同时也有点懒,不想去找。
