Skip to content

Instantly share code, notes, and snippets.

@emboss
Last active August 29, 2015 14:00
Show Gist options
  • Save emboss/91696b56cd227c8a0c13 to your computer and use it in GitHub Desktop.
Save emboss/91696b56cd227c8a0c13 to your computer and use it in GitHub Desktop.
castealer.rb analysis
# Original castealer.rb with analysis
#####################################################################################
require 'rubygems' #redundant
require 'openssl'
require 'digest/md5' #redundant
key = OpenSSL::PKey::RSA.new(2048)
cipher = OpenSSL::Cipher::AES.new(256, :CBC) #redundant
ctx = OpenSSL::SSL::SSLContext.new #redundant
puts "Spoof must be in DER format and saved as root.cer"
raw = File.read "root.cer"
cert = OpenSSL::X509::Certificate.new raw
# The following lines are redundant. What this code does is it
# creates a copy of the original certificate.
# 'ef.issuer_certificate' is what's going to be signed below,
# it would be equivalent to simply sign 'certificate' instead.
# IMHO it doesn't matter if the certificate is changed or not
# in any way for what is to follow.
cert.version = 2
ef = OpenSSL::X509::ExtensionFactory.new
ef.issuer_certificate = OpenSSL::X509::Certificate.new raw
cert.subject = ef.issuer_certificate.subject
ef.subject_certificate = ef.issuer_certificate
cert.issuer = ef.issuer_certificate.issuer
cert.serial = ef.issuer_certificate.serial
ctx.key = ef.issuer_certificate.public_key
cert.public_key = ef.issuer_certificate.public_key
cert.not_after = ef.issuer_certificate.not_after
cert.not_before = ef.issuer_certificate.not_before
cert.extensions = ef.issuer_certificate.extensions
# Note that two distinct keys are written to the file 'root.key'.
# The first one is the original certificate's public key, the
# second one is the public/private key pair of the newly created
# RSA key. The resulting file looks like this:
#
#-----BEGIN PUBLIC KEY-----
#<original certificate's public key>
#-----END PUBLIC KEY-----
#-----BEGIN RSA PRIVATE KEY-----
#<public private key/pair of new RSA key>
#-----END RSA PRIVATE KEY-----
#
# 'spoof' and 'key' both represent the same key
a = File.open("root"".key", "w")
a.syswrite("#{cert.public_key}") # 'syswrite' or normal 'write', no difference
a.syswrite("#{key.to_pem}") # 'syswrite' or normal 'write', no difference
# This simply reloads the new RSA key, cf. 'keys.rb' for proof.
# The certificate's original public key is simply ignored.
spoof = OpenSSL::PKey::RSA.new File.read 'root.key'
printf "Verifying Keys Work: "
puts spoof.private? # true (it's the new key, after all)
ctx.cert = ef.issuer_certificate #redundant
puts "============================================================="
# This effectively resigns the original certificate with the new key
root = ef.issuer_certificate.sign(spoof, OpenSSL::Digest::SHA1.new)
# Not important, just saving objects involved in files
filer = File.open("#{cert.serial}"".key", "w")
filer.syswrite("#{spoof.to_pem}")
file = File.open("spoof"".cer", "w")
file.syswrite("#{cert.to_der}")
files = File.open("#{cert.serial}"".pem", "w")
files.syswrite("#{cert.to_pem}")
files.syswrite("#{spoof.to_pem}")
puts "Hijacked Certificate with chainloaded key saved @ #{cert.serial}.pem"
printf "Verifying Keys Intergity: "
# This prints 'true', but this is expected behavior. We resigned the certificate
# with the new key, and all that X509Certificate#verify does is to check whether
# a certificate's signature is valid given a certain private key. It *does not*
# check whether the private key actually matches the public key information which
# is part of the certificate itself.
# While it certainly is surprising at first that X509Certificate#verify reports
# "true" in this case, this is still correct behavior. Since we resigned the
# certificate, it faithfully reports that the signature is correct. While the
# signature value matches, it is true that the public key referenced in the
# certificate and the private key used for issuing the new signature do not. But
# this is not a security-critical issue IMO. A certificate resigned this way
# cannot be used for anything meaningful unless you tricked somebody into explicitly
# trusting it. But such an attack is clearly out of scope concerning the
# responsibilities of Ruby OpenSSL as a library.
# In fact, OpenSSL itself doesn't check whether keys match for root certificates.
# We trust root certificates merely by possession (i.e. they are part of our
# preconfigured 'trust stores').
# Everybody could generate a valid self-signed root
# certifcate, whose data may even match exactly that of a real CA root certificate.
# But they are not included in any trust store (browser, OS, etc.) and thus useless
# unless you explicitly/actively *want to* trust such a certificate.
# If we spoofed an intermediate or end entity certificate in this way, it wouldn't
# help either. Because the signature would no longer be accepted during the PKIX
# validation process (where the signature of the spoofed certificate would be checked
# against the public key of the supposed issuing certificate and consequently fail).
puts root.verify(key)
# Second, simplified PoC with analysis
#####################################################################################
require 'openssl'
raw = File.read "root.cer"
b = OpenSSL::PKey::RSA.new File.read 'root.key'
cert = OpenSSL::X509::Certificate.new raw
# The following lines are redundant regarding the final
# outcome
issuer_certificate = OpenSSL::X509::Certificate.new raw
cert.subject = issuer_certificate.subject
subject_certificate = issuer_certificate
cert.issuer = issuer_certificate.issuer
cert.serial = issuer_certificate.serial
cert.public_key = issuer_certificate.public_key
cert.not_after = issuer_certificate.not_after
cert.not_before = issuer_certificate.not_before
cert.extensions = issuer_certificate.extensions
# Again, this just reloads 'b', as in the original PoC.
# Cf. 'keys.rb' for proof.
a = File.open("spoof"".key", "w")
a.syswrite("#{cert.public_key}")
a.syswrite("#{b.to_pem}")
# 'spoof' is the same as 'b'
spoof = OpenSSL::PKey::RSA.new File.read 'spoof.key'
# The certificate is resigned, so the same considerations
# apply as for the original PoC.
root = cert.sign(spoof, OpenSSL::Digest::SHA1.new)
file = File.open("spoof"".cer", "w")
file.syswrite("#{root.to_der}")
file.syswrite("#{cert.to_der}")
printf "Check spoof.cer"
# keys.rb: Proof that mixing the original certificate public key with a newly generated RSA
# key in one file actually yields only the new RSA key when reloaded. Nothing shady happening.
#####################################################################################
require 'openssl'
raw = File.read 'root.pem'
new_key = OpenSSL::PKey::RSA.new(2048)
cert = OpenSSL::X509::Certificate.new raw
public_key = cert.public_key
File.open('spoof.key', 'w') do |f|
f.write(cert.public_key)
f.write(new_key.to_pem)
end
spoof_key = OpenSSL::PKey::RSA.new(File.read('spoof.key'))
puts "cert.public_key == public_key: #{cert.public_key.to_der == public_key.to_der}" # true
puts "spoof_key.public_key == public_key: #{spoof_key.public_key.to_der == public_key.to_der}" # false
puts "spoof_key.public_key == new_key.public_key: #{spoof_key.public_key.to_der == new_key.public_key.to_der}" #true
puts "spoof_key == new_key: #{spoof_key.to_der == new_key.to_der}" #true
# Simplified castealer.rb - IMHO, this covers all the important aspects of both PoC scripts
#####################################################################################
require 'openssl'
spoof = OpenSSL::PKey::RSA.new(2048)
cert = OpenSSL::X509::Certificate.new(File.read("some_certificate.cer"))
resigned = cert.sign(spoof, OpenSSL::Digest::SHA1.new) # original certificate, resigned with new RSA key
puts "Resigned certificate verifies with the new key? #{resigned.verify(key)}" # true and expected
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment