mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-21 06:51:25 +02:00
fix: preserve shell output and clarify fact relationships
This commit is contained in:
1 parent
76da98944c
commit
ab17ffda87
8 files changed
+128
-13
No files matched your search
@@ -28,6 +28,8 @@
|
||||
| 19 WebShell 口令明文回显 | 列表和创建/更新响应返回 Password | 响应掩码;保存时保留/替换/清空;执行按有权限的 connection_id 取服务端凭据 |
|
||||
| Markdown 智能体字段与重复 ID | 保存缺少关键字段和 ID 唯一性检查 | 创建/更新统一校验 name、ID、指令;ID 不允许文件间重复;创建使用排他文件写入 |
|
||||
| 辅助文件编辑描述丢失 | 辅助文件保存接口不更新 SKILL.md 元数据 | 辅助文件禁用描述编辑;描述未保存时阻止切换文件,提示先保存 SKILL.md |
|
||||
| C2 终端输出粘连/尾部分号语法错误 | Unix 探测直接追加 `&& pwd`,结果按最后一行切分 | 使用随机分隔标记和换行探测,保留用户命令的输出及退出状态 |
|
||||
| 事实正文的关系语法未生成图谱 | 关系字段是显式结构化输入,普通正文不是通用关系解析器 | 按问题表给出的替代方案,明确提示关系应填写到关系字段;保持既有正文语义 |
|
||||
|
||||
## 不适用
|
||||
|
||||
@@ -46,3 +48,4 @@
|
||||
- WebShell 路径、错误输出、目录树测试与通知查看/待审批区分测试通过。
|
||||
- 配置凭据处理保留既有字段与空字符串清除语义;掩码标记只用于已保存的同路径凭据,新通道不能直接保存掩码。通道探活新增可选 `channel_id`,模型列表/审查测试支持可选凭据范围,避免同 URL 多密钥选择错误。
|
||||
- 新增 GET 配置不泄漏/不变更内存、掩码保留/替换/清空、歧义拒绝、WebShell 回显及执行凭据、Markdown 创建/更新唯一性、技能描述切换测试。
|
||||
- C2 Shell 回归只提取模板中的 Shell 辅助函数,不运行传输循环。验证无换行、有换行、尾部分号、注释、失败退出、带空格目录切换;Unix 系统通过,Windows 路径未修改。模板修复适用于重新生成的客户端。
|
||||
@@ -51,18 +51,18 @@
|
||||
| 46 | — | Agent ID 可重复- 两个不同文件可使用同一个 ID。 | 已修复并有针对性验证 |
|
||||
| 47 | — | nmap 参数缺少前置校验 | 待核对 |
|
||||
| 48 | — | 对单个资产发起“只读连通性与服务指纹核验”后,Agent 已通过 nmap 和 HTTP 指纹工 | 待核对 |
|
||||
| 49 | — | C2 会话终端执行没有换行结尾的命令时,系统内部用于获取当前目录的 pwd 输出会与用户命令输出 | 待核对 |
|
||||
| 49 | — | C2 会话终端执行没有换行结尾的命令时,系统内部用于获取当前目录的 pwd 输出会与用户命令输出 | 已修复,Unix Shell 回归通过 |
|
||||
| 50 | — | 监听器编辑后点击保存没有反应 | 待核对 |
|
||||
| 51 | — | 创建 Malleable Profile 时 UA、响应头和 Jitter 配置被静默丢弃 | 已修复并有针对性验证 |
|
||||
| 52 | — | C2 载荷生成未校验回连地址,非法地址仍能生成 Oneliner | 待核对 |
|
||||
| 53 | — | 知识库索引构建失败原因不怎么太清晰 | 待核对 |
|
||||
| 54 | — | Agent 未挂载知识检索工具 | 待核对 |
|
||||
| 55 | — | - 选择其专用测试会话,打开「终端」。 | 待核对 |
|
||||
| 55 | — | - 选择其专用测试会话,打开「终端」。 | 已修复,Unix Shell 回归通过 |
|
||||
| 56 | — | AI看代码给提的问题 | 已修复并有针对性验证 |
|
||||
| 57 | — | 审计日志搜索无结果后,点击“清空”不会清除关键词或恢复列表,无反应 | 待核对 |
|
||||
| 58 | — | 任务失败,也显示已完成 | 待核对 |
|
||||
| 59 | — | 批量任务在“用户并发配额已满”时进入等待调度后,即使后来停止容器、腾出配额,队列仍停在“执行中 | 当前仓库未实现,不扩展能力 |
|
||||
| 60 | — | 资产扫描任务在后续工具调用的 HITL 审批阶段已被系统中断,审批卡片明确显示“任务已中断、审批 | 待核对 |
|
||||
| 61 | — | 问题:事实正文里的 discovered_on: target/xxx 只会当普通文本保存,不会 | 待核对 |
|
||||
| 61 | — | 问题:事实正文里的 discovered_on: target/xxx 只会当普通文本保存,不会 | 已按建议明确关系字段说明 |
|
||||
| 62 | — | 无效工作流仍显示“启用”: 即使工作流校验不通过、无法试运行,列表和详情仍显示为“启用”,容易让 | 待核对 |
|
||||
| 63 | — | 编辑辅助文件时,描述修改未保存 | 已修复并有针对性验证 |
|
||||
@@ -0,0 +1,86 @@
|
||||
package c2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Execute only the shell helpers from the template, never its transport loop.
|
||||
func TestBeaconShellTemplateSeparatesCwdAndPreservesExitStatus(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Unix shell regression")
|
||||
}
|
||||
raw, err := os.ReadFile("payload_templates/beacon.go.tmpl")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw = regexp.MustCompile(`\{\{\.(SleepSeconds|JitterPercent)\}\}`).ReplaceAll(raw, []byte("5"))
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, "template.go", raw, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var code bytes.Buffer
|
||||
code.WriteString(`package main
|
||||
import("crypto/rand";"fmt";"os";"os/exec";"path/filepath";"runtime";"strings";"sync";"time")
|
||||
var cwdMu sync.Mutex
|
||||
var currentCwd string
|
||||
func prepareHiddenCmd(cmd *exec.Cmd) {}
|
||||
`)
|
||||
needed := map[string]bool{"taskShell": true, "runWithTimeout": true, "getTimeoutFromPayload": true, "shellByOS": true, "shellFlag": true}
|
||||
for _, declaration := range file.Decls {
|
||||
if fn, ok := declaration.(*ast.FuncDecl); ok && needed[fn.Name.Name] {
|
||||
if err := format.Node(&code, fset, fn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
code.WriteByte('\n')
|
||||
delete(needed, fn.Name.Name)
|
||||
}
|
||||
}
|
||||
if len(needed) != 0 {
|
||||
t.Fatal("missing template helpers")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
for name, content := range map[string]string{
|
||||
"go.mod": "module shellregression\ngo 1.23\n",
|
||||
"shell.go": code.String(),
|
||||
"shell_test.go": `package main
|
||||
import("os";"path/filepath";"testing")
|
||||
func TestShell(t *testing.T) {
|
||||
currentCwd=t.TempDir()
|
||||
cases:=[]struct{cmd,want string; failed bool}{
|
||||
{"printf hello","hello",false},
|
||||
{"printf 'hello\\n'","hello\n",false},
|
||||
{"printf hello;","hello",false},
|
||||
{"sleep 0.01;","",false},
|
||||
{"printf hello; false;","hello",true},
|
||||
{"printf hello # comment","hello",false},
|
||||
}
|
||||
for _, c:=range cases {out,_,_,failure:=taskShell(map[string]interface{}{"command":c.cmd}); if out!=c.want || (failure!="")!=c.failed {t.Fatalf("%q output=%q failure=%q",c.cmd,out,failure)} }
|
||||
target:=filepath.Join(currentCwd,"directory with spaces")
|
||||
if err:=os.Mkdir(target,0700);err!=nil{t.Fatal(err)}
|
||||
out,_,_,failure:=taskShell(map[string]interface{}{"command":"cd '"+target+"'; printf OK"})
|
||||
if out!="OK" || failure!="" || currentCwd!=target {t.Fatalf("cwd/output failure: %q %q",out,failure)}
|
||||
}
|
||||
`,
|
||||
} {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
cmd := exec.Command("go", "test", "-count=1", ".")
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "GOPROXY=off", "GOWORK=off")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("shell regression: %v\n%s", err, output)
|
||||
}
|
||||
}
|
||||
@@ -783,10 +783,18 @@ func taskShell(payload map[string]interface{}) (string, string, string, string)
|
||||
// Append a pwd/cd probe to the command so we can capture the real cwd
|
||||
// after the user's command runs (e.g. "cd /tmp && ls" → cwd becomes /tmp).
|
||||
var probe string
|
||||
var cwdMarker string
|
||||
if runtime.GOOS == "windows" {
|
||||
probe = " && cd"
|
||||
} else {
|
||||
probe = " && pwd"
|
||||
markerBytes := make([]byte, 12)
|
||||
if _, err := rand.Read(markerBytes); err != nil {
|
||||
return "", "", "", "cannot prepare cwd probe"
|
||||
}
|
||||
cwdMarker = fmt.Sprintf("__CSAI_CWD_%x__", markerBytes)
|
||||
// A newline accepts a trailing semicolon/comment. Preserve the command's
|
||||
// exit status and separate probe output even when stdout has no newline.
|
||||
probe = "\n__csai_command_status=$?\nprintf '\\n" + cwdMarker + "'; pwd\nexit \"$__csai_command_status\""
|
||||
}
|
||||
combined := cmdStr + probe
|
||||
|
||||
@@ -794,15 +802,29 @@ func taskShell(payload map[string]interface{}) (string, string, string, string)
|
||||
|
||||
// The last line of output is the cwd from the probe command.
|
||||
// Split it off so we don't return the probe output to the operator.
|
||||
lines := strings.Split(strings.TrimRight(out, "\r\n"), "\n")
|
||||
if len(lines) > 0 {
|
||||
candidate := strings.TrimSpace(lines[len(lines)-1])
|
||||
if filepath.IsAbs(candidate) {
|
||||
if info, statErr := os.Stat(candidate); statErr == nil && info.IsDir() {
|
||||
cwdMu.Lock()
|
||||
currentCwd = candidate
|
||||
cwdMu.Unlock()
|
||||
out = strings.Join(lines[:len(lines)-1], "\n")
|
||||
if cwdMarker != "" {
|
||||
if idx := strings.LastIndex(out, "\n"+cwdMarker); idx >= 0 {
|
||||
candidate := strings.TrimRight(out[idx+len(cwdMarker)+1:], "\r\n")
|
||||
if filepath.IsAbs(candidate) {
|
||||
if info, statErr := os.Stat(candidate); statErr == nil && info.IsDir() {
|
||||
cwdMu.Lock()
|
||||
currentCwd = candidate
|
||||
cwdMu.Unlock()
|
||||
out = out[:idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lines := strings.Split(strings.TrimRight(out, "\r\n"), "\n")
|
||||
if len(lines) > 0 {
|
||||
candidate := strings.TrimSpace(lines[len(lines)-1])
|
||||
if filepath.IsAbs(candidate) {
|
||||
if info, statErr := os.Stat(candidate); statErr == nil && info.IsDir() {
|
||||
cwdMu.Lock()
|
||||
currentCwd = candidate
|
||||
cwdMu.Unlock()
|
||||
out = strings.Join(lines[:len(lines)-1], "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +341,7 @@
|
||||
"graphLegendExploits": "exploits",
|
||||
"graphLegendTentative": "Tentative (dashed)",
|
||||
"factLinksLabel": "Links (from → this fact)",
|
||||
"factBodyRelationsHint": "Enter relationships such as discovered_on in the Links field below to create graph edges.",
|
||||
"factLinksPlaceholder": "discovered_on: target/primary_domain\nexploits: exploit/upload-rce",
|
||||
"factLinksHint": "One per line: type: source_fact_key (source → this fact). Common types: discovered_on, depends_on, leads_to, enables, exploits. Saving replaces all links.",
|
||||
"factIncomingLinksLabel": "Incoming links (read-only)",
|
||||
|
||||
@@ -353,6 +353,7 @@
|
||||
"graphLegendExploits": "exploits",
|
||||
"graphLegendTentative": "Предварительно (пунктир)",
|
||||
"factLinksLabel": "Связи (откуда → этот факт)",
|
||||
"factBodyRelationsHint": "Укажите отношения, например discovered_on, в поле связей ниже, чтобы создать рёбра графа.",
|
||||
"factLinksPlaceholder": "discovered_on: target/primary_domain\nexploits: exploit/upload-rce",
|
||||
"factLinksHint": "По одной строке: тип: source_fact_key (источник → этот факт). Частые типы: discovered_on, depends_on, leads_to, enables, exploits. При сохранении все связи заменяются.",
|
||||
"factIncomingLinksLabel": "Входящие связи (только чтение)",
|
||||
|
||||
@@ -329,6 +329,7 @@
|
||||
"graphLegendExploits": "exploits",
|
||||
"graphLegendTentative": "待确认(虚线)",
|
||||
"factLinksLabel": "关系边(from → 本事实)",
|
||||
"factBodyRelationsHint": "正文中的 discovered_on 等关系请填写到下方“关系边”字段,才能生成图谱连线。",
|
||||
"factLinksPlaceholder": "discovered_on: target/primary_domain\nexploits: exploit/upload-rce",
|
||||
"factLinksHint": "每行一条:type: source_fact_key(来源 → 当前事实)。常用 type:discovered_on、depends_on、leads_to、enables、exploits。保存时替换全部关系边。",
|
||||
"factIncomingLinksLabel": "入边(只读)",
|
||||
|
||||
@@ -6778,6 +6778,7 @@
|
||||
</div>
|
||||
<textarea id="fact-modal-body" class="form-input fact-modal-body-input" rows="14" placeholder="攻击链步骤、HTTP/命令 POC、响应现象、证据…" oninput="updateFactFormHints()"></textarea>
|
||||
<p id="fact-modal-body-hint" class="projects-field-hint" role="status"></p>
|
||||
<small class="form-hint" data-i18n="projects.factBodyRelationsHint">正文中的 discovered_on 等关系请填写到下方“关系边”字段,才能生成图谱连线。</small>
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label class="projects-filter-check projects-pin-toggle">
|
||||
|
||||
Reference in new issue
Block a user