mirror of
https://github.com/Mr-In4inci3le/sliver-gui
synced 2026-08-09 12:14:37 +00:00
Add Script Manager + Operator Panels; overhaul pivot/jump graph
- Script Manager (scripts.go): 30+ lateral-move/privesc/persistence/creds/enum recipes, dry-run preview, MITRE ATT&CK + OpSec labels, spawn with listener pre-flight + post-spawn verification - Operator Panels (panels.go): file/process browser, kill-chain tracker, engagement timer, IOC tracker, cleanup + report generators - Graph: correct pivot direction (session-id), jump/lateral lineage, firewall->root edges, working Reset Layout, tidy arrow spacing - Safe native file-dialog wrappers; spawn beacon-interval fix; README Operator Toolkit section
This commit is contained in:
@@ -46,10 +46,6 @@ directly: no grpc-web proxy, no protocol reimplementation, no drift when upstrea
|
||||
<img width="1912" height="832" alt="image" src="https://github.com/user-attachments/assets/5db41809-17f4-40e2-bf7c-20a5a4901666" />
|
||||
<br><br>
|
||||
<img width="1912" height="756" alt="image" src="https://github.com/user-attachments/assets/b779ca54-7b43-4d0d-b066-66e25d7f73aa" />
|
||||
<br><br>
|
||||
<img width="1912" height="802" alt="image" src="https://github.com/user-attachments/assets/e43bcdc2-3c84-4ad2-8010-aa43c88d4b5d" />
|
||||
<br><br>
|
||||
<img width="1913" height="807" alt="Screenshot 2026-07-18 014308" src="https://github.com/user-attachments/assets/5780d98a-8e5e-4683-90bc-3649587d4b00" />
|
||||
|
||||
---
|
||||
|
||||
@@ -86,6 +82,43 @@ kill-session · kill-beacon · version · mtls/http/https/dns/wg`
|
||||
|
||||
---
|
||||
|
||||
## Operator Toolkit — Script Manager, Panels & Pivot Graph
|
||||
|
||||
> A cohesive operator layer on top of Sliver's RPCs — **designed and built by
|
||||
> [Mr-In4inci3le](https://github.com/Mr-In4inci3le) aka Raj Kumar Mullapudi.** No C2 logic is
|
||||
> reimplemented; every action composes existing Sliver RPCs.
|
||||
|
||||
### Script Manager
|
||||
One-click post-exploitation **recipes** (Cobalt-Strike-style jump / spawn / remote-exec), each with a
|
||||
**dry-run preview**, a **MITRE ATT&CK ID**, and an **OpSec noise** rating:
|
||||
|
||||
- **Spawn** — `spawn <os> <arch> <profile>` builds + runs a fresh agent on the current host, with a
|
||||
pre-flight **listener check** and **post-spawn verification** (reports the real new agent ID).
|
||||
- **Lateral Movement** — SSH deploy, PsExec, WMI, WinRM, SC, SMB (`jump ssh|psexec|winrm|wmi`).
|
||||
- **Privilege Escalation** — Linux (sudo / SUID), Windows (privesc check, token impersonation,
|
||||
GetSystem, UAC bypass).
|
||||
- **Persistence** — cron · SSH-key · systemd · Registry Run · scheduled task · service · WMI · startup.
|
||||
- **Credentials** — Linux & Windows harvest, Kerberoast, DCSync (harvested creds routed to Loot).
|
||||
- **Enumeration** — network scan, Active Directory enumeration, local Windows enum.
|
||||
|
||||
### Operator Panels
|
||||
- **File Browser** — visual remote filesystem (list / delete).
|
||||
- **Process Browser** — visual process list + kill.
|
||||
- **Kill-Chain Tracker** — record and visualise the current engagement stage.
|
||||
- **Engagement Timer** — start / elapsed stopwatch.
|
||||
- **IOC Tracker** — log indicators as you drop them (files, registry keys, services).
|
||||
- **Cleanup Script Generator** — emit a teardown script from the tracked IOCs.
|
||||
- **Engagement Report Generator** — one-click engagement report.
|
||||
|
||||
### Interactive Pivot / Jump Graph
|
||||
A Cobalt-Strike-style topology view: a **firewall / egress boundary** on the left, agents laid out
|
||||
left-to-right in their **real pivot order** (chains joined by session id so same-host pivots render
|
||||
correctly), edges colour-coded by agent (**green** session · **red** SYSTEM/privileged · **blue**
|
||||
beacon), and **orange dashed lateral-move edges** that show exactly where each jump came from.
|
||||
Drag nodes · scroll-zoom · pan · per-teamserver saved layout · one-click **Reset Layout**.
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
**Prerequisites**
|
||||
|
||||
@@ -64,8 +64,36 @@ func (a *App) shutdown(ctx context.Context) {
|
||||
|
||||
// ─── Connection ───────────────────────────────────────────────────────────────
|
||||
|
||||
// safeOpenFileDialog wraps runtime.OpenFileDialog with a nil-ctx check and panic
|
||||
// recovery to prevent app crashes if native OS file dialogs fail on Windows/Linux.
|
||||
func (a *App) safeOpenFileDialog(opts runtime.OpenDialogOptions) (res string, err error) {
|
||||
if a.ctx == nil {
|
||||
return "", fmt.Errorf("application context not initialized")
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("file dialog error: %v", r)
|
||||
}
|
||||
}()
|
||||
return runtime.OpenFileDialog(a.ctx, opts)
|
||||
}
|
||||
|
||||
// safeSaveFileDialog wraps runtime.SaveFileDialog with a nil-ctx check and panic
|
||||
// recovery to prevent app crashes if native OS file dialogs fail on Windows/Linux.
|
||||
func (a *App) safeSaveFileDialog(opts runtime.SaveDialogOptions) (res string, err error) {
|
||||
if a.ctx == nil {
|
||||
return "", fmt.Errorf("application context not initialized")
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("file dialog error: %v", r)
|
||||
}
|
||||
}()
|
||||
return runtime.SaveFileDialog(a.ctx, opts)
|
||||
}
|
||||
|
||||
func (a *App) PickConfigFile() (string, error) {
|
||||
return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
return a.safeOpenFileDialog(runtime.OpenDialogOptions{
|
||||
Title: "Select Sliver operator config (.cfg)",
|
||||
Filters: []runtime.FileFilter{{DisplayName: "Sliver Config (*.cfg)", Pattern: "*.cfg"}},
|
||||
})
|
||||
@@ -868,7 +896,7 @@ func (a *App) ProcessDump(sessionID string, pid int32) TransferResult {
|
||||
if err != nil {
|
||||
return TransferResult{Error: err.Error()}
|
||||
}
|
||||
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
|
||||
DefaultFilename: fmt.Sprintf("procdump_%d.dmp", pid),
|
||||
Title: "Save process dump",
|
||||
})
|
||||
@@ -918,7 +946,7 @@ func (a *App) DownloadFile(sessionID, remotePath string) TransferResult {
|
||||
if err != nil {
|
||||
return TransferResult{Error: "decode: " + err.Error()}
|
||||
}
|
||||
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
|
||||
DefaultFilename: filepath.Base(remotePath),
|
||||
Title: "Save downloaded file",
|
||||
})
|
||||
@@ -950,7 +978,7 @@ func (a *App) UploadFile(sessionID, remotePath string) TransferResult {
|
||||
if err != nil {
|
||||
return TransferResult{Error: err.Error()}
|
||||
}
|
||||
localPath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
localPath, err := a.safeOpenFileDialog(runtime.OpenDialogOptions{
|
||||
Title: "Select file to upload",
|
||||
})
|
||||
if err != nil || localPath == "" {
|
||||
@@ -1295,7 +1323,7 @@ func (a *App) RegenerateBuild(name string) TransferResult {
|
||||
if resp.File == nil {
|
||||
return TransferResult{Error: "no stored build for " + name}
|
||||
}
|
||||
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
|
||||
DefaultFilename: resp.File.Name,
|
||||
Title: "Save regenerated implant",
|
||||
})
|
||||
@@ -1660,7 +1688,7 @@ func (a *App) DownloadLoot(lootID string) TransferResult {
|
||||
if resp.File == nil {
|
||||
return TransferResult{Error: "this loot item has no file content"}
|
||||
}
|
||||
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
|
||||
DefaultFilename: resp.File.Name,
|
||||
Title: "Save loot",
|
||||
})
|
||||
@@ -2522,7 +2550,7 @@ type AssemblyResult struct {
|
||||
// an explicit path (execute-assembly <path>) or fall back to a picker.
|
||||
func (a *App) readLocalOrDialog(localPath, title string) ([]byte, error) {
|
||||
if localPath == "" {
|
||||
p, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{Title: title})
|
||||
p, err := a.safeOpenFileDialog(runtime.OpenDialogOptions{Title: title})
|
||||
if err != nil || p == "" {
|
||||
return nil, fmt.Errorf("selection cancelled")
|
||||
}
|
||||
|
||||
+3
-3
@@ -210,7 +210,7 @@ func (a *App) RegistryReadHiveExport(sessionID, rootHive, requestedHive string)
|
||||
data = dec
|
||||
}
|
||||
}
|
||||
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
|
||||
DefaultFilename: strings.ToLower(rootHive) + ".hive",
|
||||
Title: "Save registry hive",
|
||||
})
|
||||
@@ -959,7 +959,7 @@ func (a *App) ConvertDLLToShellcode(dllPath, functionName, args string) (string,
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
|
||||
DefaultFilename: strings.TrimSuffix(filepath.Base(dllPath), filepath.Ext(dllPath)) + ".bin",
|
||||
Title: "Save shellcode",
|
||||
})
|
||||
@@ -999,7 +999,7 @@ func (a *App) EncodeShellcode(inPath, architecture string, iterations int) (stri
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
|
||||
DefaultFilename: strings.TrimSuffix(filepath.Base(inPath), filepath.Ext(inPath)) + ".enc.bin",
|
||||
Title: "Save encoded shellcode",
|
||||
})
|
||||
|
||||
Vendored
+16
-8
@@ -39,6 +39,10 @@
|
||||
<button class="tb-btn" data-view="creds">Creds</button>
|
||||
<button class="tb-btn" data-view="hosts">Hosts</button>
|
||||
<button class="tb-btn" data-view="operators">Operators</button>
|
||||
<button class="tb-btn" data-view="scripts">Scripts</button>
|
||||
<button class="tb-btn" data-view="iocs">IOCs</button>
|
||||
<button class="tb-btn" data-view="report">Report</button>
|
||||
<button class="tb-btn" data-view="c2profiles">C2 Profiles</button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<span id="server-version" class="version-tag"></span>
|
||||
@@ -67,8 +71,8 @@
|
||||
<div class="table-wrap">
|
||||
<table class="data-table" id="agents-table">
|
||||
<thead><tr>
|
||||
<th>Type</th><th>Name / ID</th><th>Host</th><th>User</th><th>OS/Arch</th>
|
||||
<th>PID</th><th>Transport</th><th>Remote</th><th>Last Checkin</th><th>Status</th>
|
||||
<th>Type</th><th>Name / ID</th><th>Host</th><th>User</th><th>Remote IP</th><th>OS/Arch</th>
|
||||
<th>PID</th><th>Transport</th><th>Last Checkin</th><th>Status</th>
|
||||
</tr></thead>
|
||||
<tbody id="agents-body"></tbody>
|
||||
</table>
|
||||
@@ -79,14 +83,16 @@
|
||||
<div id="graph-view" class="panel-content hidden">
|
||||
<svg id="graph-svg" xmlns="http://www.w3.org/2000/svg"></svg>
|
||||
<div class="graph-legend">
|
||||
<span class="leg-item"><span class="leg-line priv"></span> Privileged (red)</span>
|
||||
<span class="leg-item"><span class="leg-line user"></span> User session (green)</span>
|
||||
<span class="leg-item"><span class="leg-line beacon"></span> Beacon (blue dashed)</span>
|
||||
<span class="leg-item"><span class="leg-line dead"></span> Dead (grey)</span>
|
||||
<span class="leg-item"><span class="leg-line" style="border-top:3px solid #35c46b"></span> User session</span>
|
||||
<span class="leg-item"><span class="leg-line" style="border-top:3px solid #e23c4e"></span> Privileged session</span>
|
||||
<span class="leg-item"><span class="leg-line" style="border-top:3px solid #4d9fe6"></span> Beacon</span>
|
||||
<span class="leg-item"><span class="leg-line" style="border-top:3px dashed #f5a623"></span> Jump / Lateral move</span>
|
||||
<span class="leg-item"><span class="leg-line" style="border-top:3px solid #6b7280;opacity:.6"></span> Dead agent</span>
|
||||
<span class="sep">|</span>
|
||||
<span class="leg-item">OS logo + integrity shown per node</span>
|
||||
<span class="leg-item"><span style="display:inline-block;width:10px;height:10px;border:1.5px dashed #35c46b;border-radius:2px;vertical-align:middle;margin-right:4px"></span> Active selection</span>
|
||||
<span class="leg-item"><span style="display:inline-block;width:10px;height:10px;border:1.2px dashed #f5a623;border-radius:2px;vertical-align:middle;margin-right:4px"></span> Jump-linked node</span>
|
||||
<span class="sep">|</span>
|
||||
<span class="leg-item">drag nodes · scroll to zoom · drag bg to pan</span>
|
||||
<span class="leg-item" style="color:var(--muted)">drag · scroll zoom · drag bg to pan</span>
|
||||
<span class="spacer"></span>
|
||||
<button id="graph-reset-btn" class="btn small">Reset Layout</button>
|
||||
</div>
|
||||
@@ -151,6 +157,8 @@
|
||||
<!-- Context menu -->
|
||||
<div id="ctx-menu" class="ctx-menu hidden">
|
||||
<div class="ctx-item" id="ctx-interact">Interact</div>
|
||||
<div class="ctx-item" id="ctx-files">File Browser</div>
|
||||
<div class="ctx-item" id="ctx-processes">Process Browser</div>
|
||||
<div class="ctx-item" id="ctx-integrity">Check Integrity</div>
|
||||
<div class="ctx-item" id="ctx-rename">Rename</div>
|
||||
<div class="ctx-divider"></div>
|
||||
|
||||
Vendored
+1558
-108
File diff suppressed because it is too large
Load Diff
Vendored
+300
-1
@@ -192,7 +192,7 @@ input::placeholder{color:var(--muted);}
|
||||
.check-label{display:flex;align-items:center;gap:7px;font-size:13px;color:var(--text-dim);cursor:pointer;}
|
||||
.modal-body .btn.accent{padding:10px 18px;font-size:13px;font-weight:700;}
|
||||
.gen-status{font-size:12px;color:var(--text-dim);display:flex;align-items:center;gap:8px;}
|
||||
.spinner{width:13px;height:13px;border:2px solid var(--border-hi);border-top-color:var(--accent);border-radius:50%;animation:spin 1s steps(12) infinite;}
|
||||
.spinner{width:13px;height:13px;border:2px solid var(--border-hi);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite;}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.result-box{font-size:12px;font-family:var(--mono);}
|
||||
.shell-out{flex:1;overflow-y:auto;margin:0;padding:10px 12px;font-family:var(--mono);font-size:12.5px;line-height:1.55;background:#08090e;color:var(--text);white-space:pre-wrap;word-break:break-word;min-height:0;}
|
||||
@@ -232,3 +232,302 @@ input::placeholder{color:var(--muted);}
|
||||
.status-msg{font-size:11px;margin-top:6px;}
|
||||
.status-msg.ok{color:var(--ok);}
|
||||
.status-msg.err{color:var(--accent);}
|
||||
|
||||
/* ── Script Manager Panel ──────────────────────────────────────────────────── */
|
||||
.scripts-panel { padding: 8px; display: flex; flex-direction: column; gap: 14px; height: 100%; box-sizing: border-border-box; }
|
||||
.scr-top-bar { display: flex; gap: 12px; align-items: center; background: var(--panel-alt); padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; }
|
||||
.scr-main-layout { display: grid; grid-template-columns: minmax(360px, 1.2fr) minmax(320px, 1fr); gap: 16px; flex: 1; min-height: 0; }
|
||||
.scr-left-pane { display: flex; flex-direction: column; gap: 14px; overflow-y: auto; padding-right: 4px; }
|
||||
.scr-right-pane { display: flex; flex-direction: column; gap: 10px; min-height: 0; }
|
||||
.scr-input { width: 100%; padding: 7px 10px; background: var(--bg); border: 1px solid var(--border); color: var(--text); border-radius: 6px; font-size: 12px; margin-top: 4px; font-family: var(--font); box-sizing: border-box; }
|
||||
.scr-input:focus { border-color: var(--cyan); outline: none; box-shadow: 0 0 0 2px rgba(56,189,248,0.2); }
|
||||
.scr-categories { display: flex; flex-direction: column; gap: 16px; }
|
||||
.scr-cat { background: var(--panel-alt); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; }
|
||||
.scr-cat h4 { color: var(--cyan); font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 10px; font-weight: 700; display: flex; align-items: center; gap: 6px; }
|
||||
.scr-cat h4::before { content: ""; width: 3px; height: 10px; background: var(--cyan); border-radius: 2px; }
|
||||
.scr-btns { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.scr-btn { display: inline-flex; align-items: center; gap: 8px; padding: 7px 12px; background: var(--panel-hi); border: 1px solid var(--border); color: var(--text-dim); border-radius: 6px; font-size: 11.5px; cursor: pointer; transition: all 0.2s ease; }
|
||||
.scr-btn:hover { background: var(--panel-elev); color: var(--text); border-color: var(--cyan); transform: translateY(-1px); }
|
||||
.scr-btn.active { background: var(--panel-elev); color: var(--text); border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 4px 12px rgba(0,0,0,0.3); }
|
||||
.scr-btn:disabled { opacity: 0.5; cursor: wait; }
|
||||
.badge-attck { background: rgba(56,188,203,0.12); color: var(--cyan); border: 1px solid rgba(56,188,203,0.3); font-family: var(--mono); font-size: 9.5px; padding: 2px 5px; border-radius: 4px; }
|
||||
.badge-opsec { font-size: 9.5px; padding: 2px 6px; border-radius: 4px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
.opsec-low { background: rgba(53,196,107,0.12); color: var(--ok); border: 1px solid rgba(53,196,107,0.3); }
|
||||
.opsec-medium { background: rgba(224,163,58,0.12); color: var(--warn); border: 1px solid rgba(224,163,58,0.3); }
|
||||
.opsec-high { background: var(--accent-dim); color: var(--accent); border: 1px solid rgba(226,60,78,0.4); }
|
||||
.scr-param-card { background: var(--panel-alt); border: 1px solid var(--border-hi); border-radius: 8px; padding: 14px 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.25); }
|
||||
.scr-param-card h3 { font-size: 13.5px; color: var(--text); margin-bottom: 4px; display: flex; align-items: center; gap: 8px; font-weight: 600; }
|
||||
.scr-param-card p { font-size: 11.5px; color: var(--text-dim); margin-bottom: 14px; line-height: 1.4; }
|
||||
.scr-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; margin-bottom: 14px; }
|
||||
.scr-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
.scr-action-btn { padding: 8px 16px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; border: 1px solid transparent; transition: all 0.2s ease; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.scr-btn-preview { background: var(--panel-hi); color: var(--cyan); border-color: var(--border-hi); }
|
||||
.scr-btn-preview:hover { border-color: var(--cyan); background: var(--panel-elev); shadow: 0 2px 8px rgba(56,189,248,0.2); }
|
||||
.scr-btn-exec { background: var(--accent); color: #0d1117; }
|
||||
.scr-btn-exec:hover { filter: brightness(1.15); box-shadow: 0 2px 10px rgba(53,196,107,0.3); }
|
||||
.scr-output-wrap { display: flex; flex-direction: column; flex: 1; height: 100%; min-height: 0; background: var(--panel-alt); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
|
||||
.scr-output { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 12px; font-family: var(--mono); font-size: 11.5px; color: var(--text); flex: 1; overflow-y: auto; white-space: pre-wrap; word-break: break-word; line-height: 1.55; margin: 0; }
|
||||
|
||||
/* ── Console-Pinned Script Manager ───────────────────────────────────────── */
|
||||
.console-script-bar {
|
||||
background: var(--panel-elev, #161b22);
|
||||
border-bottom: 1px solid var(--border, #30363d);
|
||||
padding: 8px 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.csb-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.csb-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.csb-badge {
|
||||
background: var(--accent, #4ade80);
|
||||
color: #0d1117;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.csb-target {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim, #8b949e);
|
||||
font-family: var(--mono, monospace);
|
||||
}
|
||||
.csb-cats {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.csb-cat-tab {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: var(--muted, #8b949e);
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.csb-cat-tab:hover {
|
||||
color: var(--text, #e6edf3);
|
||||
background: var(--panel-hi, #2a2e3b);
|
||||
}
|
||||
.csb-cat-tab.active {
|
||||
color: var(--cyan, #38bdf8);
|
||||
background: var(--panel-hi, #2a2e3b);
|
||||
border-color: rgba(56, 189, 248, 0.3);
|
||||
}
|
||||
.csb-toggle-btn {
|
||||
background: var(--bg-3, #252535);
|
||||
border: 1px solid var(--border, #30363d);
|
||||
color: var(--text-dim, #8b949e);
|
||||
font-size: 10.5px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.csb-toggle-btn:hover {
|
||||
color: var(--text, #e6edf3);
|
||||
}
|
||||
.csb-body {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(48, 54, 61, 0.5);
|
||||
}
|
||||
.csb-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.csb-recipe-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-3, #252535);
|
||||
border: 1px solid var(--border, #30363d);
|
||||
color: var(--fg, #e0e0e0);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.csb-recipe-btn:hover {
|
||||
border-color: var(--cyan, #38bdf8);
|
||||
background: var(--panel-hi, #2a2e3b);
|
||||
}
|
||||
.csb-recipe-btn.active {
|
||||
border-color: var(--accent, #4ade80);
|
||||
background: var(--panel-hi, #2a2e3b);
|
||||
}
|
||||
.csb-card {
|
||||
background: var(--panel-alt, #0d1117);
|
||||
border: 1px solid var(--border, #30363d);
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.csb-card-header h4 {
|
||||
font-size: 12px;
|
||||
color: var(--text, #e6edf3);
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.csb-card-header p {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim, #8b949e);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.csb-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.csb-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.csb-btn {
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.csb-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.csb-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: wait;
|
||||
}
|
||||
.csb-btn-preview {
|
||||
background: var(--bg-3, #252535);
|
||||
color: var(--cyan, #38bdf8);
|
||||
border: 1px solid var(--border, #30363d);
|
||||
}
|
||||
.csb-btn-exec {
|
||||
background: var(--accent, #4ade80);
|
||||
color: #0d1117;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* ── File Browser (Explorer-style) ────────────────────────────────────────── */
|
||||
.file-browser { font-family: var(--font, 'Segoe UI', sans-serif); font-size: 12.5px; display: flex; flex-direction: column; height: 100%; }
|
||||
.fb-toolbar { display: flex; align-items: center; gap: 4px; padding: 5px 8px; background: linear-gradient(180deg, rgba(30,33,44,1) 0%, rgba(22,24,32,1) 100%); border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
||||
.fb-toolbar .fb-nav-btn { width: 28px; height: 26px; display: flex; align-items: center; justify-content: center; background: none; border: 1px solid transparent; border-radius: 4px; color: var(--text-dim); cursor: pointer; font-size: 14px; transition: all .12s; }
|
||||
.fb-toolbar .fb-nav-btn:hover { background: var(--panel-alt); border-color: var(--border); color: var(--text); }
|
||||
.fb-toolbar .fb-nav-btn:active { background: var(--panel-hi); }
|
||||
.fb-toolbar .fb-nav-btn[disabled] { opacity: .3; pointer-events: none; }
|
||||
.fb-addressbar { flex: 1; display: flex; align-items: center; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 0 8px; height: 26px; overflow: hidden; }
|
||||
.fb-addressbar .fb-crumb { color: var(--text-dim); font-size: 11.5px; cursor: pointer; padding: 2px 3px; border-radius: 3px; white-space: nowrap; transition: color .1s, background .1s; }
|
||||
.fb-addressbar .fb-crumb:hover { color: var(--text); background: var(--panel-alt); }
|
||||
.fb-addressbar .fb-sep { color: var(--muted); font-size: 10px; margin: 0 1px; user-select: none; }
|
||||
.fb-col-header { display: grid; grid-template-columns: 28px 1fr 90px 100px; padding: 4px 10px; background: var(--panel); border-bottom: 1px solid var(--border-hi); color: var(--muted); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; flex-shrink: 0; user-select: none; }
|
||||
.fb-col-header span { padding: 2px 0; }
|
||||
.fb-list { flex: 1; overflow-y: auto; overflow-x: hidden; }
|
||||
.fb-item { display: grid; grid-template-columns: 28px 1fr 90px 100px; align-items: center; padding: 3px 10px; cursor: default; border-bottom: 1px solid rgba(42,46,59,.25); transition: background .08s; user-select: none; min-height: 26px; }
|
||||
.fb-item:nth-child(even) { background: rgba(255,255,255,.015); }
|
||||
.fb-item:hover { background: rgba(77,159,230,.08); }
|
||||
.fb-item.selected { background: rgba(77,159,230,.18); outline: 1px solid rgba(77,159,230,.35); outline-offset: -1px; }
|
||||
.fb-item .fb-icon { font-size: 16px; text-align: center; line-height: 1; }
|
||||
.fb-item .fb-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-left: 4px; }
|
||||
.fb-item.dir .fb-name { color: var(--info, #4d9fe6); font-weight: 600; }
|
||||
.fb-item .fb-size { color: var(--text-dim); font-size: 11px; text-align: right; font-family: var(--mono); }
|
||||
.fb-item .fb-date { color: var(--muted); font-size: 10.5px; }
|
||||
.fb-status { padding: 4px 10px; background: var(--panel); border-top: 1px solid var(--border); color: var(--muted); font-size: 10.5px; flex-shrink: 0; display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
/* ── Process Browser (Task Manager-style) ─────────────────────────────────── */
|
||||
.proc-browser { font-family: var(--font, 'Segoe UI', sans-serif); font-size: 12px; display: flex; flex-direction: column; height: 100%; }
|
||||
.proc-toolbar { display: flex; align-items: center; gap: 6px; padding: 5px 8px; background: linear-gradient(180deg, rgba(30,33,44,1) 0%, rgba(22,24,32,1) 100%); border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
||||
.proc-toolbar .proc-search { flex: 1; max-width: 240px; height: 26px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font-size: 11.5px; padding: 0 8px; font-family: var(--font); outline: none; transition: border-color .12s; }
|
||||
.proc-toolbar .proc-search:focus { border-color: var(--accent); }
|
||||
.proc-toolbar .proc-search::placeholder { color: var(--muted); }
|
||||
.proc-toolbar .proc-tb-btn { height: 26px; padding: 0 10px; background: none; border: 1px solid var(--border); border-radius: 4px; color: var(--text-dim); cursor: pointer; font-size: 11px; font-weight: 600; transition: all .12s; display: flex; align-items: center; gap: 4px; }
|
||||
.proc-toolbar .proc-tb-btn:hover { background: var(--panel-alt); border-color: var(--muted); color: var(--text); }
|
||||
.proc-toolbar .proc-tb-btn.danger { color: var(--accent); border-color: rgba(226,60,78,.3); }
|
||||
.proc-toolbar .proc-tb-btn.danger:hover { background: rgba(226,60,78,.12); border-color: #e23c4e; color: #e23c4e; }
|
||||
.proc-col-header { display: grid; grid-template-columns: 28px 65px 65px 1fr 160px 60px; padding: 4px 10px; background: var(--panel); border-bottom: 1px solid var(--border-hi); color: var(--muted); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; flex-shrink: 0; user-select: none; }
|
||||
.proc-col-header span { padding: 2px 0; cursor: pointer; transition: color .1s; }
|
||||
.proc-col-header span:hover { color: var(--text); }
|
||||
.proc-list { flex: 1; overflow-y: auto; overflow-x: hidden; }
|
||||
.proc-item { display: grid; grid-template-columns: 28px 65px 65px 1fr 160px 60px; align-items: center; padding: 2px 10px; cursor: default; border-bottom: 1px solid rgba(42,46,59,.25); transition: background .08s; user-select: none; min-height: 25px; font-size: 11.5px; }
|
||||
.proc-item:nth-child(even) { background: rgba(255,255,255,.015); }
|
||||
.proc-item:hover { background: rgba(77,159,230,.08); }
|
||||
.proc-item.selected { background: rgba(77,159,230,.18); outline: 1px solid rgba(77,159,230,.35); outline-offset: -1px; }
|
||||
.proc-item .proc-icon { font-size: 14px; text-align: center; line-height: 1; }
|
||||
.proc-item .proc-pid { font-family: var(--mono); color: var(--text-dim); font-size: 11px; }
|
||||
.proc-item .proc-exe { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
||||
.proc-item .proc-owner { color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
|
||||
.proc-item .proc-arch { color: var(--muted); font-size: 10.5px; font-family: var(--mono); }
|
||||
.proc-item.proc-system .proc-exe { color: var(--accent); }
|
||||
.proc-item.proc-highlight .proc-exe { color: var(--ok); }
|
||||
.proc-status { padding: 4px 10px; background: var(--panel); border-top: 1px solid var(--border); color: var(--muted); font-size: 10.5px; flex-shrink: 0; display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
/* ── IOC Tracker ───────────────────────────────────────────────────────────── */
|
||||
.ioc-panel { padding: 4px; }
|
||||
[class^="badge-"] { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; }
|
||||
.badge-file { background: #1e3a5f; color: #93c5fd; }
|
||||
.badge-service { background: #4c1d95; color: #c4b5fd; }
|
||||
.badge-regkey { background: #713f12; color: #fde047; }
|
||||
.badge-schtask { background: #7c2d12; color: #fdba74; }
|
||||
.badge-cron { background: #14532d; color: #86efac; }
|
||||
.badge-user { background: #7f1d1d; color: #fca5a5; }
|
||||
|
||||
/* ── File Browser Split Layout ────────────────────────────────────────────── */
|
||||
.fb-body-split { display: flex; flex: 1; overflow: hidden; min-height: 0; }
|
||||
.fb-left-pane { display: flex; flex-direction: column; flex: 0 0 65%; min-width: 0; border-right: 1px solid var(--border); overflow: hidden; }
|
||||
.fb-left-pane .fb-col-header { flex-shrink: 0; }
|
||||
.fb-left-pane .fb-list { flex: 1; overflow-y: auto; overflow-x: hidden; }
|
||||
.fb-right-pane { flex: 1; display: flex; flex-direction: column; overflow-y: auto; background: var(--bg); }
|
||||
|
||||
/* ── File Browser Preview Panel ───────────────────────────────────────────── */
|
||||
.fb-preview-empty { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--muted); font-size: 11.5px; text-align: center; padding: 20px; }
|
||||
.fb-preview-icon { font-size: 40px; text-align: center; padding: 24px 0 8px; }
|
||||
.fb-preview-name { font-size: 13px; font-weight: 700; text-align: center; padding: 0 12px 4px; word-break: break-all; color: var(--text); }
|
||||
.fb-preview-type { font-size: 10.5px; text-align: center; color: var(--muted); padding-bottom: 12px; }
|
||||
.fb-preview-divider { height: 1px; background: var(--border); margin: 0 12px 12px; }
|
||||
.fb-preview-row { display: flex; flex-direction: column; padding: 4px 14px; gap: 2px; }
|
||||
.fb-preview-label { font-size: 9.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); font-weight: 600; }
|
||||
.fb-preview-val { font-size: 11px; color: var(--text-dim); font-family: var(--mono); word-break: break-all; }
|
||||
.fb-preview-tip { text-align: center; color: var(--muted); font-size: 10.5px; padding: 16px 12px; font-style: italic; }
|
||||
.fb-preview-actions { display: flex; flex-direction: column; gap: 6px; padding: 14px 14px 0; }
|
||||
.fb-preview-btn { padding: 6px 12px; border: 1px solid var(--border); background: var(--panel); color: var(--text); border-radius: 4px; cursor: pointer; font-size: 11.5px; font-weight: 500; transition: all .12s; text-align: center; }
|
||||
.fb-preview-btn:hover { background: var(--panel-alt); border-color: var(--muted); }
|
||||
.fb-preview-btn.danger { color: var(--accent); border-color: rgba(226,60,78,.3); }
|
||||
.fb-preview-btn.danger:hover { background: rgba(226,60,78,.12); border-color: #e23c4e; color: #e23c4e; }
|
||||
|
||||
/* ── C2 Profile Editor ────────────────────────────────────────────────────── */
|
||||
.c2-editor { display: flex; gap: 0; height: 100%; min-height: 460px; overflow: hidden; }
|
||||
.c2-left { display: flex; flex-direction: column; width: 200px; flex-shrink: 0; border-right: 1px solid var(--border); background: var(--panel); }
|
||||
.c2-list-header { padding: 8px 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
||||
.c2-profile-list { flex: 1; overflow-y: auto; }
|
||||
.c2p-item { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-dim); border-bottom: 1px solid rgba(42,46,59,.3); transition: background .1s, color .1s; }
|
||||
.c2p-item:hover { background: var(--panel-alt); color: var(--text); }
|
||||
.c2p-item.active { background: rgba(77,159,230,.15); color: var(--info); border-left: 2px solid var(--info); font-weight: 600; }
|
||||
.c2-list-footer { padding: 8px; border-top: 1px solid var(--border); flex-shrink: 0; }
|
||||
.c2-right { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
|
||||
.c2-editor-toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--panel); border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
||||
.c2-editor-name { font-size: 12px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.c2-json-editor { flex: 1; resize: none; background: var(--bg); color: var(--text); font-family: var(--mono); font-size: 12px; border: none; outline: none; padding: 12px 14px; line-height: 1.6; overflow: auto; }
|
||||
.c2-json-editor::placeholder { color: var(--muted); }
|
||||
.c2-editor-status { padding: 5px 12px; background: var(--panel); border-top: 1px solid var(--border); font-size: 10.5px; color: var(--muted); flex-shrink: 0; min-height: 24px; }
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
)
|
||||
|
||||
// panels.go implements: File Browser, Process Browser, Credential Auto-Populate,
|
||||
// Kill Chain Tracker, Engagement Timer, Internal/External IP resolution,
|
||||
// and Builder Streaming.
|
||||
|
||||
// ─── File Browser ─────────────────────────────────────────────────────────────
|
||||
|
||||
type FileEntry struct {
|
||||
Name string `json:"name"`
|
||||
IsDir bool `json:"isDir"`
|
||||
Size int64 `json:"size"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type FileBrowserResult struct {
|
||||
Path string `json:"path"`
|
||||
Files []FileEntry `json:"files"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// FileBrowserList lists files at a path for the visual file browser.
|
||||
func (a *App) FileBrowserList(sessionID, path string) FileBrowserResult {
|
||||
client, err := a.requireClient()
|
||||
if err != nil {
|
||||
return FileBrowserResult{Error: err.Error()}
|
||||
}
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
a.audit.log("file-browse", sessionID, path)
|
||||
|
||||
resp, err := client.RPC.Ls(a.ctx, &sliverpb.LsReq{
|
||||
Path: path,
|
||||
Request: &commonpb.Request{SessionID: sessionID},
|
||||
})
|
||||
if err != nil {
|
||||
return FileBrowserResult{Error: err.Error()}
|
||||
}
|
||||
|
||||
files := make([]FileEntry, 0, len(resp.Files))
|
||||
for _, f := range resp.Files {
|
||||
files = append(files, FileEntry{
|
||||
Name: f.Name,
|
||||
IsDir: f.IsDir,
|
||||
Size: f.Size,
|
||||
Mode: f.Mode,
|
||||
})
|
||||
}
|
||||
return FileBrowserResult{Path: resp.Path, Files: files}
|
||||
}
|
||||
|
||||
// FileBrowserDelete removes a file/directory.
|
||||
func (a *App) FileBrowserDelete(sessionID, path string) error {
|
||||
client, err := a.requireClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.audit.log("file-delete", sessionID, path)
|
||||
_, err = client.RPC.Rm(a.ctx, &sliverpb.RmReq{
|
||||
Path: path,
|
||||
Recursive: true,
|
||||
Request: &commonpb.Request{SessionID: sessionID},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// ─── Process Browser ──────────────────────────────────────────────────────────
|
||||
|
||||
type ProcessEntry struct {
|
||||
PID int32 `json:"pid"`
|
||||
PPID int32 `json:"ppid"`
|
||||
Executable string `json:"executable"`
|
||||
Owner string `json:"owner"`
|
||||
Arch string `json:"arch"`
|
||||
SessionID string `json:"sessionID"`
|
||||
}
|
||||
|
||||
type ProcessBrowserResult struct {
|
||||
Processes []ProcessEntry `json:"processes"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ProcessBrowserList lists all processes for the visual process browser.
|
||||
func (a *App) ProcessBrowserList(sessionID string) ProcessBrowserResult {
|
||||
client, err := a.requireClient()
|
||||
if err != nil {
|
||||
return ProcessBrowserResult{Error: err.Error()}
|
||||
}
|
||||
a.audit.log("process-browse", sessionID, "")
|
||||
|
||||
resp, err := client.RPC.Ps(a.ctx, &sliverpb.PsReq{
|
||||
Request: &commonpb.Request{SessionID: sessionID},
|
||||
})
|
||||
if err != nil {
|
||||
return ProcessBrowserResult{Error: err.Error()}
|
||||
}
|
||||
|
||||
procs := make([]ProcessEntry, 0, len(resp.Processes))
|
||||
for _, p := range resp.Processes {
|
||||
procs = append(procs, ProcessEntry{
|
||||
PID: p.Pid,
|
||||
PPID: p.Ppid,
|
||||
Executable: p.Executable,
|
||||
Owner: p.Owner,
|
||||
Arch: p.Architecture,
|
||||
SessionID: sessionID,
|
||||
})
|
||||
}
|
||||
return ProcessBrowserResult{Processes: procs}
|
||||
}
|
||||
|
||||
// ProcessBrowserKill kills a remote process.
|
||||
func (a *App) ProcessBrowserKill(sessionID string, pid int32) error {
|
||||
client, err := a.requireClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.audit.log("process-kill", sessionID, fmt.Sprintf("PID %d", pid))
|
||||
_, err = client.RPC.Terminate(a.ctx, &sliverpb.TerminateReq{
|
||||
Pid: pid,
|
||||
Request: &commonpb.Request{SessionID: sessionID},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// ─── Internal/External IP Resolution ──────────────────────────────────────────
|
||||
|
||||
type AgentIPs struct {
|
||||
InternalIP string `json:"internalIP"`
|
||||
ExternalIP string `json:"externalIP"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// GetAgentIPs returns the internal (private) and external (public/remote) IPs
|
||||
// for an agent. Internal is pulled from ifconfig; external is the peer address.
|
||||
func (a *App) GetAgentIPs(sessionID string) AgentIPs {
|
||||
client, err := a.requireClient()
|
||||
if err != nil {
|
||||
return AgentIPs{Error: err.Error()}
|
||||
}
|
||||
|
||||
// Get internal IPs from ifconfig
|
||||
resp, err := client.RPC.Ifconfig(a.ctx, &sliverpb.IfconfigReq{
|
||||
Request: &commonpb.Request{SessionID: sessionID},
|
||||
})
|
||||
|
||||
internalIP := ""
|
||||
if err == nil && resp != nil {
|
||||
for _, iface := range resp.NetInterfaces {
|
||||
for _, addr := range iface.IPAddresses {
|
||||
ip := strings.Split(addr, "/")[0]
|
||||
if isPrivateIP(ip) && internalIP == "" {
|
||||
internalIP = ip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get external IP from the session's remote address (handles IPv4 and IPv6)
|
||||
externalIP := ""
|
||||
sessions, _ := client.ListSessions(a.ctx)
|
||||
for _, s := range sessions {
|
||||
if s.ID == sessionID {
|
||||
host, _, err := net.SplitHostPort(s.RemoteAddress)
|
||||
if err != nil {
|
||||
// Fallback: might be bare IP without port
|
||||
host = s.RemoteAddress
|
||||
}
|
||||
externalIP = host
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return AgentIPs{InternalIP: internalIP, ExternalIP: externalIP}
|
||||
}
|
||||
|
||||
func isPrivateIP(ip string) bool {
|
||||
privates := []string{"10.", "172.16.", "172.17.", "172.18.", "172.19.",
|
||||
"172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.",
|
||||
"172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31.",
|
||||
"192.168.", "169.254."}
|
||||
for _, prefix := range privates {
|
||||
if strings.HasPrefix(ip, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ─── Credential Auto-Populate ─────────────────────────────────────────────────
|
||||
|
||||
type ParsedCredential struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Hash string `json:"hash"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// ParseAndStoreCredentials parses script output for well-known credential formats.
|
||||
// It does NOT auto-store into Sliver's cred DB — call ConfirmAndStoreCredentials
|
||||
// with the returned slice after the operator reviews them.
|
||||
func (a *App) ParseAndStoreCredentials(output, source string) []ParsedCredential {
|
||||
creds := parseCredentials(output)
|
||||
for i := range creds {
|
||||
creds[i].Source = source
|
||||
}
|
||||
if len(creds) > 0 {
|
||||
a.audit.log("creds-parsed", "", fmt.Sprintf("%d candidates from %s", len(creds), source))
|
||||
}
|
||||
return creds
|
||||
}
|
||||
|
||||
// ConfirmAndStoreCredentials stores operator-confirmed credentials in Sliver's DB.
|
||||
func (a *App) ConfirmAndStoreCredentials(creds []ParsedCredential) error {
|
||||
client, err := a.requireClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range creds {
|
||||
hashType := int32(0) // plaintext
|
||||
if c.Hash != "" {
|
||||
hashType = 1
|
||||
}
|
||||
_, _ = client.RPC.CredsAdd(a.ctx, &clientpb.Credentials{
|
||||
Credentials: []*clientpb.Credential{{
|
||||
Username: c.Username,
|
||||
Plaintext: c.Password,
|
||||
Hash: c.Hash,
|
||||
HashType: clientpb.HashType(hashType),
|
||||
Collection: c.Source,
|
||||
}},
|
||||
})
|
||||
}
|
||||
a.audit.log("creds-stored", "", fmt.Sprintf("%d credentials confirmed+stored", len(creds)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compiled credential regexes — only match well-known output formats.
|
||||
var (
|
||||
// SAM dump: user:RID:LM_hash:NTLM_hash:::
|
||||
reSAMHash = regexp.MustCompile(`(?m)^([\w$\.\-]+):\d+:[a-fA-F0-9]{32}:([a-fA-F0-9]{32})`)
|
||||
// impacket secretsdump: DOMAIN\user:plaintext or DOMAIN/user:plaintext (after ":::")
|
||||
reSecretsDump = regexp.MustCompile(`(?m)^([\w\.\-]+(?:[/\\][\w\.\-]+)?):[^:]+:[a-fA-F0-9]{32}:([a-fA-F0-9]{32})`)
|
||||
// mimikatz: "Username : value" / "* Password : value" / "* NTLM : hex"
|
||||
reMimikatzUser = regexp.MustCompile(`(?i)Username\s*:\s*(\S+)`)
|
||||
reMimikatzPass = regexp.MustCompile(`(?i)\*\s*Password\s*:\s*(.+)$`)
|
||||
reMimikatzNTLM = regexp.MustCompile(`(?i)\*\s*NTLM\s*:\s*([a-fA-F0-9]{32})`)
|
||||
)
|
||||
|
||||
func parseCredentials(output string) []ParsedCredential {
|
||||
var creds []ParsedCredential
|
||||
seen := map[string]bool{}
|
||||
add := func(user, pass, hash string) {
|
||||
user = strings.TrimSpace(user)
|
||||
pass = strings.TrimSpace(pass)
|
||||
hash = strings.TrimSpace(hash)
|
||||
if user == "" || (pass == "" && hash == "") {
|
||||
return
|
||||
}
|
||||
// Skip obvious noise
|
||||
if user == "(null)" || pass == "(null)" || pass == "(null" {
|
||||
return
|
||||
}
|
||||
key := user + ":" + pass + ":" + hash
|
||||
if seen[key] {
|
||||
return
|
||||
}
|
||||
seen[key] = true
|
||||
creds = append(creds, ParsedCredential{Username: user, Password: pass, Hash: hash})
|
||||
}
|
||||
|
||||
// 1. SAM hash lines: user:RID:LM:NTLM
|
||||
for _, m := range reSAMHash.FindAllStringSubmatch(output, -1) {
|
||||
add(m[1], "", m[2])
|
||||
}
|
||||
// 2. secretsdump NTLM lines
|
||||
for _, m := range reSecretsDump.FindAllStringSubmatch(output, -1) {
|
||||
add(m[1], "", m[2])
|
||||
}
|
||||
|
||||
// 3. mimikatz block parsing: Username line followed by Password/NTLM lines
|
||||
lines := strings.Split(output, "\n")
|
||||
var curUser string
|
||||
for _, line := range lines {
|
||||
if m := reMimikatzUser.FindStringSubmatch(line); m != nil {
|
||||
curUser = m[1]
|
||||
} else if curUser != "" {
|
||||
if m := reMimikatzPass.FindStringSubmatch(line); m != nil {
|
||||
p := strings.TrimSpace(m[1])
|
||||
if p != "" && p != "(null)" {
|
||||
add(curUser, p, "")
|
||||
}
|
||||
}
|
||||
if m := reMimikatzNTLM.FindStringSubmatch(line); m != nil {
|
||||
add(curUser, "", m[1])
|
||||
}
|
||||
}
|
||||
// Reset on blank lines (new logon block)
|
||||
if strings.TrimSpace(line) == "" {
|
||||
curUser = ""
|
||||
}
|
||||
}
|
||||
return creds
|
||||
}
|
||||
|
||||
// ─── Kill Chain Tracker ───────────────────────────────────────────────────────
|
||||
|
||||
// KillChainStage represents the progress of an engagement.
|
||||
type KillChainState struct {
|
||||
Recon bool `json:"recon"`
|
||||
Access bool `json:"access"`
|
||||
PrivEsc bool `json:"privesc"`
|
||||
Lateral bool `json:"lateral"`
|
||||
DomainAdmin bool `json:"domainAdmin"`
|
||||
Persistence bool `json:"persistence"`
|
||||
LastUpdate string `json:"lastUpdate"`
|
||||
}
|
||||
|
||||
var (
|
||||
stateMu sync.Mutex
|
||||
killChain = KillChainState{}
|
||||
iocList []IOCEntry
|
||||
iocCounter int
|
||||
engagementStart *time.Time
|
||||
)
|
||||
|
||||
// GetKillChain returns current kill chain progress.
|
||||
func (a *App) GetKillChain() KillChainState {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
return killChain
|
||||
}
|
||||
|
||||
// UpdateKillChain advances a kill chain stage.
|
||||
func (a *App) UpdateKillChain(stage string) KillChainState {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
switch strings.ToLower(stage) {
|
||||
case "recon":
|
||||
killChain.Recon = true
|
||||
case "access":
|
||||
killChain.Access = true
|
||||
case "privesc":
|
||||
killChain.PrivEsc = true
|
||||
case "lateral":
|
||||
killChain.Lateral = true
|
||||
case "domainadmin", "da":
|
||||
killChain.DomainAdmin = true
|
||||
case "persistence", "persist":
|
||||
killChain.Persistence = true
|
||||
}
|
||||
killChain.LastUpdate = time.Now().Format(time.RFC3339)
|
||||
a.audit.log("killchain", stage, "")
|
||||
return killChain
|
||||
}
|
||||
|
||||
// ResetKillChain resets all stages.
|
||||
func (a *App) ResetKillChain() KillChainState {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
killChain = KillChainState{}
|
||||
return killChain
|
||||
}
|
||||
|
||||
// ResetEngagementState clears all per-engagement state (call on disconnect).
|
||||
func (a *App) ResetEngagementState() {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
killChain = KillChainState{}
|
||||
iocList = nil
|
||||
iocCounter = 0
|
||||
engagementStart = nil
|
||||
a.audit.log("engagement", "reset", "")
|
||||
}
|
||||
|
||||
// ─── Engagement Timer ─────────────────────────────────────────────────────────
|
||||
|
||||
// StartEngagementTimer marks the beginning of the engagement.
|
||||
func (a *App) StartEngagementTimer() string {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
now := time.Now()
|
||||
engagementStart = &now
|
||||
a.audit.log("engagement", "start", now.Format(time.RFC3339))
|
||||
return now.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// GetEngagementElapsed returns seconds since engagement started.
|
||||
func (a *App) GetEngagementElapsed() int64 {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
if engagementStart == nil {
|
||||
return 0
|
||||
}
|
||||
return int64(time.Since(*engagementStart).Seconds())
|
||||
}
|
||||
|
||||
// ─── Builder Streaming (placeholder — streams are complex in Wails) ───────────
|
||||
|
||||
// GetBuilders lists external builders connected to the teamserver.
|
||||
func (a *App) GetBuilders() ([]map[string]interface{}, error) {
|
||||
client, err := a.requireClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.RPC.Builders(a.ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []map[string]interface{}
|
||||
for _, b := range resp.Builders {
|
||||
result = append(result, map[string]interface{}{
|
||||
"name": b.Name,
|
||||
"operator": b.OperatorName,
|
||||
"goos": b.GOOS,
|
||||
"goarch": b.GOARCH,
|
||||
"targets": b.CrossCompilers,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ─── IOC Tracker ──────────────────────────────────────────────────────────────
|
||||
|
||||
// IOCEntry represents an Indicator of Compromise left on a target.
|
||||
type IOCEntry struct {
|
||||
ID int `json:"id"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Host string `json:"host"`
|
||||
Type string `json:"type"` // file, service, regkey, schtask, user, cron
|
||||
Path string `json:"path"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
// AddIOC records a new IOC. Thread-safe.
|
||||
func (a *App) AddIOC(host, iocType, path, detail string) IOCEntry {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
iocCounter++
|
||||
entry := IOCEntry{
|
||||
ID: iocCounter,
|
||||
Timestamp: time.Now().Format("15:04:05"),
|
||||
Host: host,
|
||||
Type: iocType,
|
||||
Path: path,
|
||||
Detail: detail,
|
||||
}
|
||||
iocList = append(iocList, entry)
|
||||
a.audit.log("ioc-added", host, fmt.Sprintf("%s: %s", iocType, path))
|
||||
return entry
|
||||
}
|
||||
|
||||
// GetIOCs returns all tracked IOCs.
|
||||
func (a *App) GetIOCs() []IOCEntry {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
out := make([]IOCEntry, len(iocList))
|
||||
copy(out, iocList)
|
||||
return out
|
||||
}
|
||||
|
||||
// ClearIOCs resets the IOC list.
|
||||
func (a *App) ClearIOCs() {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
iocList = nil
|
||||
iocCounter = 0
|
||||
}
|
||||
|
||||
// GenerateCleanupScript produces a script to remove all IOCs.
|
||||
func (a *App) GenerateCleanupScript() string {
|
||||
if len(iocList) == 0 {
|
||||
return "# No IOCs tracked"
|
||||
}
|
||||
|
||||
var winCmds, linCmds []string
|
||||
|
||||
for _, ioc := range iocList {
|
||||
switch ioc.Type {
|
||||
case "file":
|
||||
winCmds = append(winCmds, fmt.Sprintf(`del /f "%s"`, ioc.Path))
|
||||
linCmds = append(linCmds, fmt.Sprintf(`rm -f "%s"`, ioc.Path))
|
||||
case "service":
|
||||
winCmds = append(winCmds, fmt.Sprintf(`sc stop %s & sc delete %s`, ioc.Path, ioc.Path))
|
||||
linCmds = append(linCmds, fmt.Sprintf(`systemctl stop %s && systemctl disable %s && rm /etc/systemd/system/%s.service`, ioc.Path, ioc.Path, ioc.Path))
|
||||
case "regkey":
|
||||
winCmds = append(winCmds, fmt.Sprintf(`reg delete "%s" /f`, ioc.Path))
|
||||
case "schtask":
|
||||
winCmds = append(winCmds, fmt.Sprintf(`schtasks /delete /tn "%s" /f`, ioc.Path))
|
||||
case "cron":
|
||||
linCmds = append(linCmds, fmt.Sprintf(`sed -i '/%s/d' /etc/crontab`, strings.ReplaceAll(ioc.Path, "/", `\/`)))
|
||||
case "user":
|
||||
winCmds = append(winCmds, fmt.Sprintf(`net user %s /delete`, ioc.Path))
|
||||
linCmds = append(linCmds, fmt.Sprintf(`userdel -r %s`, ioc.Path))
|
||||
}
|
||||
}
|
||||
|
||||
script := "# ═══ VulnNetRed IOC Cleanup Script ═══\n"
|
||||
script += fmt.Sprintf("# Generated: %s\n", time.Now().Format(time.RFC3339))
|
||||
script += fmt.Sprintf("# IOCs tracked: %d\n\n", len(iocList))
|
||||
|
||||
if len(winCmds) > 0 {
|
||||
script += "# ── Windows Cleanup ──\n"
|
||||
for _, c := range winCmds {
|
||||
script += c + "\n"
|
||||
}
|
||||
script += "\n"
|
||||
}
|
||||
if len(linCmds) > 0 {
|
||||
script += "# ── Linux Cleanup ──\n"
|
||||
for _, c := range linCmds {
|
||||
script += c + "\n"
|
||||
}
|
||||
}
|
||||
return script
|
||||
}
|
||||
|
||||
// ─── Engagement Report ────────────────────────────────────────────────────────
|
||||
|
||||
// GenerateReport creates a Markdown engagement report from audit log + IOCs + kill chain.
|
||||
func (a *App) GenerateReport() string {
|
||||
elapsed := a.GetEngagementElapsed()
|
||||
h := elapsed / 3600
|
||||
m := (elapsed % 3600) / 60
|
||||
s := elapsed % 60
|
||||
|
||||
report := "# Engagement Report\n\n"
|
||||
report += fmt.Sprintf("**Duration:** %02d:%02d:%02d\n\n", h, m, s)
|
||||
report += fmt.Sprintf("**Generated:** %s\n\n", time.Now().Format(time.RFC3339))
|
||||
|
||||
// Kill Chain
|
||||
report += "## Kill Chain Progress\n\n"
|
||||
report += "| Stage | Status |\n|-------|--------|\n"
|
||||
stages := []struct {
|
||||
name string
|
||||
done bool
|
||||
}{
|
||||
{"Reconnaissance", killChain.Recon},
|
||||
{"Initial Access", killChain.Access},
|
||||
{"Privilege Escalation", killChain.PrivEsc},
|
||||
{"Lateral Movement", killChain.Lateral},
|
||||
{"Domain Admin", killChain.DomainAdmin},
|
||||
{"Persistence", killChain.Persistence},
|
||||
}
|
||||
for _, st := range stages {
|
||||
status := "[----]"
|
||||
if st.done {
|
||||
status = "[DONE]"
|
||||
}
|
||||
report += fmt.Sprintf("| %s | %s |\n", st.name, status)
|
||||
}
|
||||
|
||||
// IOCs
|
||||
report += "\n## Indicators of Compromise\n\n"
|
||||
if len(iocList) == 0 {
|
||||
report += "No IOCs tracked.\n"
|
||||
} else {
|
||||
report += "| Time | Host | Type | Path | Detail |\n|------|------|------|------|--------|\n"
|
||||
for _, ioc := range iocList {
|
||||
report += fmt.Sprintf("| %s | %s | %s | `%s` | %s |\n",
|
||||
ioc.Timestamp, ioc.Host, ioc.Type, ioc.Path, ioc.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
// Audit trail (last 50 entries)
|
||||
report += "\n## Operator Actions (Last 50)\n\n"
|
||||
entries, _ := a.RecentAudit(50)
|
||||
if len(entries) > 0 {
|
||||
report += "| Time | Action | Target | Detail |\n|------|--------|--------|--------|\n"
|
||||
for _, e := range entries {
|
||||
report += fmt.Sprintf("| %s | %s | %s | %s |\n",
|
||||
e.Time, e.Action, e.Target, e.Detail)
|
||||
}
|
||||
} else {
|
||||
report += "No audit entries.\n"
|
||||
}
|
||||
|
||||
// Cleanup script
|
||||
report += "\n## Cleanup Script\n\n```bash\n"
|
||||
report += a.GenerateCleanupScript()
|
||||
report += "\n```\n"
|
||||
|
||||
return report
|
||||
}
|
||||
+2000
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user