Add SAN parsing with a proper ASN.1 definition

The ORAddress field is left out because it's significantly more
complicated than the rest and doesn't appear to be necessary at this
time.
This commit is contained in:
Spencer McIntyre 2023-06-13 15:48:49 -04:00
parent 39c9355715
commit 0555b4ada0
4 changed files with 216 additions and 117 deletions

View File

@ -113,7 +113,7 @@ module Exploit::Remote::MsIcpr
begin
connect
rescue Rex::ConnectionError => e
raise MsIcprConnectionError, e.message
raise MsIcprConnectionError, e.message
end
begin
@ -211,6 +211,14 @@ module Exploit::Remote::MsIcpr
return unless response[:certificate]
if (dns = get_cert_san_dns(response[:certificate]))
print_status("Certificate DNS: #{dns}")
end
if (email = get_cert_san_email(response[:certificate]))
print_status("Certificate Email: #{email}")
end
if (sid = get_cert_msext_sid(response[:certificate]))
print_status("Certificate SID: #{sid}")
end
@ -382,26 +390,34 @@ module Exploit::Remote::MsIcpr
# @param [OpenSSL::X509::Certificate] cert
# @return [String, nil] The UPN if it was found, otherwise nil.
def get_cert_msext_upn(cert)
get_cert_ext_property(cert, 'subjectAltName', 'msUPN')
return unless (san = get_cert_san(cert))
return unless (gn = san[:GeneralNames].value.find { |gn| gn[:otherName][:type_id]&.value == OID_NT_PRINCIPAL_NAME })
RASN1::Types::Utf8String.parse(gn[:otherName][:value].value, explicit: 0, constructed: true).value
end
# Get a value from a certificate extension. Returns nil if it's not found. Allows fetching values not natively
# supported by Ruby's OpenSSL by parsing the ASN1 directly.
def get_cert_ext_property(cert, ext_oid, key)
ext = cert.extensions.find { |e| e.oid == ext_oid }
def get_cert_san(cert)
ext = cert.extensions.find { |e| e.oid == 'subjectAltName' }
return unless ext
# need to decode the contents and handle them ourselves
ext_asn = OpenSSL::ASN1.decode(OpenSSL::ASN1.decode(ext.to_der).value[1].value)
ext_asn.value.each do |value|
value = value.value
next unless value.is_a?(Array)
next unless value[0]&.value == key
Rex::Proto::CryptoAsn1::X509::SubjectAltName.parse(ext.value_der)
end
return value[1].value[0].value
end
def get_cert_san_dns(cert)
return unless (san = get_cert_san(cert))
nil
return unless (gn = san[:GeneralNames].value.find { |gn| gn[:dNSName].value? })
gn[:dNSName].value
end
def get_cert_san_email(cert)
return unless (san = get_cert_san(cert))
return unless (gn = san[:GeneralNames].value.find { |gn| gn[:rfc822Name].value? })
gn[:rfc822Name].value
end
def icpr_service_data

View File

