Last active
December 19, 2015 02:09
-
-
Save nahi/5880963 to your computer and use it in GitHub Desktop.
Hostname check bypassing vulnerability in SSL client (CVE-2013-4073) patches.
0001-Hostname-check-bypassing-vulnerability-in-SSL-client.patch is for 2.0.
0001-Hostname-check-bypassing-vulnerability-in-SSL-client.ruby_1_9_3.patch is for 1.9.3. *UPDATE* The patches could cause SSL crash bug. Please apply https://gist.github.com/nahi/5934959, too.
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
From ddaf5b57bdc051ccc1161ec5273a59d30fc2fb72 Mon Sep 17 00:00:00 2001 | |
From: Hiroshi Nakamura <[email protected]> | |
Date: Wed, 5 Jun 2013 23:14:16 +0900 | |
Subject: [PATCH] Hostname check bypassing vulnerability in SSL client | |
(CVE-2013-4073) | |
Ruby's SSL client implements hostname identity check but the OpenSSL | |
function it depends cannot properly handle hostnames in subjectAltName | |
that contain null bytes. The fix parses DER encoded bytes of | |
subjectAltName to extract GeneralName of dNSName and check it against | |
hostname. | |
= Vulnerability Summary = | |
A vulnerability in Ruby's SSL client that could allow man-in-the-middle | |
attackers to spoof SSL servers via valid certificate issued by a trusted | |
certification authority. | |
Ruby's SSL client implements hostname identity check but it does not | |
properly handle hostnames in the certificate that contain null bytes. | |
= Details = | |
OpenSSL::SSL.verify_certificate_identity implements RFC2818 Server | |
Identity check for Ruby's SSL client but it does not properly handle | |
hostnames in the subjectAltName X509 extension that contain null bytes. | |
Existing code in lib/openssl/ssl.rb uses OpenSSL::X509::Extension#value | |
for extracting identity from subjectAltName. Extension#value depends | |
OpenSSL function X509V3_EXT_print() and for dNSName of subjectAltName it | |
utilizes sprintf() that is known as null byte unsafe. As the result | |
Extension#value returns 'www.ruby-lang.org' if the subjectAltName is | |
'www.ruby-lang.org\0.example.com' and | |
OpenSSL::SSL.verify_certificate_identity wrongly identifies the | |
certificate is for 'www.ruby-lang.org'. | |
When a CA a SSL client trusts allows to issue the server certificate | |
that has null byte in subjectAltName, remote attackers can obtain the | |
certificate for 'www.ruby-lang.org\0.example.com' from the CA to spoof | |
'www.ruby-lang.org' and do man-in-the-middle between Ruby's SSL client | |
and SSL servers. | |
= Credit = | |
This vulnerability is found by William (B.J.) Snow Orvis and coordinated | |
with [email protected] by David Thiel from iSEC Partners. | |
--- | |
ext/openssl/lib/openssl/ssl.rb | 18 +++++++++++++----- | |
test/openssl/test_ssl.rb | 22 ++++++++++++++++++++++ | |
2 files changed, 35 insertions(+), 5 deletions(-) | |
diff --git a/ext/openssl/lib/openssl/ssl.rb b/ext/openssl/lib/openssl/ssl.rb | |
index bc3b781..04eb592 100644 | |
--- a/ext/openssl/lib/openssl/ssl.rb | |
+++ b/ext/openssl/lib/openssl/ssl.rb | |
@@ -98,14 +98,22 @@ module OpenSSL | |
should_verify_common_name = true | |
cert.extensions.each{|ext| | |
next if ext.oid != "subjectAltName" | |
- ext.value.split(/,\s+/).each{|general_name| | |
- if /\ADNS:(.*)/ =~ general_name | |
+ id, ostr = OpenSSL::ASN1.decode(ext.to_der).value | |
+ sequence = OpenSSL::ASN1.decode(ostr.value) | |
+ sequence.value.each{|san| | |
+ case san.tag | |
+ when 2 # dNSName in GeneralName (RFC5280) | |
should_verify_common_name = false | |
- reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+") | |
+ reg = Regexp.escape(san.value).gsub(/\\\*/, "[^.]+") | |
return true if /\A#{reg}\z/i =~ hostname | |
- elsif /\AIP Address:(.*)/ =~ general_name | |
+ when 7 # iPAddress in GeneralName (RFC5280) | |
should_verify_common_name = false | |
- return true if $1 == hostname | |
+ # follows GENERAL_NAME_print() in x509v3/v3_alt.c | |
+ if san.value.size == 4 | |
+ return true if san.value.unpack('C*').join('.') == hostname | |
+ elsif san.value.size == 16 | |
+ return true if san.value.unpack('n*').map { |e| sprintf("%X", e) }.join(':') == hostname | |
+ end | |
end | |
} | |
} | |
diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb | |
index f3c1aac..daaaa0a 100644 | |
--- a/test/openssl/test_ssl.rb | |
+++ b/test/openssl/test_ssl.rb | |
@@ -337,6 +337,28 @@ class OpenSSL::TestSSL < OpenSSL::SSLTestCase | |
} | |
end | |
+ def test_verify_certificate_identity | |
+ # creating NULL byte SAN certificate | |
+ ef = OpenSSL::X509::ExtensionFactory.new | |
+ cert = OpenSSL::X509::Certificate.new | |
+ cert.subject = OpenSSL::X509::Name.parse "/DC=some/DC=site/CN=Some Site" | |
+ ext = ef.create_ext('subjectAltName', 'DNS:placeholder,IP:192.168.7.1,IP:13::17') | |
+ ext_asn1 = OpenSSL::ASN1.decode(ext.to_der) | |
+ san_list_der = ext_asn1.value.reduce(nil) { |memo,val| val.tag == 4 ? val.value : memo } | |
+ san_list_asn1 = OpenSSL::ASN1.decode(san_list_der) | |
+ san_list_asn1.value[0].value = 'www.example.com\0.evil.com' | |
+ ext_asn1.value[1].value = san_list_asn1.to_der | |
+ real_ext = OpenSSL::X509::Extension.new ext_asn1 | |
+ cert.add_extension(real_ext) | |
+ | |
+ assert_equal(false, OpenSSL::SSL.verify_certificate_identity(cert, 'www.example.com')) | |
+ assert_equal(true, OpenSSL::SSL.verify_certificate_identity(cert, 'www.example.com\0.evil.com')) | |
+ assert_equal(false, OpenSSL::SSL.verify_certificate_identity(cert, '192.168.7.255')) | |
+ assert_equal(true, OpenSSL::SSL.verify_certificate_identity(cert, '192.168.7.1')) | |
+ assert_equal(false, OpenSSL::SSL.verify_certificate_identity(cert, '13::17')) | |
+ assert_equal(true, OpenSSL::SSL.verify_certificate_identity(cert, '13:0:0:0:0:0:0:17')) | |
+ end | |
+ | |
def test_tlsext_hostname | |
return unless OpenSSL::SSL::SSLSocket.instance_methods.include?(:hostname) | |
-- | |
1.8.1.2 | |
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
From 0eb55c28a7f696df3f57bb8532c6396588bd82cb Mon Sep 17 00:00:00 2001 | |
From: Hiroshi Nakamura <[email protected]> | |
Date: Sun, 23 Jun 2013 16:30:01 +0900 | |
Subject: [PATCH] Hostname check bypassing vulnerability in SSL client | |
(CVE-2013-4073) | |
Ruby's SSL client implements hostname identity check but the OpenSSL | |
function it depends cannot properly handle hostnames in subjectAltName | |
that contain null bytes. The fix parses DER encoded bytes of | |
subjectAltName to extract GeneralName of dNSName and check it against | |
hostname. | |
= Vulnerability Summary = | |
A vulnerability in Ruby's SSL client that could allow man-in-the-middle | |
attackers to spoof SSL servers via valid certificate issued by a trusted | |
certification authority. | |
Ruby's SSL client implements hostname identity check but it does not | |
properly handle hostnames in the certificate that contain null bytes. | |
= Details = | |
OpenSSL::SSL.verify_certificate_identity implements RFC2818 Server | |
Identity check for Ruby's SSL client but it does not properly handle | |
hostnames in the subjectAltName X509 extension that contain null bytes. | |
Existing code in lib/openssl/ssl.rb uses OpenSSL::X509::Extension#value | |
for extracting identity from subjectAltName. Extension#value depends | |
OpenSSL function X509V3_EXT_print() and for dNSName of subjectAltName it | |
utilizes sprintf() that is known as null byte unsafe. As the result | |
Extension#value returns 'www.ruby-lang.org' if the subjectAltName is | |
'www.ruby-lang.org\0.example.com' and | |
OpenSSL::SSL.verify_certificate_identity wrongly identifies the | |
certificate is for 'www.ruby-lang.org'. | |
When a CA a SSL client trusts allows to issue the server certificate | |
that has null byte in subjectAltName, remote attackers can obtain the | |
certificate for 'www.ruby-lang.org\0.example.com' from the CA to spoof | |
'www.ruby-lang.org' and do man-in-the-middle between Ruby's SSL client | |
and SSL servers. | |
= Credit = | |
This vulnerability is found by William (B.J.) Snow Orvis and coordinated | |
with [email protected] by David Thiel from iSEC Partners. | |
--- | |
ext/openssl/lib/openssl/ssl-internal.rb | 18 +++++++++++++----- | |
test/openssl/test_ssl.rb | 22 ++++++++++++++++++++++ | |
2 files changed, 35 insertions(+), 5 deletions(-) | |
diff --git a/ext/openssl/lib/openssl/ssl-internal.rb b/ext/openssl/lib/openssl/ssl-internal.rb | |
index c70b5b8..356d4e8 100644 | |
--- a/ext/openssl/lib/openssl/ssl-internal.rb | |
+++ b/ext/openssl/lib/openssl/ssl-internal.rb | |
@@ -88,14 +88,22 @@ module OpenSSL | |
should_verify_common_name = true | |
cert.extensions.each{|ext| | |
next if ext.oid != "subjectAltName" | |
- ext.value.split(/,\s+/).each{|general_name| | |
- if /\ADNS:(.*)/ =~ general_name | |
+ id, ostr = OpenSSL::ASN1.decode(ext.to_der).value | |
+ sequence = OpenSSL::ASN1.decode(ostr.value) | |
+ sequence.value.each{|san| | |
+ case san.tag | |
+ when 2 # dNSName in GeneralName (RFC5280) | |
should_verify_common_name = false | |
- reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+") | |
+ reg = Regexp.escape(san.value).gsub(/\\\*/, "[^.]+") | |
return true if /\A#{reg}\z/i =~ hostname | |
- elsif /\AIP Address:(.*)/ =~ general_name | |
+ when 7 # iPAddress in GeneralName (RFC5280) | |
should_verify_common_name = false | |
- return true if $1 == hostname | |
+ # follows GENERAL_NAME_print() in x509v3/v3_alt.c | |
+ if san.value.size == 4 | |
+ return true if san.value.unpack('C*').join('.') == hostname | |
+ elsif san.value.size == 16 | |
+ return true if san.value.unpack('n*').map { |e| sprintf("%X", e) }.join(':') == hostname | |
+ end | |
end | |
} | |
} | |
diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb | |
index cf0f1b7..58493bf 100644 | |
--- a/test/openssl/test_ssl.rb | |
+++ b/test/openssl/test_ssl.rb | |
@@ -351,6 +351,28 @@ class OpenSSL::TestSSL < OpenSSL::SSLTestCase | |
} | |
end | |
+ def test_verify_certificate_identity | |
+ # creating NULL byte SAN certificate | |
+ ef = OpenSSL::X509::ExtensionFactory.new | |
+ cert = OpenSSL::X509::Certificate.new | |
+ cert.subject = OpenSSL::X509::Name.parse "/DC=some/DC=site/CN=Some Site" | |
+ ext = ef.create_ext('subjectAltName', 'DNS:placeholder,IP:192.168.7.1,IP:13::17') | |
+ ext_asn1 = OpenSSL::ASN1.decode(ext.to_der) | |
+ san_list_der = ext_asn1.value.reduce(nil) { |memo,val| val.tag == 4 ? val.value : memo } | |
+ san_list_asn1 = OpenSSL::ASN1.decode(san_list_der) | |
+ san_list_asn1.value[0].value = 'www.example.com\0.evil.com' | |
+ ext_asn1.value[1].value = san_list_asn1.to_der | |
+ real_ext = OpenSSL::X509::Extension.new ext_asn1 | |
+ cert.add_extension(real_ext) | |
+ | |
+ assert_equal(false, OpenSSL::SSL.verify_certificate_identity(cert, 'www.example.com')) | |
+ assert_equal(true, OpenSSL::SSL.verify_certificate_identity(cert, 'www.example.com\0.evil.com')) | |
+ assert_equal(false, OpenSSL::SSL.verify_certificate_identity(cert, '192.168.7.255')) | |
+ assert_equal(true, OpenSSL::SSL.verify_certificate_identity(cert, '192.168.7.1')) | |
+ assert_equal(false, OpenSSL::SSL.verify_certificate_identity(cert, '13::17')) | |
+ assert_equal(true, OpenSSL::SSL.verify_certificate_identity(cert, '13:0:0:0:0:0:0:17')) | |
+ end | |
+ | |
def test_tlsext_hostname | |
return unless OpenSSL::SSL::SSLSocket.instance_methods.include?(:hostname) | |
-- | |
1.8.1.2 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment