Last active
August 29, 2015 14:00
-
-
Save emboss/91696b56cd227c8a0c13 to your computer and use it in GitHub Desktop.
castealer.rb analysis
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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