first commit

This commit is contained in:
ethanseow2002@gmail.com
2026-01-27 17:23:23 +08:00
commit 08c282335a
13 changed files with 4918 additions and 0 deletions
+57
View File
@@ -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/
+138
View File
@@ -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
+82
View File
@@ -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
+155
View File
@@ -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;
}
+293
View File
@@ -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;
}
+39
View File
@@ -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
+486
View File
@@ -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 */
+835
View File
@@ -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())}")
+253
View File
@@ -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;
}
+215
View File
@@ -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
+108
View File
@@ -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;
}
}
}