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
Hàm này sẽ gọi đến expandAtFiles()
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 theo ra biến phía sau -> read được content file.
Ở đâ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
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/, 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.
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
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.
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
Ở 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 đề
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.
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
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
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.
Giờ ta sẽ retrieve từng dòng và ghép binary lại.
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.
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
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, và tại sao lại thay thế 3F
tại nhiều nguồn khác.
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.
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 :Đ.
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
Khi decrypt, tool sẽ báo lỗi
Khi đổi thành 8F
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()
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()
Tại onLoginSuccess()
, sẽ nhảy vào makeTokenSignature()
để tạo signatureValue, đây có vẻ như là nơi ta cần
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
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()
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()
Sau đó vào hàm getKey()
Nhảy vào load()
Tại load()
, master key và content từ file <jenkins>/secrets/org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac
được đưa vào hàm verifyMagic()
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.
Tiếp tục trace dần theo doFinal()
của hàm mac()
Sau một loạt xử lí, dừng lại ở Util.toHexString()
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()
.
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
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
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
Trong file này đã 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
Việc retreive ta có thể làm tương tự với thằng hudson ở phần trên
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
Cuối cùng là master.key và file binary vừa retreive trên để khởi tạo mac
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
Auto login thành admin thành công.
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