Last active

Embed URL

HTTPS clone URL

SSH clone URL

You can clone with HTTPS or SSH.

Download Gist

castealer.rb analysis

View analysis.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
# 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
Something went wrong with that request. Please try again.