From 9b4c6dedc81d4469d1bbf2408e8a67c1c110ae00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=85=AC=E6=98=8E?=
<83812544+Ed1s0nZ@users.noreply.github.com>
Date: Mon, 4 May 2026 04:50:53 +0800
Subject: [PATCH] Add files via upload
---
go.mod | 2 +-
web/static/css/c2.css | 261 ++++++++++++++++++++++++++++++-------
web/static/i18n/en-US.json | 12 ++
web/static/i18n/zh-CN.json | 12 ++
web/static/js/c2.js | 254 +++++++++++++++++++++++++++++++-----
5 files changed, 460 insertions(+), 81 deletions(-)
diff --git a/go.mod b/go.mod
index b9c6c049..ba95d3c2 100644
--- a/go.mod
+++ b/go.mod
@@ -28,6 +28,7 @@ require (
github.com/pkoukk/tiktoken-go v0.1.8
github.com/robfig/cron/v3 v3.0.1
go.uber.org/zap v1.26.0
+ golang.org/x/text v0.26.0
golang.org/x/time v0.14.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -77,7 +78,6 @@ require (
golang.org/x/net v0.24.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.33.0 // indirect
- golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)
diff --git a/web/static/css/c2.css b/web/static/css/c2.css
index 96edb0dc..f7674d3f 100644
--- a/web/static/css/c2.css
+++ b/web/static/css/c2.css
@@ -84,6 +84,16 @@
cursor: pointer;
}
+/* 原生下拉:避免 appearance:none 在部分浏览器中导致 select 无法正常展开 */
+#page-c2 select.form-control.c2-native-select,
+#page-c2-payloads select.form-control.c2-native-select,
+.c2-modal select.form-control.c2-native-select {
+ appearance: auto;
+ -webkit-appearance: menulist-button;
+ background-image: none;
+ padding-right: 14px;
+}
+
#page-c2 textarea.form-control,
#page-c2-payloads textarea.form-control,
.c2-modal textarea.form-control {
@@ -104,7 +114,7 @@
C2 Button Overrides (within C2 scope)
============================================================================ */
-.c2-listener-actions .btn-primary,
+.c2-listener-card-actions .btn-primary,
.c2-payload-card .btn-primary,
.c2-modal-footer .btn-primary {
background: var(--c2-accent);
@@ -118,7 +128,7 @@
transition: all 0.2s;
}
-.c2-listener-actions .btn-primary:hover,
+.c2-listener-card-actions .btn-primary:hover,
.c2-payload-card .btn-primary:hover,
.c2-modal-footer .btn-primary:hover {
background: var(--c2-accent-hover);
@@ -258,10 +268,10 @@
.c2-listener-grid {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
- gap: 16px;
- padding: 24px;
- align-items: start;
+ grid-template-columns: repeat(auto-fill, minmax(292px, 1fr));
+ gap: 20px;
+ padding: 20px 24px 28px;
+ align-items: stretch;
}
.c2-listener-grid:has(.c2-empty) {
display: flex;
@@ -269,68 +279,214 @@
.c2-listener-card {
background: var(--c2-surface);
- border: 1.5px solid var(--c2-border);
- border-radius: var(--c2-radius);
- padding: 24px;
- transition: all 0.25s ease;
- position: relative;
+ border: 1px solid var(--c2-border);
+ border-radius: 14px;
+ padding: 0;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ min-height: 100%;
+ box-shadow: var(--c2-shadow-sm);
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+ border-top: 3px solid var(--c2-border);
}
-.c2-listener-card.running { border-left: 4px solid var(--c2-green); }
-.c2-listener-card.stopped { border-left: 4px solid var(--c2-text-muted); }
-.c2-listener-card.error { border-left: 4px solid var(--c2-amber); }
+.c2-listener-card--running { border-top-color: var(--c2-green); }
+.c2-listener-card--stopped { border-top-color: var(--c2-text-muted); }
+.c2-listener-card--error { border-top-color: var(--c2-amber); }
.c2-listener-card:hover {
box-shadow: var(--c2-shadow-md);
border-color: var(--c2-border-hover);
}
-.c2-listener-header {
+.c2-listener-card-head {
display: flex;
- justify-content: space-between;
+ gap: 14px;
align-items: flex-start;
- margin-bottom: 16px;
+ padding: 18px 18px 0;
+}
+
+.c2-ltype-mark {
+ flex-shrink: 0;
+ width: 44px;
+ height: 44px;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ color: #fff;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15);
+}
+
+.c2-ltype-mark--http { background: linear-gradient(145deg, #3b82f6, #1d4ed8); }
+.c2-ltype-mark--https { background: linear-gradient(145deg, #6366f1, #4338ca); }
+.c2-ltype-mark--tcp { background: linear-gradient(145deg, #8b5cf6, #6d28d9); }
+.c2-ltype-mark--ws { background: linear-gradient(145deg, #0ea5e9, #0369a1); }
+.c2-ltype-mark--def { background: linear-gradient(145deg, #64748b, #475569); }
+
+.c2-listener-card-head-main {
+ flex: 1;
+ min-width: 0;
+}
+
+.c2-listener-card-title-row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
}
.c2-listener-name {
+ margin: 0;
font-weight: 700;
- font-size: 16px;
+ font-size: 17px;
+ line-height: 1.3;
color: var(--c2-text);
+ letter-spacing: -0.02em;
+ flex: 1;
+ min-width: 0;
+ word-break: break-word;
}
-.c2-listener-id {
- font-size: 11px;
- color: var(--c2-text-muted);
- font-family: var(--c2-mono);
- margin-top: 4px;
-}
-
-.c2-listener-type {
+.c2-listener-pill {
+ flex-shrink: 0;
font-size: 11px;
+ font-weight: 700;
padding: 4px 10px;
- border-radius: var(--c2-radius-xs);
+ border-radius: 999px;
+ letter-spacing: 0.02em;
+}
+
+.c2-listener-pill--running {
+ background: var(--c2-green-dim);
+ color: #047857;
+}
+
+.c2-listener-pill--stopped {
background: var(--c2-surface-alt);
color: var(--c2-text-dim);
- font-weight: 600;
border: 1px solid var(--c2-border);
- text-transform: capitalize;
}
-.c2-listener-info {
- font-size: 13px;
- color: var(--c2-text-dim);
- margin-bottom: 20px;
- line-height: 1.8;
+.c2-listener-pill--error {
+ background: var(--c2-amber-dim);
+ color: #b45309;
}
-.c2-listener-address {
+.c2-listener-id-row {
+ margin-top: 8px;
+}
+
+.c2-listener-id-full {
+ display: block;
font-family: var(--c2-mono);
- font-size: 13px;
- margin-bottom: 6px;
+ font-size: 11px;
+ color: var(--c2-text-muted);
+ line-height: 1.4;
+ word-break: break-all;
+ background: var(--c2-surface-alt);
+ padding: 6px 8px;
+ border-radius: 8px;
+ border: 1px solid var(--c2-border);
+}
+
+.c2-listener-card-body {
+ padding: 14px 18px 4px;
+ flex: 1;
display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.c2-listener-kv {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 8px 12px;
+ align-items: baseline;
+ font-size: 13px;
+}
+
+.c2-listener-kv-label {
+ color: var(--c2-text-muted);
+ font-weight: 600;
+ font-size: 12px;
+}
+
+.c2-listener-kv-val {
+ color: var(--c2-text);
+ font-weight: 600;
+ display: inline-flex;
align-items: center;
gap: 8px;
- color: var(--c2-text);
+ min-width: 0;
+ word-break: break-all;
+}
+
+.c2-listener-mono {
+ font-family: var(--c2-mono);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.c2-listener-profile-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ align-self: flex-start;
+ font-size: 12px;
+ font-weight: 600;
+ color: #5b21b6;
+ background: rgba(139, 92, 246, 0.1);
+ border: 1px solid rgba(139, 92, 246, 0.25);
+ padding: 6px 10px;
+ border-radius: 999px;
+ max-width: 100%;
+}
+
+.c2-listener-profile-badge span:last-child {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ min-width: 0;
+}
+
+.c2-listener-profile-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: #7c3aed;
+ flex-shrink: 0;
+}
+
+.c2-listener-remark {
+ font-size: 12px;
+ color: var(--c2-text-dim);
+ line-height: 1.45;
+ padding: 8px 10px;
+ background: var(--c2-surface-alt);
+ border-radius: 8px;
+ border: 1px dashed var(--c2-border);
+}
+
+.c2-listener-meta-row {
+ font-size: 12px;
+ color: var(--c2-text-dim);
+ padding-top: 4px;
+}
+
+.c2-listener-meta-label {
+ font-weight: 600;
+ color: var(--c2-text-muted);
+}
+
+.c2-listener-meta-time {
+ font-family: var(--c2-mono);
+ font-size: 11px;
+ color: var(--c2-text-dim);
}
.c2-status-dot {
@@ -339,23 +495,32 @@
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
+ background: var(--c2-text-muted);
}
-.c2-status-dot.running { background: var(--c2-green); box-shadow: 0 0 0 3px var(--c2-green-dim); }
-.c2-status-dot.stopped { background: var(--c2-text-muted); }
-.c2-status-dot.active { background: var(--c2-green); box-shadow: 0 0 0 3px var(--c2-green-dim); }
+.c2-status-dot.running { background: var(--c2-green); box-shadow: 0 0 0 3px var(--c2-green-dim); }
+.c2-status-dot.stopped { background: var(--c2-text-muted); }
+.c2-status-dot.error { background: var(--c2-amber); box-shadow: 0 0 0 3px var(--c2-amber-dim); }
+.c2-status-dot.active { background: var(--c2-green); box-shadow: 0 0 0 3px var(--c2-green-dim); }
.c2-status-dot.sleeping { background: var(--c2-amber); box-shadow: 0 0 0 3px var(--c2-amber-dim); }
-.c2-status-dot.dead { background: var(--c2-text-muted); }
+.c2-status-dot.dead { background: var(--c2-text-muted); }
-.c2-listener-actions {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
- padding-top: 16px;
+.c2-listener-card-actions {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 10px;
+ padding: 14px 16px 16px;
+ margin-top: auto;
border-top: 1px solid var(--c2-border);
+ background: linear-gradient(180deg, rgba(248, 250, 252, 0.5) 0%, var(--c2-surface-alt) 100%);
}
-.c2-listener-actions button { flex: 1; min-width: 70px; }
+.c2-listener-card-actions button {
+ min-height: 40px;
+ font-size: 13px;
+ font-weight: 600;
+ border-radius: var(--c2-radius-xs);
+}
/* ============================================================================
Session Management
diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json
index e86c926a..bf52aa7e 100644
--- a/web/static/i18n/en-US.json
+++ b/web/static/i18n/en-US.json
@@ -2106,9 +2106,19 @@
"bindHintExternal": "Use 0.0.0.0 to allow external access",
"callbackHost": "Callback host (optional)",
"callbackHostHint": "Public IP or hostname stored for payloads/beacons; separate from bind address. If empty, payload generation falls back to bind address / auto-detect.",
+ "malleableProfile": "Malleable Profile",
+ "malleableProfileHint": "Optional; HTTP/HTTPS Beacon response headers and traffic disguise. Stop and start the listener again for changes to take effect.",
+ "malleableProfileNone": "None",
+ "malleableProfileNonHttpHint": "This listener type does not use a Malleable Profile. You can still bind one here for later if you switch to HTTP/HTTPS Beacon.",
+ "malleableProfileEmptyListHint": "No saved profiles yet. Create one under C2 → Traffic disguise / Malleable Profile, then pick it here.",
"placeholderRemarkLong": "Optional remark",
"editTitle": "Edit Listener",
"startedAt": "Started {{time}}",
+ "startedAtPrefix": "Started",
+ "statusError": "Error",
+ "bindEndpoint": "Listen address",
+ "callbackShort": "Callback",
+ "profileBadgeTitle": "Malleable Profile bound",
"confirmDelete": "Delete this listener? All related sessions and tasks will be removed.",
"toastFillRequired": "Please fill in all required fields",
"toastCreated": "Listener created",
@@ -2116,6 +2126,8 @@
"toastStopped": "Listener stopped",
"toastDeleted": "Listener deleted",
"toastUpdated": "Listener updated",
+ "loadingProfiles": "Loading Malleable Profiles…",
+ "toastProfilesLoadFailed": "Failed to load Malleable Profiles",
"submitCreate": "Create",
"typeLabels": {
"http_beacon": "HTTP Beacon",
diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json
index b099f3fa..f060c313 100644
--- a/web/static/i18n/zh-CN.json
+++ b/web/static/i18n/zh-CN.json
@@ -2095,9 +2095,19 @@
"bindHintExternal": "使用 0.0.0.0 允许外部访问",
"callbackHost": "回连地址(可选)",
"callbackHostHint": "公网 IP 或域名,写入配置供 Payload/Beacon 使用;与「绑定地址」分离。不填则生成 Payload 时按绑定地址或自动探测。",
+ "malleableProfile": "Malleable Profile",
+ "malleableProfileHint": "可选;用于 HTTP/HTTPS Beacon 服务端响应头等流量伪装。修改后需停止并重新启动监听器才会生效。",
+ "malleableProfileNone": "不使用",
+ "malleableProfileNonHttpHint": "当前监听器类型不会使用 Profile;若之后改为 HTTP/HTTPS Beacon,可在此预先绑定。",
+ "malleableProfileEmptyListHint": "暂无已保存的 Profile。请先到侧边栏「流量伪装 / Malleable Profile」页创建,再返回此处选择。",
"placeholderRemarkLong": "可选的备注说明",
"editTitle": "编辑监听器",
"startedAt": "启动于 {{time}}",
+ "startedAtPrefix": "启动于",
+ "statusError": "异常",
+ "bindEndpoint": "监听地址",
+ "callbackShort": "回连",
+ "profileBadgeTitle": "已绑定 Malleable Profile",
"confirmDelete": "确定删除此监听器?相关会话与任务将被清除。",
"toastFillRequired": "请填写必填项",
"toastCreated": "监听器已创建",
@@ -2105,6 +2115,8 @@
"toastStopped": "监听器已停止",
"toastDeleted": "监听器已删除",
"toastUpdated": "监听器已更新",
+ "loadingProfiles": "正在加载 Malleable Profile 列表…",
+ "toastProfilesLoadFailed": "加载 Malleable Profile 列表失败",
"submitCreate": "创建",
"typeLabels": {
"http_beacon": "HTTP Beacon",
diff --git a/web/static/js/c2.js b/web/static/js/c2.js
index 6b8e82c4..7d2a7bf9 100644
--- a/web/static/js/c2.js
+++ b/web/static/js/c2.js
@@ -151,6 +151,74 @@
return div.innerHTML;
}
+ /** 监听器表单:Malleable Profile 下拉选项 HTML(value / 文本已转义) */
+ function listenerProfileSelectHtml(selectedProfileId) {
+ const sel = selectedProfileId ? String(selectedProfileId) : '';
+ let opts = ``;
+ for (const p of (C2.profiles || [])) {
+ if (!p) continue;
+ const pid = p.id || p.ID;
+ if (!pid) continue;
+ const idEsc = escapeHtml(String(pid));
+ const nameEsc = escapeHtml(p.name || pid);
+ const selected = sel && String(pid) === sel ? ' selected' : '';
+ opts += ``;
+ }
+ return opts;
+ }
+
+ function listenerResolvedProfileId(l) {
+ if (!l) return '';
+ const v = l.profileId != null && l.profileId !== '' ? l.profileId : l.profile_id;
+ return v != null ? String(v).trim() : '';
+ }
+
+ /** 监听器卡片展示用 Profile 名称(依赖 C2.profiles,由 loadListeners 一并拉取) */
+ function listenerProfileDisplayName(l) {
+ const pid = listenerResolvedProfileId(l);
+ if (!pid) return '';
+ const list = C2.profiles || [];
+ for (let i = 0; i < list.length; i++) {
+ const p = list[i];
+ if (p && (p.id === pid || p.ID === pid)) return String(p.name || p.id || pid).trim() || pid;
+ }
+ return pid.length > 18 ? pid.substring(0, 16) + '…' : pid;
+ }
+
+ function listenerTypeVisualClass(type) {
+ const t = String(type || '').toLowerCase();
+ if (t === 'https_beacon') return 'c2-ltype-mark--https';
+ if (t === 'http_beacon') return 'c2-ltype-mark--http';
+ if (t === 'tcp_reverse') return 'c2-ltype-mark--tcp';
+ if (t === 'websocket') return 'c2-ltype-mark--ws';
+ return 'c2-ltype-mark--def';
+ }
+
+ function listenerTypeShortLabel(type) {
+ const t = String(type || '').toLowerCase();
+ if (t === 'https_beacon') return 'HTTPS';
+ if (t === 'http_beacon') return 'HTTP';
+ if (t === 'tcp_reverse') return 'TCP';
+ if (t === 'websocket') return 'WS';
+ return '?';
+ }
+
+ function listenerCardStatusPillLabel(status) {
+ const s = String(status || '').toLowerCase();
+ if (s === 'running') return c2t('c2.listeners.running');
+ if (s === 'stopped') return c2t('c2.listeners.stopped');
+ if (s === 'error') return c2t('c2.listeners.statusError');
+ return c2t('c2.listeners.stopped');
+ }
+
+ /** 避免 i18n 插值把日期里的「/」转成 /,与 formatTime 拼接后整体转义 */
+ function formatListenerStartedHtml(dateStr) {
+ if (!dateStr) return '';
+ const prefix = c2t('c2.listeners.startedAtPrefix');
+ const time = formatTime(dateStr);
+ return '
' + escapeHtml(prefix) + ' ' + escapeHtml(time) + '
';
+ }
+
function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => showToast(c2t('c2.clipboardCopied'), 'success'));
@@ -204,13 +272,33 @@
// ============================================================================
C2.loadListeners = function() {
- apiRequest('GET', `${API_BASE}/listeners`).then(data => {
- C2.listeners = data.listeners || [];
+ Promise.all([
+ apiRequest('GET', `${API_BASE}/listeners`),
+ apiRequest('GET', `${API_BASE}/profiles`).catch(function() { return {}; })
+ ]).then(function(results) {
+ var ldata = results[0];
+ var pdata = results[1];
+ C2.listeners = (ldata && ldata.listeners) || [];
+ if (pdata && pdata.profiles && !pdata.error) {
+ C2.profiles = pdata.profiles;
+ }
C2.renderListeners();
C2.updateDashboardStats();
});
};
+ /** 拉取 Profile 列表(监听器表单用);失败时置空列表不阻断弹窗 */
+ C2.ensureProfilesLoaded = function() {
+ return apiRequest('GET', `${API_BASE}/profiles`).then(data => {
+ if (data && data.error) {
+ C2.profiles = [];
+ return C2.profiles;
+ }
+ C2.profiles = (data && data.profiles) || [];
+ return C2.profiles;
+ });
+ };
+
C2.renderListeners = function() {
const container = document.getElementById('c2-listener-grid');
if (!container) return;
@@ -233,33 +321,60 @@
return;
}
- container.innerHTML = C2.listeners.map(l => `
-
-