mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-31 08:07:29 +02:00
Add files via upload
This commit is contained in:
@@ -772,6 +772,66 @@
|
|||||||
border: 1px solid var(--c2-border);
|
border: 1px solid var(--c2-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#c2-file-upload-btn.is-disabled,
|
||||||
|
#c2-file-upload-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--c2-text-dim, #94a3b8);
|
||||||
|
border-color: var(--c2-border, #e2e8f0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.c2-file-upload-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #b45309;
|
||||||
|
background: rgba(245, 158, 11, 0.08);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||||
|
border-radius: var(--c2-radius-xs, 4px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: -8px 0 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c2-file-upload-hint[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c2-file-upload-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: -8px 0 12px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c2-file-upload-progress[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c2-file-upload-progress-track {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--c2-border);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c2-file-upload-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0;
|
||||||
|
background: var(--c2-accent, #3b82f6);
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c2-file-upload-progress-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--c2-text-dim);
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.c2-file-list {
|
.c2-file-list {
|
||||||
background: var(--c2-surface);
|
background: var(--c2-surface);
|
||||||
border-radius: var(--c2-radius);
|
border-radius: var(--c2-radius);
|
||||||
|
|||||||
@@ -2546,6 +2546,15 @@
|
|||||||
"files": {
|
"files": {
|
||||||
"parent": "Parent",
|
"parent": "Parent",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
|
"upload": "Upload",
|
||||||
|
"uploading": "Uploading {{name}} · {{percent}}%",
|
||||||
|
"uploadOk": "Uploaded",
|
||||||
|
"uploadQueued": "Upload task queued",
|
||||||
|
"uploadPendingApproval": "Upload task pending HITL approval",
|
||||||
|
"uploadUnsupported": "Upload is not supported for this session",
|
||||||
|
"uploadCurlBeacon": "Curl beacons cannot upload files; use an HTTP Beacon",
|
||||||
|
"uploadTcpShell": "This is a TCP reverse shell (bash/nc): commands and download only. For upload, reconnect with: (1) a compiled CSB1 Beacon on the same listener, or (2) an HTTP/HTTPS Beacon.",
|
||||||
|
"uploadTcpReverse": "This is a TCP reverse shell (bash/nc): commands and download only. For upload, reconnect with: (1) a compiled CSB1 Beacon on the same listener, or (2) an HTTP/HTTPS Beacon.",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"timeout": "Timed out loading files",
|
"timeout": "Timed out loading files",
|
||||||
"emptyDir": "Empty directory",
|
"emptyDir": "Empty directory",
|
||||||
@@ -2555,6 +2564,7 @@
|
|||||||
"colActions": "Actions",
|
"colActions": "Actions",
|
||||||
"open": "Open",
|
"open": "Open",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
"downloadOk": "Downloaded",
|
||||||
"failed": "Failed"
|
"failed": "Failed"
|
||||||
},
|
},
|
||||||
"listeners": {
|
"listeners": {
|
||||||
|
|||||||
@@ -2535,6 +2535,15 @@
|
|||||||
"files": {
|
"files": {
|
||||||
"parent": "上级目录",
|
"parent": "上级目录",
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
|
"upload": "上传",
|
||||||
|
"uploading": "正在上传 {{name}} · {{percent}}%",
|
||||||
|
"uploadOk": "上传成功",
|
||||||
|
"uploadQueued": "上传任务已入队",
|
||||||
|
"uploadPendingApproval": "上传任务待人机协同审批",
|
||||||
|
"uploadUnsupported": "当前会话不支持上传",
|
||||||
|
"uploadCurlBeacon": "Curl 轻量信标不支持文件上传,请使用 HTTP Beacon",
|
||||||
|
"uploadTcpShell": "当前为 TCP 反弹 Shell(bash/nc),仅支持命令与下载。上传请改用:① 同一监听器下编译 CSB1 Beacon,或 ② HTTP/HTTPS Beacon 重新上线。",
|
||||||
|
"uploadTcpReverse": "当前为 TCP 反弹 Shell(bash/nc),仅支持命令与下载。上传请改用:① 同一监听器下编译 CSB1 Beacon,或 ② HTTP/HTTPS Beacon 重新上线。",
|
||||||
"loading": "加载中…",
|
"loading": "加载中…",
|
||||||
"timeout": "加载文件超时",
|
"timeout": "加载文件超时",
|
||||||
"emptyDir": "空目录",
|
"emptyDir": "空目录",
|
||||||
@@ -2544,6 +2553,7 @@
|
|||||||
"colActions": "操作",
|
"colActions": "操作",
|
||||||
"open": "打开",
|
"open": "打开",
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
|
"downloadOk": "下载成功",
|
||||||
"failed": "失败"
|
"failed": "失败"
|
||||||
},
|
},
|
||||||
"listeners": {
|
"listeners": {
|
||||||
|
|||||||
+498
-20
@@ -33,8 +33,10 @@
|
|||||||
terminalBusy: false,
|
terminalBusy: false,
|
||||||
terminalQueue: [],
|
terminalQueue: [],
|
||||||
// 文件管理
|
// 文件管理
|
||||||
currentPath: '/',
|
currentPath: '.',
|
||||||
|
implantPwd: null,
|
||||||
fileList: [],
|
fileList: [],
|
||||||
|
fileUploadBusy: false,
|
||||||
// 任务轮询
|
// 任务轮询
|
||||||
taskPollInterval: null,
|
taskPollInterval: null,
|
||||||
};
|
};
|
||||||
@@ -325,6 +327,7 @@
|
|||||||
break;
|
break;
|
||||||
case 'c2-sessions':
|
case 'c2-sessions':
|
||||||
C2.loadSessions();
|
C2.loadSessions();
|
||||||
|
C2.ensureListenersLoaded();
|
||||||
break;
|
break;
|
||||||
case 'c2-tasks':
|
case 'c2-tasks':
|
||||||
C2.loadTasks();
|
C2.loadTasks();
|
||||||
@@ -345,6 +348,16 @@
|
|||||||
// 监听器管理
|
// 监听器管理
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
C2.ensureListenersLoaded = function() {
|
||||||
|
if (C2.listeners && C2.listeners.length > 0) {
|
||||||
|
return Promise.resolve(C2.listeners);
|
||||||
|
}
|
||||||
|
return apiRequest('GET', `${API_BASE}/listeners`).then(function(data) {
|
||||||
|
C2.listeners = (data && data.listeners) || [];
|
||||||
|
return C2.listeners;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
C2.loadListeners = function() {
|
C2.loadListeners = function() {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
apiRequest('GET', `${API_BASE}/listeners`),
|
apiRequest('GET', `${API_BASE}/listeners`),
|
||||||
@@ -778,6 +791,8 @@
|
|||||||
|
|
||||||
C2.selectSession = function(id) {
|
C2.selectSession = function(id) {
|
||||||
C2.selectedSessionId = id;
|
C2.selectedSessionId = id;
|
||||||
|
C2.implantPwd = null;
|
||||||
|
C2.currentPath = '.';
|
||||||
C2.renderSessions();
|
C2.renderSessions();
|
||||||
C2.renderSessionDetail(id);
|
C2.renderSessionDetail(id);
|
||||||
};
|
};
|
||||||
@@ -822,10 +837,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="c2-tab-files" class="c2-tab-panel" style="display:none;">
|
<div id="c2-tab-files" class="c2-tab-panel" style="display:none;">
|
||||||
<div class="c2-file-toolbar">
|
<div class="c2-file-toolbar">
|
||||||
<button class="btn-ghost btn-sm" onclick="C2.loadFileList('..')">⬆ ${escapeHtml(c2t('c2.files.parent'))}</button>
|
<button class="btn-ghost btn-sm" onclick="C2.goToParentDirectory()">⬆ ${escapeHtml(c2t('c2.files.parent'))}</button>
|
||||||
<button class="btn-ghost btn-sm" onclick="C2.refreshFiles()">${escapeHtml(c2t('c2.files.refresh'))}</button>
|
<button class="btn-ghost btn-sm" onclick="C2.refreshFiles()">${escapeHtml(c2t('c2.files.refresh'))}</button>
|
||||||
|
<button type="button" class="btn-ghost btn-sm" id="c2-file-upload-btn" onclick="C2.openFileUploadPicker()" title="${escapeHtml(c2t('c2.files.upload'))}">📤 ${escapeHtml(c2t('c2.files.upload'))}</button>
|
||||||
|
<input type="file" id="c2-file-upload-input" style="display:none" onchange="C2.onC2FileUploadPick(event)" />
|
||||||
<span id="c2-current-path" class="c2-path-breadcrumb">/</span>
|
<span id="c2-current-path" class="c2-path-breadcrumb">/</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="c2-file-upload-hint" class="c2-file-upload-hint" hidden role="status"></div>
|
||||||
|
<div id="c2-file-upload-progress" class="c2-file-upload-progress" hidden role="status" aria-live="polite">
|
||||||
|
<div class="c2-file-upload-progress-track" aria-hidden="true"><div class="c2-file-upload-progress-fill" id="c2-file-upload-progress-fill"></div></div>
|
||||||
|
<span class="c2-file-upload-progress-label" id="c2-file-upload-progress-label"></span>
|
||||||
|
</div>
|
||||||
<div id="c2-file-list" class="c2-file-list"></div>
|
<div id="c2-file-list" class="c2-file-list"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="c2-tab-tasks" class="c2-tab-panel" style="display:none;">
|
<div id="c2-tab-tasks" class="c2-tab-panel" style="display:none;">
|
||||||
@@ -869,6 +891,10 @@
|
|||||||
if (!isCurlBeacon) C2.initTerminal();
|
if (!isCurlBeacon) C2.initTerminal();
|
||||||
C2.loadFileList(s.id, '.');
|
C2.loadFileList(s.id, '.');
|
||||||
C2.loadSessionTasks(s.id);
|
C2.loadSessionTasks(s.id);
|
||||||
|
C2.updateFileUploadButton(s);
|
||||||
|
C2.ensureListenersLoaded().then(function() {
|
||||||
|
C2.updateFileUploadButton(s);
|
||||||
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1420,10 +1446,183 @@
|
|||||||
// 文件管理
|
// 文件管理
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
C2.normalizeFilePath = function(path) {
|
||||||
|
var p = path == null ? '.' : String(path).trim();
|
||||||
|
if (!p || p === '/') return '.';
|
||||||
|
p = p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/+$/, '');
|
||||||
|
return p || '.';
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.joinFilePath = function(base, name) {
|
||||||
|
var b = C2.normalizeFilePath(base);
|
||||||
|
var n = String(name || '').trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
||||||
|
if (!n) return b;
|
||||||
|
if (b === '.' || b === '/') return n;
|
||||||
|
return b + '/' + n;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 将相对浏览路径解析为 implant 工作目录下的绝对路径 */
|
||||||
|
C2.resolvePathAgainstPwd = function(pwd, rel) {
|
||||||
|
var base = String(pwd || '').trim().replace(/\\/g, '/').replace(/\/+$/, '');
|
||||||
|
if (!base) base = '/';
|
||||||
|
if (!base.startsWith('/')) base = '/' + base;
|
||||||
|
var parts = String(rel || '.').replace(/\\/g, '/').split('/');
|
||||||
|
var stack = base === '/' ? [] : base.split('/').filter(Boolean);
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
var p = parts[i];
|
||||||
|
if (!p || p === '.') continue;
|
||||||
|
if (p === '..') {
|
||||||
|
if (stack.length) stack.pop();
|
||||||
|
} else {
|
||||||
|
stack.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '/' + stack.join('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.resolveRemotePath = function(browsePath, filename) {
|
||||||
|
var joined = C2.joinFilePath(browsePath || '.', filename);
|
||||||
|
if (!C2.implantPwd) return joined;
|
||||||
|
return C2.resolvePathAgainstPwd(C2.implantPwd, joined);
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.updateFileBreadcrumb = function(browsePath) {
|
||||||
|
var breadcrumb = document.getElementById('c2-current-path');
|
||||||
|
if (!breadcrumb) return;
|
||||||
|
var rel = C2.normalizeFilePath(browsePath || '.');
|
||||||
|
if (C2.implantPwd) {
|
||||||
|
breadcrumb.textContent = C2.resolvePathAgainstPwd(C2.implantPwd, rel);
|
||||||
|
breadcrumb.title = rel;
|
||||||
|
} else {
|
||||||
|
breadcrumb.textContent = rel;
|
||||||
|
breadcrumb.title = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.parseLsLine = function(line) {
|
||||||
|
var trimmed = String(line || '').trim();
|
||||||
|
if (!trimmed || /^total\s+\d+/i.test(trimmed)) return null;
|
||||||
|
|
||||||
|
// Beacon 结构化输出:type\tmode\tsize\tname
|
||||||
|
var beaconParts = trimmed.split('\t');
|
||||||
|
if (beaconParts.length >= 4) {
|
||||||
|
var bName = beaconParts.slice(3).join('\t').trim();
|
||||||
|
var bMode = beaconParts[1].trim();
|
||||||
|
var bType = beaconParts[0].trim();
|
||||||
|
if (bName && bName !== '.' && bName !== '..') {
|
||||||
|
return {
|
||||||
|
mode: bMode || bType,
|
||||||
|
size: beaconParts[2].trim(),
|
||||||
|
name: bName,
|
||||||
|
isDir: bType.charAt(0) === 'd' || bMode.charAt(0) === 'd'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原生 ls -l 输出
|
||||||
|
var m = trimmed.match(/^(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/);
|
||||||
|
if (!m) return null;
|
||||||
|
var name = m[9].trim();
|
||||||
|
var arrow = name.indexOf(' -> ');
|
||||||
|
if (arrow > 0) name = name.slice(0, arrow).trim();
|
||||||
|
if (!name || name === '.' || name === '..') return null;
|
||||||
|
return {
|
||||||
|
mode: m[1],
|
||||||
|
size: m[5],
|
||||||
|
name: name,
|
||||||
|
isDir: m[1].charAt(0) === 'd'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.isDownloadShellError = function(text) {
|
||||||
|
var lower = String(text || '').toLowerCase();
|
||||||
|
return lower.indexOf('c2_download_err:') >= 0 ||
|
||||||
|
lower.indexOf('no such file') >= 0 ||
|
||||||
|
lower.indexOf('permission denied') >= 0 ||
|
||||||
|
lower.indexOf('is a directory') >= 0 ||
|
||||||
|
lower.indexOf('cannot open') >= 0 ||
|
||||||
|
lower.indexOf('not a regular file') >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.refreshImplantPwd = function(sessionId, callback) {
|
||||||
|
if (!sessionId) {
|
||||||
|
if (callback) callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apiRequest('POST', `${API_BASE}/tasks`, {
|
||||||
|
session_id: sessionId,
|
||||||
|
task_type: 'pwd',
|
||||||
|
payload: {}
|
||||||
|
}).then(function(data) {
|
||||||
|
if (data.error) {
|
||||||
|
if (callback) callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var taskId = data.task && data.task.id ? data.task.id : data.task_id;
|
||||||
|
if (!taskId) {
|
||||||
|
if (callback) callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
C2.waitForImplantPwd(taskId, callback);
|
||||||
|
}).catch(function() {
|
||||||
|
if (callback) callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.waitForImplantPwd = function(taskId, callback) {
|
||||||
|
var attempts = 0;
|
||||||
|
var poll = function() {
|
||||||
|
if (++attempts > 30) {
|
||||||
|
if (callback) callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apiRequest('GET', `${API_BASE}/tasks/${taskId}`).then(function(data) {
|
||||||
|
var task = data.task;
|
||||||
|
if (task && task.status === 'success' && task.resultText) {
|
||||||
|
C2.implantPwd = String(task.resultText).trim().split('\n').pop().trim();
|
||||||
|
C2.updateFileBreadcrumb(C2.currentPath);
|
||||||
|
if (callback) callback();
|
||||||
|
} else if (task && task.status === 'failed') {
|
||||||
|
if (callback) callback();
|
||||||
|
} else {
|
||||||
|
setTimeout(poll, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.getParentFilePath = function(path) {
|
||||||
|
var p = C2.normalizeFilePath(path);
|
||||||
|
if (p === '.' || p === '/') return '.';
|
||||||
|
var idx = p.lastIndexOf('/');
|
||||||
|
if (idx < 0) return '.';
|
||||||
|
var parent = p.slice(0, idx);
|
||||||
|
return parent || '.';
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.goToParentDirectory = function() {
|
||||||
|
var parent = C2.getParentFilePath(C2.currentPath || '.');
|
||||||
|
C2.loadFileList(null, parent);
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.openDirectory = function(name) {
|
||||||
|
var next = C2.joinFilePath(C2.currentPath || '.', name);
|
||||||
|
C2.loadFileList(null, next);
|
||||||
|
};
|
||||||
|
|
||||||
C2.loadFileList = function(sessionId, path) {
|
C2.loadFileList = function(sessionId, path) {
|
||||||
|
// 兼容误传:仅传路径时(如旧版 loadFileList('..'))自动纠正
|
||||||
|
if (sessionId && path == null && typeof sessionId === 'string' &&
|
||||||
|
(sessionId === '..' || sessionId === '.' || sessionId.indexOf('/') >= 0)) {
|
||||||
|
path = sessionId;
|
||||||
|
sessionId = null;
|
||||||
|
}
|
||||||
if (!sessionId) sessionId = C2.selectedSessionId;
|
if (!sessionId) sessionId = C2.selectedSessionId;
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
if (!path) path = C2.currentPath || '.';
|
if (!path) path = C2.currentPath || '.';
|
||||||
|
path = C2.normalizeFilePath(path);
|
||||||
|
|
||||||
const container = document.getElementById('c2-file-list');
|
const container = document.getElementById('c2-file-list');
|
||||||
const breadcrumb = document.getElementById('c2-current-path');
|
const breadcrumb = document.getElementById('c2-current-path');
|
||||||
@@ -1455,9 +1654,9 @@
|
|||||||
const task = data.task;
|
const task = data.task;
|
||||||
if (task && task.status === 'success') {
|
if (task && task.status === 'success') {
|
||||||
C2.currentPath = path;
|
C2.currentPath = path;
|
||||||
const breadcrumb = document.getElementById('c2-current-path');
|
C2.updateFileBreadcrumb(path);
|
||||||
if (breadcrumb) breadcrumb.textContent = path;
|
|
||||||
C2.renderFileList(task.resultText || '');
|
C2.renderFileList(task.resultText || '');
|
||||||
|
C2.refreshImplantPwd(sessionId);
|
||||||
} else if (task && task.status === 'failed') {
|
} else if (task && task.status === 'failed') {
|
||||||
if (container) container.innerHTML = `<div class="c2-error">${escapeHtml(task.error || c2t('c2.files.failed'))}</div>`;
|
if (container) container.innerHTML = `<div class="c2-error">${escapeHtml(task.error || c2t('c2.files.failed'))}</div>`;
|
||||||
} else {
|
} else {
|
||||||
@@ -1472,8 +1671,10 @@
|
|||||||
const container = document.getElementById('c2-file-list');
|
const container = document.getElementById('c2-file-list');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const lines = output.split('\n').filter(l => l.trim());
|
const entries = output.split('\n')
|
||||||
if (lines.length === 0) {
|
.map(C2.parseLsLine)
|
||||||
|
.filter(function(entry) { return entry != null; });
|
||||||
|
if (entries.length === 0) {
|
||||||
container.innerHTML = '<div class="c2-empty">' + escapeHtml(c2t('c2.files.emptyDir')) + '</div>';
|
container.innerHTML = '<div class="c2-empty">' + escapeHtml(c2t('c2.files.emptyDir')) + '</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1489,22 +1690,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${lines.map(line => {
|
${entries.map(function(entry) {
|
||||||
const parts = line.split(/\s+/);
|
|
||||||
const name = parts[parts.length - 1] || line;
|
|
||||||
const isDir = line.startsWith('d') || parts[0]?.startsWith?.('d');
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td class="c2-file-name">
|
<td class="c2-file-name">
|
||||||
<span class="c2-file-icon">${isDir ? '📁' : '📄'}</span>
|
<span class="c2-file-icon">${entry.isDir ? '📁' : '📄'}</span>
|
||||||
${escapeHtml(name)}
|
${escapeHtml(entry.name)}
|
||||||
</td>
|
</td>
|
||||||
<td>${parts[parts.length - 5] || '-'}</td>
|
<td>${escapeHtml(entry.size)}</td>
|
||||||
<td>${parts[parts.length - 4] || '-'}</td>
|
<td>${escapeHtml(entry.mode)}</td>
|
||||||
<td>
|
<td>
|
||||||
${isDir
|
${entry.isDir
|
||||||
? `<button class="btn-ghost btn-sm" onclick="C2.loadFileList(null, '${escapeHtml(name)}')">${escapeHtml(c2t('c2.files.open'))}</button>`
|
? `<button class="btn-ghost btn-sm" onclick='C2.openDirectory(${JSON.stringify(entry.name)})'>${escapeHtml(c2t('c2.files.open'))}</button>`
|
||||||
: `<button class="btn-ghost btn-sm" onclick="C2.downloadFile('${escapeHtml(name)}')">${escapeHtml(c2t('c2.files.download'))}</button>`
|
: `<button class="btn-ghost btn-sm" onclick='C2.downloadFile(${JSON.stringify(entry.name)})'>${escapeHtml(c2t('c2.files.download'))}</button>`
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1519,17 +1717,297 @@
|
|||||||
C2.loadFileList(null, C2.currentPath);
|
C2.loadFileList(null, C2.currentPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
C2.sessionTransport = function(session) {
|
||||||
|
if (!session || !session.metadata) return '';
|
||||||
|
return String(session.metadata.transport || '').toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.sessionSupportsUpload = function(session) {
|
||||||
|
if (!session) {
|
||||||
|
return { supported: false, reasonKey: 'c2.files.uploadUnsupported' };
|
||||||
|
}
|
||||||
|
if (session.implantUuid && String(session.implantUuid).indexOf('curl_') === 0) {
|
||||||
|
return { supported: false, reasonKey: 'c2.files.uploadCurlBeacon' };
|
||||||
|
}
|
||||||
|
var transport = C2.sessionTransport(session);
|
||||||
|
// 编译 Beacon:HTTP/HTTPS/TCP(CSB1) 均走二进制/结构化协议,支持 upload
|
||||||
|
if (transport === 'tcp_beacon' || transport === 'http_beacon' || transport === 'https_beacon') {
|
||||||
|
return { supported: true, reasonKey: '' };
|
||||||
|
}
|
||||||
|
// 经典 TCP 反弹 Shell(bash/nc,metadata.transport=tcp_reverse)
|
||||||
|
if (transport === 'tcp_reverse' || (session.hostname && String(session.hostname).indexOf('tcp_') === 0)) {
|
||||||
|
return { supported: false, reasonKey: 'c2.files.uploadTcpShell' };
|
||||||
|
}
|
||||||
|
return { supported: true, reasonKey: '' };
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.updateFileUploadButton = function(session) {
|
||||||
|
if (!session && C2.selectedSessionId) {
|
||||||
|
session = C2.sessions.find(function(s) { return s.id === C2.selectedSessionId; });
|
||||||
|
}
|
||||||
|
var btn = document.getElementById('c2-file-upload-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
var cap = C2.sessionSupportsUpload(session);
|
||||||
|
btn.disabled = !cap.supported || !!C2.fileUploadBusy;
|
||||||
|
btn.title = cap.supported ? c2t('c2.files.upload') : c2t(cap.reasonKey);
|
||||||
|
if (!cap.supported) {
|
||||||
|
btn.classList.add('is-disabled');
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('is-disabled');
|
||||||
|
}
|
||||||
|
var hint = document.getElementById('c2-file-upload-hint');
|
||||||
|
if (hint) {
|
||||||
|
if (!cap.supported) {
|
||||||
|
hint.hidden = false;
|
||||||
|
hint.textContent = c2t(cap.reasonKey);
|
||||||
|
} else {
|
||||||
|
hint.hidden = true;
|
||||||
|
hint.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.setFileUploadProgress = function(visible, percent, filename) {
|
||||||
|
var row = document.getElementById('c2-file-upload-progress');
|
||||||
|
if (!row) return;
|
||||||
|
if (!visible) {
|
||||||
|
row.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.hidden = false;
|
||||||
|
var fill = document.getElementById('c2-file-upload-progress-fill');
|
||||||
|
var label = document.getElementById('c2-file-upload-progress-label');
|
||||||
|
if (fill) fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
|
||||||
|
if (label) {
|
||||||
|
label.textContent = c2t('c2.files.uploading', { name: filename || '', percent: percent || 0 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.openFileUploadPicker = function() {
|
||||||
|
if (!C2.selectedSessionId || C2.fileUploadBusy) return;
|
||||||
|
var session = C2.sessions.find(function(s) { return s.id === C2.selectedSessionId; });
|
||||||
|
var cap = C2.sessionSupportsUpload(session);
|
||||||
|
if (!cap.supported) {
|
||||||
|
showToast(c2t(cap.reasonKey), 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var inp = document.getElementById('c2-file-upload-input');
|
||||||
|
if (inp) inp.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.onC2FileUploadPick = function(ev) {
|
||||||
|
var input = ev && ev.target;
|
||||||
|
var file = input && input.files && input.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
if (input) input.value = '';
|
||||||
|
C2.uploadFileToImplant(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.uploadFileToImplant = function(file) {
|
||||||
|
if (!C2.selectedSessionId || C2.fileUploadBusy || !file) return;
|
||||||
|
var sessionId = C2.selectedSessionId;
|
||||||
|
var remotePath = C2.resolveRemotePath(C2.currentPath || '.', file.name);
|
||||||
|
var uploadUrl = API_BASE + '/files/upload';
|
||||||
|
|
||||||
|
C2.fileUploadBusy = true;
|
||||||
|
C2.updateFileUploadButton();
|
||||||
|
C2.setFileUploadProgress(true, 0, file.name);
|
||||||
|
|
||||||
|
var form = new FormData();
|
||||||
|
form.append('session_id', sessionId);
|
||||||
|
form.append('remote_path', remotePath);
|
||||||
|
form.append('file', file);
|
||||||
|
|
||||||
|
var uploadPromise;
|
||||||
|
if (typeof apiUploadWithProgress === 'function') {
|
||||||
|
uploadPromise = apiUploadWithProgress(uploadUrl, form, {
|
||||||
|
onProgress: function(p) {
|
||||||
|
C2.setFileUploadProgress(true, Math.min(p.percent || 0, 50), file.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (typeof apiFetch === 'function') {
|
||||||
|
uploadPromise = apiFetch(uploadUrl, { method: 'POST', body: form });
|
||||||
|
} else {
|
||||||
|
uploadPromise = fetch(uploadUrl, { method: 'POST', body: form });
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadPromise.then(function(res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
return res.text().then(function(text) {
|
||||||
|
throw new Error(text || c2t('c2.files.failed'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}).then(function(uploadData) {
|
||||||
|
var fileId = uploadData && uploadData.file_id;
|
||||||
|
if (!fileId) throw new Error(c2t('c2.files.failed'));
|
||||||
|
C2.setFileUploadProgress(true, 55, file.name);
|
||||||
|
return apiRequest('POST', API_BASE + '/tasks', {
|
||||||
|
session_id: sessionId,
|
||||||
|
task_type: 'upload',
|
||||||
|
payload: { remote_path: remotePath, file_id: fileId }
|
||||||
|
});
|
||||||
|
}).then(function(taskData) {
|
||||||
|
if (taskData && taskData.error) throw new Error(taskData.error);
|
||||||
|
var taskId = taskData.task && taskData.task.id ? taskData.task.id : taskData.task_id;
|
||||||
|
if (!taskId) {
|
||||||
|
showToast(c2t('c2.files.uploadQueued'), 'success');
|
||||||
|
C2.fileUploadBusy = false;
|
||||||
|
C2.setFileUploadProgress(false);
|
||||||
|
C2.updateFileUploadButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (taskData.task && taskData.task.approvalStatus === 'pending') {
|
||||||
|
showToast(c2t('c2.files.uploadPendingApproval'), 'info');
|
||||||
|
}
|
||||||
|
C2.waitForFileUpload(taskId, file.name);
|
||||||
|
}).catch(function(err) {
|
||||||
|
showToast((err && err.message) || c2t('c2.files.failed'), 'error');
|
||||||
|
C2.fileUploadBusy = false;
|
||||||
|
C2.setFileUploadProgress(false);
|
||||||
|
C2.updateFileUploadButton();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.waitForFileUpload = function(taskId, filename) {
|
||||||
|
var attempts = 0;
|
||||||
|
var check = function() {
|
||||||
|
if (++attempts > 120) {
|
||||||
|
showToast(c2t('c2.files.timeout'), 'error');
|
||||||
|
C2.fileUploadBusy = false;
|
||||||
|
C2.setFileUploadProgress(false);
|
||||||
|
C2.updateFileUploadButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apiRequest('GET', API_BASE + '/tasks/' + taskId).then(function(data) {
|
||||||
|
var task = data.task;
|
||||||
|
if (task && task.approvalStatus === 'pending' && task.status === 'queued') {
|
||||||
|
C2.setFileUploadProgress(true, 60, filename);
|
||||||
|
setTimeout(check, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (task && task.status === 'success') {
|
||||||
|
C2.setFileUploadProgress(true, 100, filename);
|
||||||
|
showToast(c2t('c2.files.uploadOk'), 'success');
|
||||||
|
C2.fileUploadBusy = false;
|
||||||
|
setTimeout(function() { C2.setFileUploadProgress(false); }, 400);
|
||||||
|
C2.updateFileUploadButton();
|
||||||
|
C2.refreshFiles();
|
||||||
|
} else if (task && task.status === 'failed') {
|
||||||
|
showToast(task.error || task.resultText || c2t('c2.files.failed'), 'error');
|
||||||
|
C2.fileUploadBusy = false;
|
||||||
|
C2.setFileUploadProgress(false);
|
||||||
|
C2.updateFileUploadButton();
|
||||||
|
} else {
|
||||||
|
var pct = 60 + Math.min(35, Math.floor(attempts / 3));
|
||||||
|
C2.setFileUploadProgress(true, pct, filename);
|
||||||
|
setTimeout(check, 500);
|
||||||
|
}
|
||||||
|
}).catch(function() {
|
||||||
|
C2.fileUploadBusy = false;
|
||||||
|
C2.setFileUploadProgress(false);
|
||||||
|
C2.updateFileUploadButton();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.saveDownloadBlob = function(blob, filename) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.saveDownloadContent = function(content, filename) {
|
||||||
|
const text = String(content || '');
|
||||||
|
if (C2.isDownloadShellError(text)) {
|
||||||
|
throw new Error(text.trim() || c2t('c2.files.failed'));
|
||||||
|
}
|
||||||
|
const b64 = text.replace(/\s/g, '');
|
||||||
|
let bytes;
|
||||||
|
try {
|
||||||
|
if (/^[A-Za-z0-9+/=]+$/.test(b64) && b64.length > 0) {
|
||||||
|
const binary = atob(b64);
|
||||||
|
bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||||
|
} else {
|
||||||
|
bytes = new TextEncoder().encode(text);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
bytes = new TextEncoder().encode(text);
|
||||||
|
}
|
||||||
|
C2.saveDownloadBlob(new Blob([bytes], { type: 'application/octet-stream' }), filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.fetchTaskResultFile = function(taskId, filename) {
|
||||||
|
const url = `${API_BASE}/tasks/${taskId}/result-file`;
|
||||||
|
const fetchFn = (typeof apiFetch === 'function') ? apiFetch : fetch;
|
||||||
|
fetchFn(url).then(resp => {
|
||||||
|
if (!resp.ok) throw new Error('download failed: ' + resp.status);
|
||||||
|
return resp.blob();
|
||||||
|
}).then(blob => {
|
||||||
|
C2.saveDownloadBlob(blob, filename);
|
||||||
|
}).catch(err => {
|
||||||
|
showToast((err && err.message) || c2t('c2.files.failed'), 'error');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
C2.waitForFileDownload = function(taskId, filename) {
|
||||||
|
let attempts = 0;
|
||||||
|
const check = () => {
|
||||||
|
if (++attempts > 120) {
|
||||||
|
showToast(c2t('c2.files.timeout'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apiRequest('GET', `${API_BASE}/tasks/${taskId}`).then(data => {
|
||||||
|
const task = data.task;
|
||||||
|
if (task && task.status === 'success') {
|
||||||
|
if (task.resultBlobPath) {
|
||||||
|
C2.fetchTaskResultFile(taskId, filename);
|
||||||
|
} else if (task.resultText != null) {
|
||||||
|
try {
|
||||||
|
C2.saveDownloadContent(task.resultText, filename);
|
||||||
|
showToast(c2t('c2.files.downloadOk'), 'success');
|
||||||
|
} catch (err) {
|
||||||
|
showToast((err && err.message) || c2t('c2.files.failed'), 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
C2.saveDownloadBlob(new Blob([], { type: 'application/octet-stream' }), filename);
|
||||||
|
showToast(c2t('c2.files.downloadOk'), 'success');
|
||||||
|
}
|
||||||
|
} else if (task && task.status === 'failed') {
|
||||||
|
showToast(task.error || task.resultText || c2t('c2.files.failed'), 'error');
|
||||||
|
} else {
|
||||||
|
setTimeout(check, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
};
|
||||||
|
|
||||||
C2.downloadFile = function(filename) {
|
C2.downloadFile = function(filename) {
|
||||||
if (!C2.selectedSessionId) return;
|
if (!C2.selectedSessionId) return;
|
||||||
const remotePath = C2.currentPath === '/' ? '/' + filename : C2.currentPath + '/' + filename;
|
const remotePath = C2.resolveRemotePath(C2.currentPath || '.', filename);
|
||||||
|
|
||||||
apiRequest('POST', `${API_BASE}/tasks`, {
|
apiRequest('POST', `${API_BASE}/tasks`, {
|
||||||
session_id: C2.selectedSessionId,
|
session_id: C2.selectedSessionId,
|
||||||
task_type: 'download',
|
task_type: 'download',
|
||||||
payload: { remote_path: remotePath }
|
payload: { remote_path: remotePath }
|
||||||
}).then(data => {
|
}).then(data => {
|
||||||
if (data.error) showToast(data.error, 'error');
|
if (data.error) {
|
||||||
else showToast(c2t('c2.payloads.toastDownloadQueued'), 'success');
|
showToast(data.error, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const taskId = data.task?.id || data.task_id;
|
||||||
|
if (!taskId) {
|
||||||
|
showToast(c2t('c2.payloads.toastDownloadQueued'), 'success');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
C2.waitForFileDownload(taskId, filename);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user