Add in Exchange Version mixin and module example

This commit is contained in:
Grant Willcox
2022-02-28 14:21:11 -06:00
parent e197669274
commit 269cd5cfed
2 changed files with 137 additions and 95 deletions
@@ -0,0 +1,124 @@
# -*- coding: binary -*-
module Msf
class Exploit
class Remote
module HTTP
# This module provides a way of interacting with Exchange installations
module Exchange
include Msf::Exploit::Remote::HttpClient
# Taken from https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates
@@exchange_server_4_0_builds = ["4.0.996", "4.0.995", "4.0.994", "4.0.993", "4.0.838", "4.0.837"]
@@exchange_server_5_0_builds = ["5.0.1460", "5.0.1458", "5.0.1457"]
@@exchange_server_5_5_builds = ["5.5.2653", "5.5.2650", "5.5.2448", "5.5.2232", "5.5.1960"]
@@exchange_server_2000_builds = ["6.0.6620.7", "6.0.6620.5", "6.0.6603", "6.0.6556", "6.0.6487", "6.0.6249", "6.0.5762", "6.0.4712", "6.0.4417"]
@@exchange_server_2003_builds = ["6.5.7654.4", "6.5.7653.33", "6.5.7683", "6.5.7226", "6.5.6944"]
@@exchange_server_2007_builds = ["8.3.517.0", "8.3.502.0", "8.3.485.1", "8.3.468.0", "8.3.459.0", "8.3.445.0", "8.3.417.1", "8.3.406.0", "8.3.389.2", "8.3.379.2", "8.3.348.2", "8.3.342.4", "8.3.327.1", "8.3.298.3", "8.3.297.2", "8.3.279.6", "8.3.279.5", "8.3.279.3", "8.3.264.0", "8.3.245.2", "8.3.213.1", "8.3.192.1", "8.3.159.2", "8.3.137.3", "8.3.106.2", "8.3.83.6", "8.2.305.3", "8.2.254.0", "8.2.247.2", "8.2.234.1", "8.2.217.3", "8.2.176.2", "8.1.436.0", "8.1.393.1", "8.1.375.2", "8.1.359.2", "8.1.340.1", "8.1.336.1", "8.1.311.3", "8.1.291.2", "8.1.278.2", "8.1.263.1", "8.1.240.6", "8.0.813.0", "8.0.783.2", "8.0.754.0", "8.0.744.0", "8.0.730.1", "8.0.711.2", "8.0.708.3", "8.0.685.25"]
@@exchange_server_2010_builds = ["14.3.513.0", "14.3.509.0", "14.3.496.0", "14.3.468.0", "14.3.461.1", "14.3.452.0", "14.3.442.0", "14.3.435.0", "14.3.419.0", "14.3.417.1", "14.3.411.0", "14.3.399.2", "14.3.389.1", "14.3.382.0", "14.3.361.1", "14.3.352.0", "14.3.336.0", "14.3.319.2", "14.3.301.0", "14.3.294.0", "14.3.279.2", "14.3.266.2", "14.3.248.2", "14.3.235.1", "14.3.224.2", "14.3.224.1", "14.3.210.2", "14.3.195.1", "14.3.181.6", "14.3.174.1", "14.3.169.1", "14.3.158.1", "14.3.146.0", "14.3.123.4", "14.2.390.3", "14.2.375.0", "14.2.342.3", "14.2.328.10", "14.3.328.5", "14.2.318.4", "14.2.318.2", "14.2.309.2", "14.2.298.4", "14.2.283.3", "14.2.247.5", "14.1.438.0", "14.1.421.3", "14.1.421.2", "14.1.421.0", "14.1.355.2", "14.1.339.1", "14.1.323.6", "14.1.289.7", "14.1.270.1", "14.1.255.2", "14.1.218.15", "14.0.726.0", "14.0.702.1", "14.0.694.0", "14.0.689.0", "14.0.682.1", "14.0.639.21"]
@@exchange_server_2013_builds = ["15.0.1497.28", "15.0.1497.26", "15.0.1497.24", "15.0.1497.23", "15.0.1497.18", "15.0.1497.15", "15.0.1497.12", "15.0.1497.2", "15.0.1473.6", "15.0.1473.3", "15.0.1395.12", "15.0.1395.4", "15.0.1367.3", "15.0.1365.1", "15.0.1347.2", "15.0.1320.4", "15.0.1293.2", "15.0.1263.5", "15.0.1236.3", "15.0.1210.3", "15.0.1178.4", "15.0.1156.6", "15.0.1130.7", "15.0.1104.5", "15.0.1076.9", "15.0.1044.25", "15.0.995.29", "15.0.913.22", "15.0.847.64", "15.0.847.32", "15.0.775.38", "15.0.712.24", "15.0.620.29", "15.0.516.32"]
@@exchange_server_2016_builds = ["15.1.2375.18", "15.1.2375.17", "15.1.2375.12", "15.1.2375.7", "15.1.2308.21", "15.1.2308.20", "15.1.2308.15", "15.1.2308.14", "15.1.2308.8", "15.1.2242.12", "15.1.2242.10", "15.1.2242.8", "15.1.2242.4", "15.1.2176.14", "15.1.2176.12", "15.1.2176.9", "15.1.2176.2", "15.1.2106.13", "15.1.2106.2", "15.1.2044.13", "15.1.2044.4", "15.1.1979.8", "15.1.1979.3", "15.1.1913.12", "15.1.1913.5", "15.1.1847.12", "15.1.1847.3", "15.1.1779.8", "15.1.1779.2", "15.1.1713.10", "15.1.1713.5", "15.1.1591.18", "15.1.1591.10", "15.1.1531.12", "15.1.1531.3", "15.1.1466.16", "15.1.1466.3", "15.1.1415.10", "15.1.1415.2", "15.1.1261.35", "15.1.1034.26", "15.1.845.34", "15.1.669.32", "15.1.544.27", "15.1.466.34", "15.1.396.30", "15.1.225.42", "15.1.225.16"]
@@exchange_server_2019_builds = ["15.2.986.15", "15.2.986.14", "15.2.986.9", "15.2.986.5", "15.2.922.20", "15.2.922.19", "15.2.922.14", "15.2.922.13", "15.2.922.7", "15.2.858.15", "15.2.858.12", "15.2.858.10", "15.2.858.5", "15.2.792.15", "15.2.792.13", "15.2.792.10", "15.2.792.3", "15.2.721.13", "15.2.721.2", "15.2.659.12", "15.2.659.4", "15.2.595.8", "15.2.595.3", "15.2.529.13", "15.2.529.5", "15.2.464.15", "15.2.464.5", "15.2.397.11", "15.2.397.3", "15.2.330.11", "15.2.330.5", "15.2.221.18", "15.2.221.12", "15.2.196.0"]
# Full list of all known Exchange Server builds.
@@exchange_builds = [*@@exchange_server_4_0_builds, *@@exchange_server_5_0_builds, *@@exchange_server_5_5_builds, *@@exchange_server_2000_builds, *@@exchange_server_2003_builds, *@@exchange_server_2007_builds, *@@exchange_server_2010_builds, *@@exchange_server_2013_builds, *@@exchange_server_2016_builds, *@@exchange_server_2019_builds]
# Get the Exchange version number.
#
# @see https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates Exchange Version Numbers
#
# @return [Rex::Version, nil] The Exchange version if it was able to be recovered. Nil otherwise
def exchange_get_version()
=begin
# First lets try a cheap way of doing this via a leak of the X-OWA-Version header.
# If we get this we know the version number for sure and we can skip a lot of leg work.
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/owa/service')
)
unless res
print_error('Target did not respond!')
return nil
end
if res.headers['X-OWA-Version']
build = res.headers['X-OWA-Version']
return Rex::Version.new(build)
end
# Next, determine if we are up against an older version of Exchange Server where
# the /owa/auth/logon.aspx page gives the full version. Recent versions of Exchange
# give only a partial version without the build number.
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/owa/auth/logon.aspx')
)
unless res
print_error('Target did not respond!')
return nil
end
if res.code == 200 && ((%r{/owa/(?<build>\d+\.\d+\.\d+\.\d+)} =~ res.body) || (%r{/owa/auth/(?<build>\d+\.\d+\.\d+\.\d+)} =~ res.body))
return Rex::Version.new(build)
end
# Next try @tseller's way and try /ecp/Current/exporttool/microsoft.exchange.ediscovery.exporttool.application
# URL which if successful should provide some XML with entries like the following:
#
# <assemblyIdentity name="microsoft.exchange.ediscovery.exporttool.application"
# version="15.2.986.5" publicKeyToken="b1d1a6c45aa418ce" language="neutral"
# processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" />
#
# This only works on Exchange Server 2013 and later and may not always work, but if it
# does work it provides the full version number so its a nice strategy.
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/ecp/current/exporttool/microsoft.exchange.ediscovery.exporttool.application')
)
unless res
print_error('Target did not respond!')
return nil
end
if res.code == 200 && res.body =~ /name="microsoft.exchange.ediscovery.exporttool" version="\d+\.\d+\.\d+\.\d+"/
build = res.body.match(/name="microsoft.exchange.ediscovery.exporttool" version="(\d+\.\d+\.\d+\.\d+)"/)[1]
return Rex::Version.new(build)
end
=end
# Finally, try a variation on the above and use a well known trick of grabbing /owa/auth/logon.aspx
# to get a partial version number, then use the URL at /ecp/<version here>/exporttool/. If we get a 200
# OK response, we found the target version number, otherwise we didn't find it.
#
# Props go to @jmartin-r7 for improving my original code for this and suggestion the use of
# canonical_segments to make this close to the Rex::Version code format. Also for noticing that
# version_range is a Rex::Version object already and cleaning up some of my original code to simplify
# things on this premise.
@@exchange_builds.each do |version|
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "/ecp/#{version}/exporttool/")
)
unless res
print_error('Target did not respond!')
return nil
end
if res && res.code == 200
return Rex::Version.new(version)
end
end
# If we reach here we couldn't find the Exchange Server version, so just return nil to indicate this.
nil
end
end
end
end
end
end
@@ -13,6 +13,7 @@ class MetasploitModule < Msf::Exploit::Remote
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
include Msf::Exploit::Powershell
include Msf::Exploit::Remote::HTTP::Exchange
def initialize(info = {})
super(
@@ -134,104 +135,21 @@ class MetasploitModule < Msf::Exploit::Remote
]
end
# Credits to Alan David Foster for the assistance on this :)
def vulnerable_build?(current_build_rex)
vuln_builds.any? { |start_version, end_version| (current_build_rex >= start_version) && (current_build_rex < end_version) }
end
def check
# First lets try a cheap way of doing this via a leak of the X-OWA-Version header.
# If we get this we know the version number for sure and we can skip a lot of leg work.
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/owa/service')
)
unless res
return CheckCode::Unknown('Target did not respond to check.')
current_build_rex = exchange_get_version()
if current_build_rex.nil?
return CheckCode::Unknown("Couldn't retrieve the target Exchange Server version!")
end
if res.headers['X-OWA-Version']
build = res.headers['X-OWA-Version']
if vuln_builds.any? { |build_range| Rex::Version.new(build).between?(*build_range) }
return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")
else
return CheckCode::Safe("Exchange Server #{build} is not a vulnerable build.")
end
end
# Next, determine if we are up against an older version of Exchange Server where
# the /owa/auth/logon.aspx page gives the full version. Recent versions of Exchange
# give only a partial version without the build number.
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/owa/auth/logon.aspx')
)
unless res
return CheckCode::Unknown('Target did not respond to check.')
end
if res.code == 200 && ((%r{/owa/(?<build>\d+\.\d+\.\d+\.\d+)} =~ res.body) || (%r{/owa/auth/(?<build>\d+\.\d+\.\d+\.\d+)} =~ res.body))
if vuln_builds.any? { |build_range| Rex::Version.new(build).between?(*build_range) }
return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")
else
return CheckCode::Safe("Exchange Server #{build} is not a vulnerable build.")
end
end
# Next try @tseller's way and try /ecp/Current/exporttool/microsoft.exchange.ediscovery.exporttool.application
# URL which if successful should provide some XML with entries like the following:
#
# <assemblyIdentity name="microsoft.exchange.ediscovery.exporttool.application"
# version="15.2.986.5" publicKeyToken="b1d1a6c45aa418ce" language="neutral"
# processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" />
#
# This only works on Exchange Server 2013 and later and may not always work, but if it
# does work it provides the full version number so its a nice strategy.
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/ecp/current/exporttool/microsoft.exchange.ediscovery.exporttool.application')
)
unless res
return CheckCode::Unknown('Target did not respond to check.')
end
if res.code == 200 && res.body =~ /name="microsoft.exchange.ediscovery.exporttool" version="\d+\.\d+\.\d+\.\d+"/
build = res.body.match(/name="microsoft.exchange.ediscovery.exporttool" version="(\d+\.\d+\.\d+\.\d+)"/)[1]
if vuln_builds.any? { |build_range| Rex::Version.new(build).between?(*build_range) }
return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")
else
return CheckCode::Safe("Exchange Server #{build} is not a vulnerable build.")
end
end
# Finally, try a variation on the above and use a well known trick of grabbing /owa/auth/logon.aspx
# to get a partial version number, then use the URL at /ecp/<version here>/exporttool/. If we get a 200
# OK response, we found the target version number, otherwise we didn't find it.
#
# Props go to @jmartin-r7 for improving my original code for this and suggestion the use of
# canonical_segments to make this close to the Rex::Version code format. Also for noticing that
# version_range is a Rex::Version object already and cleaning up some of my original code to simplify
# things on this premise.
vuln_builds.each do |version_range|
return CheckCode::Unknown('Range provided is not iterable') unless version_range[0].canonical_segments[0..-2] == version_range[1].canonical_segments[0..-2]
prepend_range = version_range[0].canonical_segments[0..-2]
lowest_patch = version_range[0].canonical_segments.last
while Rex::Version.new((prepend_range.dup << lowest_patch).join('.')) <= version_range[1]
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "/ecp/#{build}/exporttool/")
)
unless res
return CheckCode::Unknown('Target did not respond to check.')
end
if res && res.code == 200
return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")
end
lowest_patch += 1
end
CheckCode::Unknown('Could not determine the build number of the target Exchange Server.')
if vulnerable_build?(current_build_rex)
CheckCode::Appears("Exchange Server #{current_build_rex.to_s} is a vulnerable version!")
else
CheckCode::Safe("Exchange Server #{current_build_rex.to_s} does not appear to be a vulnerable version!")
end
end