@ -2,108 +2,6 @@
require 'rasn1'
module Rex::Proto::CryptoAsn1
class RASN1::Model
def self.bmp_string(name, options = {})
custom_primitive_type_for(name, BmpString, options)
end
def self.teletex_string(name, options = {})
strict_encoding = options.fetch(:strict_encoding, true)
options.delete(:strict_encoding)
if strict_encoding
raise NotImplementedError.new('The ITU T.61 codec is not available.')
custom_primitive_type_for(name, TeletexString, options)
else
custom_primitive_type_for(name, TeletexString::Permissive, options)
end
end
def self.universal_string(name, options = {})
custom_primitive_type_for(name, UniversalString, options)
end
def self.custom_primitive_type_for(name, clazz, options = {})
options.merge!(name: name)
proc = proc do |opts|
clazz.new(options.merge(opts))
end
@root = Elem.new(name, proc, nil)
end
private_class_method :custom_primitive_type_for
end
class BmpString < RASN1::Types::OctetString
ID = 30
# Get ASN.1 type
# @return [String]
def self.type
'BmpString'
end
private
def value_to_der
@value.to_s.dup.encode('UTF-16BE').b
end
def der_to_value(der, ber: false)
super
@value = der.dup.force_encoding('UTF-16BE')
end
end
class TeletexString < RASN1::Types::OctetString
ID = 20
def self.type
'TeletexString'
end
ENCODING = 'ITU-T.61'.freeze
# Technically this type should be using T.61 encoding, however some libraries
# such as OpenSSL use this type to label strings encoded with ISO-8859-1.
# See:
# * https://pike.lysator.liu.se/generated/manual/modref/ex/7.8_3A_3A/Standards/ASN1/Types/TeletexString.html
# * https://github.com/wbond/asn1crypto/blob/fad689f2072e405317436c8bf7f6609ba183a060/asn1crypto/x509.py#L461-L465
class Permissive < TeletexString
ENCODING = 'ISO-8859-1'.freeze
end
private
def value_to_der
@value.to_s.dup.encode(self.class::ENCODING).b
end
def der_to_value(der, ber: false)
super
@value = der.dup.force_encoding(self.class::ENCODING)
end
end
class UniversalString < RASN1::Types::OctetString
ID = 28
def self.type
'UniversalString'
end
private
def value_to_der
@value.to_s.dup.encode('UTF-32BE').b
end
def der_to_value(der, ber: false)
super
@value = der.dup.force_encoding('UTF-32BE')
end
end
# see: [[MS-WCCE]: 2.2.2.7.10 szENROLLMENT_NAME_VALUE_PAIR](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/92f07a54-2889-45e3-afd0-94b60daa80ec)
class EnrollmentNameValuePair < RASN1::Model
sequence :enrollment_name_value_pair, content: [

View File

@ -0,0 +1,106 @@
# -*- coding: binary -*-
require 'rasn1'
module Rex::Proto::CryptoAsn1::Types
class RASN1::Model
def self.bmp_string(name, options = {})
custom_primitive_type_for(name, BmpString, options)
end
def self.teletex_string(name, options = {})
strict_encoding = options.fetch(:strict_encoding, true)
options.delete(:strict_encoding)
if strict_encoding
raise NotImplementedError.new('The ITU T.61 codec is not available.')
custom_primitive_type_for(name, TeletexString, options)
else
custom_primitive_type_for(name, TeletexString::Permissive, options)
end
end
def self.universal_string(name, options = {})
custom_primitive_type_for(name, UniversalString, options)
end
def self.custom_primitive_type_for(name, clazz, options = {})
options.merge!(name: name)
proc = proc do |opts|
clazz.new(options.merge(opts))
end
@root = Elem.new(name, proc, nil)
end
private_class_method :custom_primitive_type_for
end
class BmpString < RASN1::Types::OctetString
ID = 30
# Get ASN.1 type
# @return [String]
def self.type
'BmpString'
end
private
def value_to_der
@value.to_s.dup.encode('UTF-16BE').b
end
def der_to_value(der, ber: false)
super
@value = der.dup.force_encoding('UTF-16BE')
end
end
class TeletexString < RASN1::Types::OctetString
ID = 20
def self.type
'TeletexString'
end
ENCODING = 'ITU-T.61'.freeze
# Technically this type should be using T.61 encoding, however some libraries
# such as OpenSSL use this type to label strings encoded with ISO-8859-1.
# See:
# * https://pike.lysator.liu.se/generated/manual/modref/ex/7.8_3A_3A/Standards/ASN1/Types/TeletexString.html
# * https://github.com/wbond/asn1crypto/blob/fad689f2072e405317436c8bf7f6609ba183a060/asn1crypto/x509.py#L461-L465
class Permissive < TeletexString
ENCODING = 'ISO-8859-1'.freeze
end
private
def value_to_der
@value.to_s.dup.encode(self.class::ENCODING).b
end
def der_to_value(der, ber: false)
super
@value = der.dup.force_encoding(self.class::ENCODING)
end
end
class UniversalString < RASN1::Types::OctetString
ID = 28
def self.type
'UniversalString'
end
private
def value_to_der
@value.to_s.dup.encode('UTF-32BE').b
end
def der_to_value(der, ber: false)
super
@value = der.dup.force_encoding('UTF-32BE')
end
end
end

View File

@ -0,0 +1,79 @@
# -*- coding: binary -*-
require 'rasn1'
require 'rex/proto/crypto_asn1/types'
module Rex::Proto::CryptoAsn1::X509
class AttributeType < RASN1::Types::ObjectId
end
class AttributeValue < RASN1::Types::Any
end
class AttributeTypeAndValue < RASN1::Model
sequence :AttributeTypeAndValue, content: [
wrapper(model(:type, AttributeType)),
wrapper(model(:value, AttributeValue))
]
end
class DirectoryString < RASN1::Model
choice :DirectoryString, content: [
teletex_string(:teletexString, strict_encoding: false),
printable_string(:printableString),
universal_string(:universalString),
utf8_string(:utf8String),
bmp_string(:bmpString)
]
end
class EDIPartyName < RASN1::Model
sequence :EDIPartyName, content: [
wrapper(model(:nameAssigner, DirectoryString), implicit: 0, optional: true),
wrapper(model(:partyName, DirectoryString), implicit: 1)
]
end
class RelativeDistinguishedName < RASN1::Model
set_of(:RelativeDistinguishedName, AttributeTypeAndValue)
end
class RDNSequence < RASN1::Model
sequence_of(:RDNSequence, RelativeDistinguishedName)
end
class Name < RASN1::Model
choice :Name, content: [
wrapper(model(:RDNSequence, RDNSequence))
]
end
class OtherName < RASN1::Model
sequence :OtherName, implicit: 0, content: [
objectid(:type_id),
any(:value, explicit: 0, constructed: true)
]
end
class GeneralName < RASN1::Model
choice :GeneralName, content: [
wrapper(model(:otherName, OtherName), implicit: 0),
ia5_string(:rfc822Name, implicit: 1),
ia5_string(:dNSName, implicit: 2),
# wrapper(model(:x400Address, ORAddress), implicit: 3),
wrapper(model(:directoryName, Name), implicit: 4),
wrapper(model(:ediPartyName, EDIPartyName), implicit: 5),
ia5_string(:uniformResourceIdentifier, implicit: 6),
octet_string(:iPAddress, implicit: 7),
objectid(:registeredID, implicit: 8)
]
end
# https://datatracker.ietf.org/doc/html/rfc3280#section-4.2.1.7
class GeneralNames < RASN1::Model
sequence_of(:GeneralNames, GeneralName)
end
# https://datatracker.ietf.org/doc/html/rfc3280#section-4.2.1.7
class SubjectAltName < GeneralNames
end
end