From 269cd5cfed68fc2feb9b5cc9ad5cd6b64cbbc97e Mon Sep 17 00:00:00 2001 From: Grant Willcox Date: Mon, 28 Feb 2022 14:21:11 -0600 Subject: [PATCH] Add in Exchange Version mixin and module example --- lib/msf/core/exploit/remote/http/exchange.rb | 124 ++++++++++++++++++ ...edserializationbinder_denylist_typo_rce.rb | 108 ++------------- 2 files changed, 137 insertions(+), 95 deletions(-) create mode 100644 lib/msf/core/exploit/remote/http/exchange.rb diff --git a/lib/msf/core/exploit/remote/http/exchange.rb b/lib/msf/core/exploit/remote/http/exchange.rb new file mode 100644 index 0000000000..3a2352f22b --- /dev/null +++ b/lib/msf/core/exploit/remote/http/exchange.rb @@ -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/(?\d+\.\d+\.\d+\.\d+)} =~ res.body) || (%r{/owa/auth/(?\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: + # + # + # + # 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//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 diff --git a/modules/exploits/windows/http/exchange_chainedserializationbinder_denylist_typo_rce.rb b/modules/exploits/windows/http/exchange_chainedserializationbinder_denylist_typo_rce.rb index 2019b25d51..8682bae1e1 100644 --- a/modules/exploits/windows/http/exchange_chainedserializationbinder_denylist_typo_rce.rb +++ b/modules/exploits/windows/http/exchange_chainedserializationbinder_denylist_typo_rce.rb @@ -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/(?\d+\.\d+\.\d+\.\d+)} =~ res.body) || (%r{/owa/auth/(?\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: - # - # - # - # 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//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