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.

Nói qua về lịch sử của product này thì 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ừ đó có tên Apereo CAS và cũng đổi luôn package name thành org.apereo.cas từ version 5.0 và được tích cực maintain cho đến ngày nay. Dù Jasig CAS EOL từ 2012 nhưng vẫn được nhiều bên tiếp tục sử dụng.

Bài viết này sẽ ghi lại quá trình mình tìm hiểu một số lỗ hổng trên CAS khá nghiêm trọng, trong số đó là 1day chưa được công bố mà mình đã có cơ hội gặp và ứng dụng trong thực tế. 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)

CAS có sử dụng thư viện 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ác factory, repository tương ứng được cấu hình ở cas-servlet.xml, 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

Exposed endpoints:

/statistics/ping
/statistics/threads
/statistics/metrics
/statistics/healthcheck
/statistics/ssosessions
/statistics/ssosessions/**

Lỗ hổng cho phép truy cập các tính năng của admin như phía trên không cần xác thực: threads

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

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

Từ đó các entry này thay vì được tomcat mapping trực tiếp đến các servlets thì sẽ được mapping bởi DispatcherServlet của spring.

Vấn đề là tại securityContext.xml config của spring có RequiresAuthenticationInterceptor được cấu hình để giới hạn liệu các entry /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 bị exposed do không đi qua DispatcherServlet của spring nên cũng không đi qua interceptor được cấu hình cho spring.

bonus easy RCE

Tại file viewConfig.js có khai báo endpoint /cas/status/config/getProperties: viewConfig.js

Dù không được đề cập ở advisory trên nhưng entry này có thể truy cập không cần xác thực và trả về toàn bộ file cấu hình cas.properties - có thể RCE với webflow key trong file trên :)

(mình để tạm ảnh này do 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

Và trong 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ách servlet handle url. 4.2.6-2 Request: req_4.2.6-2

Đội dev cũng đã cấu hình entry dùng 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 (trả về toàn bộ active sessions trên CAS -> session hijacking) dù 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 entry 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 lỗ hổng 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

Theo pull request mình biết sink là 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

refs