mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-07 01:56:40 +02:00
128 lines
4.8 KiB
Go
128 lines
4.8 KiB
Go
package handler
|
||
|
||
import (
|
||
"bytes"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// webshellOSProbeCommand 探活命令:利用 Windows cmd 与 POSIX shell 对 `%OS%` 展开差异进行判定。
|
||
// - Windows cmd:`%OS%` 被展开为 `Windows_NT`,回显 `:OSPROBE_Windows_NT:END`
|
||
// - POSIX sh/bash:`%OS%` 不是变量语法,作为字面量原样保留,回显 `:OSPROBE_%OS%:END`
|
||
//
|
||
// 一条命令即可得到明确的、互斥的信号,避免探活成本(相比发两次命令)。
|
||
// 冒号包裹是为了避免部分 shell 输出多余空白/BOM 时字符串匹配失效。
|
||
const webshellOSProbeCommand = "echo :OSPROBE_%OS%:END"
|
||
|
||
// probeWebshellOSViaExec 通过一次命令执行的回显推断目标操作系统。
|
||
//
|
||
// 返回值:
|
||
// - "windows" / "linux":识别成功
|
||
// - "":无法判定(调用方应保留既有 fallback 逻辑)
|
||
//
|
||
// 入参 execFn 是一个"发命令并拿到回显"的闭包;让 HTTP 入口和 MCP 入口可以共用同一套探活逻辑
|
||
// 而不必关心底层是如何发包的。
|
||
func probeWebshellOSViaExec(execFn func(cmd string) (output string, ok bool)) string {
|
||
if execFn == nil {
|
||
return ""
|
||
}
|
||
out, ok := execFn(webshellOSProbeCommand)
|
||
if !ok {
|
||
return ""
|
||
}
|
||
return classifyWebshellOSProbeOutput(out)
|
||
}
|
||
|
||
// classifyWebshellOSProbeOutput 纯函数:根据探活命令的回显判定 OS。
|
||
// 抽出来是为了单测可直接覆盖所有分支,无需真实 HTTP 调用。
|
||
func classifyWebshellOSProbeOutput(out string) string {
|
||
if out == "" {
|
||
return ""
|
||
}
|
||
lower := strings.ToLower(out)
|
||
|
||
// Windows 强信号:cmd.exe 成功展开了 %OS% 变量
|
||
if strings.Contains(out, "Windows_NT") {
|
||
return "windows"
|
||
}
|
||
// 容错:部分老版本 Windows 可能 `%OS%` 展开为其他字样(极少见),再看 PATH/OS 等次级线索
|
||
if strings.Contains(lower, "microsoft windows") {
|
||
return "windows"
|
||
}
|
||
|
||
// Linux/Unix 强信号:`%OS%` 字面量被原样回显,说明 shell 不是 cmd.exe
|
||
if strings.Contains(out, "%OS%") {
|
||
return "linux"
|
||
}
|
||
|
||
// 次级线索:部分 webshell 在 Linux 上可能走了其他外壳(如 zsh/ash),
|
||
// 但它们对 `%OS%` 同样不展开;若命中 OSPROBE 头部却没拿到 %OS% 字面量,
|
||
// 说明回显被中途截断或过滤,保守返回空让上层 fallback。
|
||
return ""
|
||
}
|
||
|
||
// newHTTPExecFn 为 HTTP FileOp 路径构造"发命令取回显"的闭包,供探活复用。
|
||
// 参数来自 HTTP 请求,复用 buildExecURL / buildExecBody 两个已有的命令编排器,
|
||
// 确保探活包与实际文件操作包走完全一致的 webshell 协议(GET/POST、参数名、编码)。
|
||
func (h *WebShellHandler) newHTTPExecFn(targetURL, password, shellType, method, cmdParam, encoding string) func(string) (string, bool) {
|
||
useGET := strings.ToUpper(strings.TrimSpace(method)) == "GET"
|
||
if strings.TrimSpace(cmdParam) == "" {
|
||
cmdParam = "cmd"
|
||
}
|
||
return func(cmd string) (string, bool) {
|
||
var (
|
||
httpReq *http.Request
|
||
err error
|
||
)
|
||
if useGET {
|
||
u := h.buildExecURL(targetURL, shellType, password, cmdParam, cmd)
|
||
httpReq, err = http.NewRequest(http.MethodGet, u, nil)
|
||
} else {
|
||
body := h.buildExecBody(shellType, password, cmdParam, cmd)
|
||
httpReq, err = http.NewRequest(http.MethodPost, targetURL, bytes.NewReader(body))
|
||
if err == nil {
|
||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
}
|
||
}
|
||
if err != nil {
|
||
return "", false
|
||
}
|
||
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyberStrikeAI-WebShell/1.0)")
|
||
resp, err := h.client.Do(httpReq)
|
||
if err != nil {
|
||
return "", false
|
||
}
|
||
defer resp.Body.Close()
|
||
raw, _ := io.ReadAll(resp.Body)
|
||
return decodeWebshellOutput(raw, encoding), resp.StatusCode == http.StatusOK
|
||
}
|
||
}
|
||
|
||
// persistDetectedOS 把探活结果回写到连接表;失败只记日志不阻断主流程。
|
||
// 设计上故意只触发 UPDATE,不会新建记录,因此即便 connectionID 不存在也只是悄悄放弃。
|
||
func (h *WebShellHandler) persistDetectedOS(connectionID, detected string) {
|
||
connectionID = strings.TrimSpace(connectionID)
|
||
detected = normalizeWebshellOS(detected)
|
||
if connectionID == "" || detected == "" || detected == "auto" {
|
||
return
|
||
}
|
||
conn, err := h.db.GetWebshellConnection(connectionID)
|
||
if err != nil || conn == nil {
|
||
// 不是所有调用方都能提供有效 ID(比如临时测试),这里静默返回
|
||
return
|
||
}
|
||
if normalizeWebshellOS(conn.OS) != "auto" {
|
||
// 用户已经显式选过 OS,尊重用户选择,不自动覆盖
|
||
return
|
||
}
|
||
conn.OS = detected
|
||
if err := h.db.UpdateWebshellConnection(conn); err != nil {
|
||
h.logger.Warn("webshell 探活结果持久化失败", zap.String("id", connectionID), zap.String("os", detected), zap.Error(err))
|
||
return
|
||
}
|
||
h.logger.Info("webshell auto OS 探活成功并持久化", zap.String("id", connectionID), zap.String("os", detected))
|
||
}
|