DASCTF X CBCTF 2023 bypassjava 复现
前言
本次比赛其他的不说,web题个人感觉质量还是挺高的(因为做不出来)赛题复现先从java题入手,bypassjava这道题涉及到的知识点挺多,复现下来能学到不少新东西。由于本题是边学边打边复现,所以文章篇幅可能比较长,所以单独写一篇文章来记录这道题的复现过程。
出题人WP:官方WP(真的很顶)
题目附件分析
给的源码,代码并不是很多
常规的反序列化点,只不过自己定义了一个Filter
限制了payload的长度,pom.xml里也没有特别明显能打的依赖
只有Springboot的依赖。
复现过程
jackson原生反序列化
看了出题人师傅的WP才知道可以打jackson原生的反序列化,也算是唤起了我一些记忆。之前复现的2023极客巅峰的babyurl题目就用到了jackson反序列化(链接:babyurl)更早是今年 AliyunCTF 爆出来的新链子。
本来是知道Springboot的依赖自带 jackson 的
固化思维以为 jackson 依赖只能打 Json 反序列化。
简单说一下链子的流程
出发点是jdk自带的 BadAttributeValueExpException 类,它的 readObject 方法可以调任意对象的toString,
接下来就要用到 jackson包里的类了——POJONode
它本身是没有定义 toString 方法的实现,然而追溯到两层继承类,BaseJsonNode
该类重写了 toString 方法。跟进到 writeValueAsString 方法
该方法实现了一种类似于Json序列化的操作,可以继续往下跟进看看,在babyurl那道题已经跟进过了。序列化的时候会调用任意 getter 方法。
而在CC链常用作执行类的 TemplatesImpl 就存在合适的getter方法
重点是这个方法会调用 newTransformer(),最终会走到类加载的那个地方。
实际上链子的流程大概就是这样,但是存在两个细节点需要处理
第一个就是 jackson 链子的不稳定问题,序列化的时候首先会获取javabean的属性然后调用它的getter方法
有时反序列化的时候会优先调用 getStylesheetDOM 方法,由于_dom的属性为null,导致异常链子中断。而且存在缓存,也就是说如果第一次反序列化失败,后面再执行多少次都一样,除非重启环境。因为执行getter方法的顺序不确定,所以也就存在一个不稳定的问题。
源码分析详见出题人另一篇文章:关于java反序列化中jackson链子不稳定问题
解决办法是用一个动态代理类——JdkDynamicAopProxy
它是Springboot 的内置类,Springboot框架有一个显著的特性就是AOP(面向切面编程),JdkDynamicAopProxy就是 Spring AOP 框架的一部分,用于实现切面编程。
让JdkDynamicAopProxy类对 TemplatesImpl 类进行封装或进行代理,设置指定接口,那么在调用getter的时候也会调用我们指定的方法。
重点看JdkDynamicAopProxy类的invoke方法
这三个参数分别为代理对象,方法,以及参数。在120行,对方法进行调用
而target是通过 targetSource 获取
targetSource 是通过本类的 advised 属性赋值
它是一个AdvisedSupport类型
所以要用这个类去封装我们的 TemplatesImpl 对象。
第二个问题就是序列化的时候会发生报错
报错点在这里
这个方法是Java序列化机制的一部分,它是在对象进行序列化时被调用的特殊方法。它的存在可以默认改变它的序列化行为,可能会偏离我们的预期结果。
所以可以通过字节码动态删除 writeReplace 这个方法
绕过长度限制
题目中自己定义了一个Filter,限制了payload的长度,
发送的请求数据首先会被过滤器链处理,最后才会交给server,调试代码看看 getContentLength 是怎么获取的。
checkFacade 方法判断请求是否为null
一路跟进,实际上就与 contentLength 属性有关
查看调用,有对应的setter
继续忘上找,在 Http11Processor 类中
把长度设置为-1不就可以满足长度限制了,满足if条件顺利执行这个方法
contentLength 表示数据包的 Content-Length字段值,自然大于0,查看 contentDelimitation 属性的调用
数据报文的编码格式为 chunked,就设置为True,那么就可以用 chunked 编码绕过getContentLength
注意chunk编码的格式,尾部用大小为0的块标识
成功反序列化
JNI绕过RASP
很离谱,复制代码在idea上起的环境能够写入内存马,用出题人的docker起的环境写不进去。看出题人的WP利用代码执行来目录遍历和读文件,不会整,留个坑。
该题目是开了RASP防护的,RASP 全称为 Runtime Application Self-Protection,实时程序自我保护。RASP 通常嵌入在程序内部,具备实时监控危险函数调用,并阻止该危险调用的功能。与传统 WAF 对比, RASP 实现更为底层,规则制定更为简单,攻击行为识别更为精准。
Java RASP 通常使用 java agent 技术实现,官方WP说可以读文件读到 RASP的jar包内容
ban掉了forkAndExec和loadLibrary0,java的一些执行类,比如ProcessBuilder,Runtime.exec底层会调用forkAndExec,本地加载动态链接库的方法,比如System.loadLibrary或者System.load底层会调用loadLibrary0
禁用掉这两个底层接口方法能够防御几乎所有的命令执行以及大部分绕过
WP说到可以利用反射来调用 java.lang.ClassLoader.NativeLibrary
中的 load
方法来加载恶意so文件执行命令
而NativeLibrary就是 JNI 的一个抽象类。JNI(Java Native Interface)是 Java 提供的一种机制,用于在 Java 程序中调用本地(Native)代码,即使用其他语言(如C、C++)编写的代码,从而可以充分利用本地代码的功能和性能优势,实现对底层系统资源和外部库的访问。
贴一张师傅的图解释JNI的机制
其实看到一些java的方法是Native修饰的,那它就是用底层C去实现的。
利用JNI去实现java程序去调用C程序的五个步骤
1. 定义一个native修饰的方法 |
剩下的就是跟着WP做了
贴个图,也算是复现过了。很奇怪,用burp发包写不进马,用python脚本模拟发包就可以。
知识点总结
重点还是了解JNI机制与RASP的绕过原理,毕竟对于我来说是新东西,后续会进一步学习,其次就是写内存马的时候经常写不进去,还是不太熟练。最后就是复习了一下Jackson的原生反序列化,希望这次不会忘了。
参考链接:
https://www.cnblogs.com/nice0e3/p/14067160.html#0x02-jni%E5%AE%9E%E7%8E%B0