# Some Attack Vector in Jenkins Data Leak Vulnerability (CVE-2024-23897) ## Introduction Jenkins là một phần mềm tự động hóa, mã nguồn mở và viết bằng Java giúp tự động hóa các quy trình trong phát triển phần mềm, hiện nay được gọi theo thuật ngữ Tích hợp liên tục, và còn được dùng đến trong việc Phân phối liên tục. Jenkins là một phần mềm dạng server, chạy trên nền servlet với sự hỗ trợ của Apache Tomcat. Nó hỗ trợ hầu hết các phần mềm quản lý mã nguồn phổ biến hiện nay như Git, Subversion, Mercurial, ClearCase... Jenkins cũng hỗ trợ cả các mã lệnh của Shell và Windows Batch, đồng thời còn chạy được các mã lệnh của Apache Ant, Maven, Gradle... ## CVE-2024-23897 Đầu năm 2024 nay, Jenkins đã tung ra Security Advisory cho mã lỗi CVE-2024-23897 với mô tả có thể đọc file bất kì qua CLI và có nguy cơ dẫn đến RCE. Lỗ hổng này tồn tại Jenkins ở version <= 2.441 và <= LTS 2.426.2 Cho đến thời điểm hiện tại, PoC hay các bài phân tích của CVE này đã đầy rẫy trên các nền tảng Twitter hay Github, nhưng hầu hết chỉ đề cập đến việc đọc file, một nửa còn lại là tấn công sâu hơn từ việc đọc file thì chưa được đề cập nhiều nên bài hôm nay mình sẽ giới thiệu một số hướng khai thác sâu hơn từ việc đọc file trên. ### Quick Review Về root cause của bug thì không thiếu các bài phân tích đã nói, ở đây mình sẽ nhắc lại một tí. Nguyên nhân chính là vì Jenkins CLI có sử dụng hàm `parseArgument()` của class bên thứ ba `org.kohsuke.args4j.CmdLineParser` ![image](https://hackmd.io/_uploads/BkbJObR7C.png) Hàm này sẽ gọi đến `expandAtFiles()` ![image](https://hackmd.io/_uploads/HJJtubC70.png) ![image](https://hackmd.io/_uploads/HkdYO-0XR.png) `expandAtFiles()` sẽ nhận dữ liệu từ người dùng, nếu có `@` thì nó sẽ thực hiện đọc content của file theo path file đi theo sau `@` từ đó expand nội dung file thành biến phía sau -> **read được content file.** ![image](https://hackmd.io/_uploads/Hy6X5-CXR.png) Ở đây ta chỉ có thể dùng một số hàm CLI để khai thác read file nếu Jenkins đang setting default (và chỉ hạn chế đọc được vài ba dòng đầu của file), tuy nhiên nếu có account có quyền Read, hoặc Jenkins đang setting ở cho phép đăng kí, Anyone can do anything hay cho phép unauth có quyền Read thì ta có thể dùng một số hàm CLI khác để đọc full content file, ví dụ `connect-node` ![image](https://hackmd.io/_uploads/Sk4T2ZAQA.png) # Attack time Mình vừa review sơ về root cause của bug này, giờ mình sẽ tiến hành phân tích 2 trong số các hướng tấn công có thể đạt được thông qua việc đọc được file. ## Setup Có thể build Jenkins bằng file `jenkins.war` với version bất kì được tải từ trang chủ Jenkins tại [https://get.jenkins.io/war/](https://get.jenkins.io/war/), trong bài này mình dùng ver 2.441. Thực hiện deploy và bật port debug bằng câu lệnh ``` java -agentlib:jdwp=transport=dt_socket,address=*:5005,server=y,suspend=n -jar jenkins.war ``` Để thuận tiện cho việc phân tích, mình sẽ build trên môi trường Windows, về lí do thì mình sẽ giải thích ở phần dưới. ## Extracting credentials Jenkins có một plugins là Credentials, dùng để lưu trữ các thông tin đăng nhập theo Domain, các site 3rd party, các storage lưu trữ,... Các thông tin đăng nhập này sẽ được dùng vào các dự án Pipeline khi tương tác với các bên thứ ba. Nếu những thông tin này bị trích xuất ra, những account hay credential dự án/app bên thứ ba sẽ bị lộ lọt, mở đường tấn công vào các hệ thống này. ![image](https://hackmd.io/_uploads/Bkz6pZAmC.png) Những thông tin về Credentials này trong đó quan trọng nhất là password sẽ được encrypt và lưu lại trong file `<jenkins>/credentials.xml`, lợi dụng bug ta có thể đọc được file ![image](https://hackmd.io/_uploads/r1vXxMR70.png) Dễ thấy được username cũng như password được encrypt, giờ công việc là làm sao decrypt được password này. Bài toán này cũng đã xuất hiện rất nhiều trước đây, nếu search google ta cũng có thể thấy nhiều bài phân tích làm sao để giải mã chỗ này, thậm chí có cả tool, rất đỡ phải chui vào code và phân tích. ![image](https://hackmd.io/_uploads/SyxKxfA7R.png) ![image](https://hackmd.io/_uploads/BJnsxzCQA.png) Dựa trên nhiều nguồn khác nhau, có thể rút được điều kiện để decrypt là có `hudson.util.Secret` và dùng master key. Binary của class `hudson.util.Secret` được lưu tại `<jenkins>/secrets/hudson.util.Secret` và key tại `<jenkins>/secrets/master.key`, nên ta hoàn toàn có thể đọc và sử dụng để decrypt. Thử dùng file từ chính server để test với tool [jenkins-credentials-decryptor](https://github.com/hoto/jenkins-credentials-decryptor) ![image](https://hackmd.io/_uploads/SktPkoxV0.png) Ở ngữ cảnh tấn công, việc đọc `master.key` thì không bàn tới, nhưng việc đọc được file `hudson.util.Secret` dưới dạng đúng và làm sao cho sử dụng được lại là một vấn đề ![image](https://hackmd.io/_uploads/r1zK_ix4R.png) ![image](https://hackmd.io/_uploads/HJ6omM0Q0.png) Theo như Security Advisory của CVE cũng đã nói, việc trích xuất binary của file này còn phụ thuộc nhiều yếu tố trong đó có OS. Windows với encoding Windows-1252 sẽ retrieve được nhiều byte hơn so với UTF-8 trên Linux hay MAC OS. ![image](https://hackmd.io/_uploads/H19r7fCXA.png) Trong quá trình phân tích, ngoài OS, phiên bản Java dùng để chạy Jenkins CLI cũng phần nào đó ảnh hưởng lên output của file, Java bằng hoặc cũ hơn so với Jenkins đang chạy sẽ có phần trăm retrieve được đúng byte hơn, ví dụ với Java 11 và Java 18 ![image](https://hackmd.io/_uploads/H1nbVGC7A.png) ![image](https://hackmd.io/_uploads/B1vtVM0mA.png) Vậy là câu hỏi được đề cập ở bước Setup đã được giải đáp, Windows sẽ dễ để khai thác bug này hơn. Vấn đề thứ hai của việc retrieve content file, nếu file có nhiều dấu xuống dòng thì thứ tự của các dòng sẽ bị xáo trộn ![image](https://hackmd.io/_uploads/HJpv8MAmA.png) Vấn đề này có thể giải quyết bằng cách dùng nhiều function Jenkins CLI khác nhau, các bài phân tích CVE này ở việc đọc file trước đó, người ta đã chỉ ra các function sẽ lấy được chính xác dòng 1, dòng 2 và dòng 3. ![image](https://hackmd.io/_uploads/S1NePGRX0.png) Giờ ta sẽ retrieve từng dòng và ghép binary lại. ![image](https://hackmd.io/_uploads/rkIAdGAXA.png) Binary của từng file ta đọc ra chỉ giữ lại phần cần thiết, xoá đi các phần in lỗi, mình dùng HxD hỗ trợ việc này. ![image](https://hackmd.io/_uploads/SyB4box4A.png) Sau khi ghép xong, vì binary đã bị xuống dòng nên ta sẽ phân chia nó bởi 0A hoặc 0D ![image](https://hackmd.io/_uploads/r1xdtboeNC.png) Tiếp theo, ta phải thay đổi tất cả byte `3F` thành một trong các byte 81, 8D, 8F, 90, hoặc 9D. Đây là những byte mà Encoding Windows-1252 không đọc được. Nguyên nhân các byte này Windows không thể đọc được ta có thể tham khảo Codepage layout của Encoding Windows-1252 tại [đây](https://en.wikipedia.org/wiki/Windows-1252), và tại sao lại thay thế `3F` tại nhiều nguồn khác. ![image](https://hackmd.io/_uploads/ByFrvjxER.png) ![image](https://hackmd.io/_uploads/rJNxvcxVA.png) Trong trường hợp của mình, khi thay đổi hết sang `9D`, file hudson đã có thể decrypt thành công. ![image](https://hackmd.io/_uploads/SyGEXseV0.png) ![image](https://hackmd.io/_uploads/H1R4Xjx40.png) So sánh với hudson ở trên server, vẫn có khá nhiều điểm khác biệt như server sẽ dùng `0D` để ngắt hàng, các byte không được in vẫn có vị trí dùng `90`, `8D`,... Tuy nhiên file của ta vẫn hoạt động mượt :Đ. ![image](https://hackmd.io/_uploads/rJdHIolEC.png) *Note*: Ở một số trường hợp, sẽ tồn tại ít nhất 1 byte bị sai ra. Nhưng ở những trường hợp như vậy, khi brute force dần thì nếu byte không đúng, tool hoặc các script decrypt sẽ báo lỗi luôn thay vì là có thể in ra password nhưng sai format. Ví dụ trường hợp khác thay đổi tất cả thành `9D` ![image](https://hackmd.io/_uploads/rypXEjg4R.png) Khi decrypt, tool sẽ báo lỗi ![image](https://hackmd.io/_uploads/BkMH4ilNA.png) Khi đổi thành `8F` ![image](https://hackmd.io/_uploads/Sy0dVjxN0.png) Mặc dù password không được decrypt thành công, nhưng nó không báo lỗi, từ đó ta có thể brute force lại các byte tiếp, vì range brute force cũng rất ít nên thời gian cũng sẽ rất nhanh chóng. Vậy là xong phần Credentials, giờ ta sẽ đến một Attack Vector khác có phần thú vị hơn. ## Forging Remember-me Cookie Nếu phía trên chỉ để trích xuất thông tin của bên thứ ba, thì hướng khai thác này sẽ ảnh hưởng trực tiếp lên server Jenkins cụ thể hơn là Admin account. Với Admin account, ta có thể có được tất cả info về project, pipeline,... mọi thứ trong Jenkins và còn có thể chạy lệnh hệ thống thoải mái, cộng thêm chức năng remember me khi đăng nhập được bật default, nên đây là một hướng tấn công có phần vjp pr0 hơn. Để forge được thì ta cần phải biết Jenkins xử lí phần remember cookie này ra sao, sau khi search source theo keyword `rememberme` thì tại class `AbstractRememberMeServices` sẽ xử lí từ đầu cho phần remember cookie, đặt breakpoint tại `rememberMeRequested()` ![image](https://hackmd.io/_uploads/BkfL2jxER.png) Thực hiện đăng nhập với options Keep me signed in để nhảy vào breakpoint, đầu tiên nó sẽ nhảy về hàm cha `onLoginSuccess()` ![image](https://hackmd.io/_uploads/rypgpieNA.png) Tại `onLoginSuccess()`, sẽ nhảy vào `makeTokenSignature()` để tạo signatureValue, đây có vẻ như là nơi ta cần ![image](https://hackmd.io/_uploads/B1Z_12lE0.png) `makeTokenSignature()` sẽ tạo một String token là chuỗi kết hợp giữa username, userSeed, một timestamp hết hạn token và secret key ![image](https://hackmd.io/_uploads/Bkgcy3gN0.png) userSeed ở đây ta chưa biết nó là cái gì, tạm note lại hiện tại những thứ cần thiết để tạo cookie là: - userSeed - secret.key Tiếp tục đi vào `Mac.mac(token)`, hàm này đầu tiên sẽ thực hiện `getBytes()` token trên sau đó đưa vào hàm `mac()` ![image](https://hackmd.io/_uploads/Syo1l3gE0.png) ![image](https://hackmd.io/_uploads/B17vg3lVC.png) Hàm `mac()` này đầu tiên thực hiện kiểm tra HMACConfidentialKey có tồn tại hay chưa, chưa thì sẽ tạo mới, không thì sẽ đi vào `doFinal()`, ở đây với lần đầu ta dùng chức năng remember me thì nó sẽ vào hàm `createMac()` ![image](https://hackmd.io/_uploads/S1PtM6e4C.png) Sau đó vào hàm `getKey()` ![image](https://hackmd.io/_uploads/B12sGaeER.png) Nhảy vào `load()` Tại `load()`, master key và đặc biệt là content từ file `<jenkins>/secrets/org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac` được đưa vào hàm `verifyMagic()` ![image](https://hackmd.io/_uploads/ryaSXpeE0.png) ![image](https://hackmd.io/_uploads/SyHqmagEA.png) ![image](https://hackmd.io/_uploads/rJfTmaxNA.png) Vậy điều kiện lúc này - userSeed - secret.key - org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac - master key Đoạn xử lí trên sẽ return key `var7` và quay về hàm cha `getKey()`, key `var7` này sẽ là `SecretKeySpec` để tạo mac. ![image](https://hackmd.io/_uploads/SkOeBalEA.png) Tiếp tục trace dần theo `doFinal()` của hàm `mac()` ![image](https://hackmd.io/_uploads/H1QvHTlN0.png) Sau một loạt xử lí, dừng lại ở `Util.toHexString()` ![image](https://hackmd.io/_uploads/r1xXqTg4R.png) Nó sẽ lấy return của `doFinal()` cho đi qua một loop xử lí để tạo một String `buf`, đây chính là giá trị `signatureValue` của hàm `onLoginSuccess()`. ![image](https://hackmd.io/_uploads/HyTpqagEC.png) Cookie lúc này sẽ là base64 của chuỗi kết hợp giữa username, timestamp hết hạn và signatureValue trên ![image](https://hackmd.io/_uploads/BJIEs6lVC.png) Vậy, để khởi tạo được remeber me cookie, nguyên liệu sẽ là như sau - userSeed - secret.key - org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac, từ file này ta sẽ khởi tạo SecretKeySpec cho mac để thực hiện qua bước `doFinal()` -> **signatureValue** - master key Ta còn lấn cấn 2 chỗ file mac và userSeed, với userSeed thì ta có thể get từ việc đọc file tương tự với 2 cái key kia, cụ thể là Jenkins có lưu trữ các thông tin về user tại `<jenkins>/users/users.xml` ![image](https://hackmd.io/_uploads/Hy9T26xNA.png) Trong file này sẽ có thông tin về các directory lưu trữ thông tin của từng người dùng, ở đây ta muốn lấy thông tin của admin nên sẽ truy cập vào và đọc file `config.xml` ![image](https://hackmd.io/_uploads/HkcSTalVC.png) Đã có userSeed, ngoài ra nó còn có password hash của từng user. Còn với file mac, đây cũng là một file binary, tuy nhiên nó khá là dễ thở vì phần content khá ngắn ![image](https://hackmd.io/_uploads/H1gS06lVR.png) Việc retreive ta có thể làm tương tự với thằng hudson ở phần trên ![image](https://hackmd.io/_uploads/SkMs1CeEC.png) Giờ ta có thể viết 1 script nho nhỏ để tạo `signatureValue`, đầu tiên là chuỗi token gồm username, timestamp, userseed và secret key ![image](https://hackmd.io/_uploads/rJxze0x4C.png) Cuối cùng là master.key và mac binary để khởi tạo mac ![image](https://hackmd.io/_uploads/ByvdxAxE0.png) Các code xử lí ta có thể tận dụng trực tiếp theo lib của Jenkins tuỳ theo version server sử dụng. Script viết vội của mình tại [đây](https://gist.github.com/mtiennnnn/551b7320c064db02aad815c6bdb91d9c) ![image](https://hackmd.io/_uploads/HJQGH0x4R.png) ![image](https://hackmd.io/_uploads/SJNUrCeNR.png) Auto login admin thành công. ![image](https://hackmd.io/_uploads/SJh2S0xVC.png) # Remediation Cập nhật lên phiên bản Jenkins mới nhất hoặc vô hiệu Jenkins CLI để đảm bảo an toàn bảo mật. # Tham khảo - https://www.sonarsource.com/blog/excessive-expansion-uncovering-critical-security-vulnerabilities-in-jenkins - https://www.jenkins.io/security/advisory/2024-01-24/#binary-files-note - https://www.errno.fr/bruteforcing_CVE-2024-23897.html - https://devops.stackexchange.com/questions/2191/how-to-decrypt-jenkins-passwords-from-credentials-xml - https://en.wikipedia.org/wiki/Windows-1252 - https://stackoverflow.com/questions/38896686/urlencoding-form-data-with-windows-1252-charset-in-node-js - https://github.com/hoto/jenkins-credentials-decryptor