Bypass Gitlab Auth bằng ma thuật hắc ám
Cuối tháng 2, đầu tháng 3 vừa qua có lẽ là một thời gian khá khó khăn với những anh em chuyên làm về SAML, khi mà một loạt bug mới, kỹ thuật mới được công bố có liên quan tới protocol này:
- https://repzret.blogspot.com/2025/02/abusing-libxml2-quirks-to-bypass-saml.html (CVE-2025–23369)
- https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/ (CVE-2025–25291 + CVE-2025–25292)
- https://portswigger.net/research/saml-roulette-the-hacker-always-wins (CVE-2025–25291 + CVE-2025–25292 but with more detail)
- https://workos.com/blog/samlstorm (CVE-2025–29775 +CVE-2025–29774)
Điều đó đã thôi thúc sự tò mò của mình và đành giành chút thời gian xem xét một trong số đó, ở đây mình chọn target gitlab với CVE-2025–25291 + CVE-2025–25292.
- note: this blog post has nothing new than the one from portswigger, if you need more details, just read that one instead. This one is just a quick note about my approach during the analysis.
# Lab setup
Target version: Gitlab EE 17.8.4
Gitlab domain: gitlab.lab.local
Idp: ở đây mình sử dụng Okta trial cho dễ setup
Bước đầu tiên là setup Okta trial trước, vào phần Applications > Create App Integration > chọn SAML 2.0
App name: set tùy ý, ví dụ: Gitlab xádasdasd, Next
Tại cửa sổ Configure SAML tiếp theo, cần lưu ý các tham số như sau:
- Single sign-on URL: có dạng https://<gitlab server>/users/auth/saml/callback
- Audience URI: https://<gitlab server>
- Name ID format: Persistent
- Application username: Okta username
Tại mục Attribute Statements:
- email -> user.email
- firstname -> user.firstName
- lastname -> user.lastName
Cửa sổ tiếp theo, chọn “This is an internal app …”
Quay trở về tab Sign On, note lại các thông tin sau: Sign on URL và cert fingerprint
Sau khi đã setup xong okta, tiếp theo là tới gitlab, bạn đọc có thể sử dụng docker-compose template dưới đây để setup nhanh hơn: https://gist.github.com/testanull/fc6ac56e5352e02d91bb5cb358261554
Với các tham số idp_cert_fingerprint, hostname, idp_cert_fingerprint hãy chỉnh lại cho match với môi trường của bạn!
Nếu setup thành công, bạn có thể sẽ thấy nút “Okta login” ở trang chủ:
# How SAML works (basically)
Để bắt đầu thì chúng ta hãy xem qua quá trình SAML authenticate diễn ra như nào:
Đa phần các lỗ hổng liên quan tới SAML hiện tại đều tập trung vào bước số 5, 6, 7.
Khi quá trình auth kết thúc, phía Idp (Okta) sẽ gửi về cho user một chuỗi, đc gọi là SAMLResponse, sau đó browser sẽ gửi chuỗi này tới Service Provider (gitlab), kết quả auth của phiên sẽ được quyết định tại đây.
SAMLResponse có dạng như sau:
Q: Với tư duy của một attacker, chúng ta sẽ target vào phần “NameID” để thay đổi thành user khác phải không?
Để ngăn chặn điều này, một cơ chế hash verify đã được dựng lên, DigestValue nằm trong SignedInfo/Reference chính là giá trị hash của cả thẻ <Assertion> sau khi đã được chuẩn hóa (canonical).
Khi server nhận được SAMLResponse, tag Assertion (ngoại trừ tag Signature) sẽ được chuẩn hóa, và hash rồi so sánh với DigestValue.
Q: Rồi nhỡ mình modify cái DigestValue này luôn được không?
A: Câu trả lời vẫn là không!
DigestValue nằm trong SignedInfo/Reference, cả element SignedInfo cũng sẽ được chuẩn hóa và Sign với Certificate nằm trong KeyInfo/X509Data/X509Certificate, giá trị này sẽ được lưu tại tag <SignatureValue>. Và Certificate này bắt buộc phải có fingerprint match với fingerprint đã được config trên server!
=> Đảm bảo được tính toàn vẹn của toàn bộ SAMLResponse
Like a safe inside another safe ;)
# Confuse ruby xml parser with round trip attack
Rồi tiếp tục một câu hỏi nữa bây giờ là:
Q: Với cơ chế kiểm tra toàn vẹn chắc chắn như vậy, làm sao mà vẫn có rất nhiều bug ở chỗ này?
Mình chưa thể trả lời hết trong một bài này, nhưng mình sẽ lấy một ví dụ về bug gần đây của gitlab: CVE-2025–25291 + CVE-2025–25292
Theo như mình phân tích, bug này tận dụng một biến thể khác của lỗ hổng “XML round-trip”, tồn tại trên thư viện REXML của ruby. Mà REXML lại được sử dụng bởi ruby-saml (một thư viện phổ biến để xử lý saml auth trên ruby), ruby-saml lại được dùng bởi omniauth-saml, thư viện xử lý saml auth trên gitlab.
Sơ đồ quan hệ của nó như vầy nè:
rexml/document (vulnerable)
> ruby-saml
> omniauth-saml
Do code base của ruby saml sử dụng rexml là chủ yếu, nên khi rexml có lỗi, toàn bộ các thư viện phía sau đều bị ảnh hưởng hết! Do vậy, ta sẽ đi vào làm rõ lỗ hổng round-trip này trước!
Về lỗ hổng round-trip này, mình sử dụng payload của portswigger để demo:
Tại dòng số #3, một Document doc_1 được khởi tạo bởi REXML::Document,
Tiếp tục sau đó tại dòng #13, một Document thứ 2 cũng được tạo bởi REXML::Document, nhưng với XML được lấy từ doc_1.to_s. Nghĩa là được lấy từ doc_1 sau khi đã convert ngược lại sang xml string.
Và kết quả khi chạy sẽ như sau:
Với “cùng một xml document” mà lại có kết quả trả về khác nhau?
Không hẳn là như vậy, kết quả của doc_1.to_s trả về như sau:
Dấu single quote '
đã được thay bằng dấu double quote "
,
Như vậy, tại round 1, x" ><! —
được coi là một thành phần riêng biệt:
Tới round 2, do tất cả dấu single quote đã được thay bằng dấu double quote, nên thành phần này chỉ còn lại x
Kết hợp với các block comment được chèn vào vị trí thích hợp, ta có thể spoof với mỗi lần là một kết quả parse khác nhau, giống như mấy cái trò magic card trick vậy!
Q: Tại sao điều này lại quan trọng?
Quay trở lại với ruby-saml, thư viện này thực hiện validate SAMLResponse tại response.rb#L78
Response::is_valid()
> validate()
> ....
> Response::validate_signature()
> Document::validate_document()
> Document::validate_signature()
Tại Document::validate_signature, ta có thể thấy một REXML::Document mới được tạo từ self.to_s. Và tất cả quá trình validate signature phía sau đều dựa trên cái working_copy này.
=> Kết hợp với lỗi round-trip phía trên, ta có thể build được một SAMLResponse để khi validate signature sẽ dùng Response gốc, còn khi sử dụng thực tế thì lại là Response đã bị fake:
<!DOCTYPE xml SYSTEM 'x" ><!--'>
<Response>
<Assertion>fake assertion</Assertion>
<![CDATA[-->
<Response>
<Assertion>real assertion</Assertion>
<!--]]>--></Response>
Tới đây thì mình đã có thể build payload thành công trên gitlab 17.8.4-ee để impersonate được user bất kỳ rồi.
Điều kiện cần của exploit này đó là credentials của một user bất kỳ, không hoàn toàn là auth bypass ¯\_(ツ)_/¯
PoC: https://gist.github.com/testanull/56b844828ed01f4626dbb7cbde7ecd71
PoC video:
Thanks for reading!