Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: build

on:
push:
branches: [main]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.22"

- name: gofmt
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "These files need gofmt:"
echo "$unformatted"
exit 1
fi

- name: go vet
run: go vet ./...

- name: go test
run: go test ./...

- name: go build
run: go build ./...
120 changes: 90 additions & 30 deletions Loader/Loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ type Beacon_SSL struct {
var num_Profile int
var Post bool

// validateNumber checks an operator-supplied numeric flag before it reaches the
// profile. These values were written out unchecked, so "-Sleep abc" emitted
// `set sleeptime "abc000"` and "-Jitter 150" emitted a jitter percentage
// outside the permitted 0-99 range. Neither failed here: they failed when the
// teamserver refused to load the profile, which is the worst time to find out.
// A max of 0 means the flag has no meaningful upper bound.
func validateNumber(flagName, value string, min, max int) {
n, err := strconv.Atoi(value)
if err != nil {
log.Fatalf("Error: %s must be a whole number, got %q", flagName, value)
}
if n < min {
log.Fatalf("Error: %s must be %d or greater, got %d", flagName, min, n)
}
if max > 0 && n > max {
log.Fatalf("Error: %s must be between %d and %d, got %d", flagName, min, max, n)
}
}

func GenerateOptions(stage, sleeptime, jitter, useragent, uri, customuri, customuriGET, customuriPOST, beacon_PE, processinject_min_alloc, Post_EX_Process_Name, metadata, injector, Host, Profile, ProfilePath, outFile, custom_cert, cert_password, CDN, CDN_Value, datajitter, Keylogger string, Forwarder bool, tasks_max_size string, tasks_proxy_max_size string, tasks_dns_proxy_max_size string, syscall_method string, httplib string, ThreadSpoof bool, beacongate string, eaf_bypass bool, rdll_use_syscalls bool, copy_pe_header bool, rdll_loader string, transform_obfuscate string, smartinject bool, sleep_mask bool) {
Beacon_Com := &Beacon_Com{}
Beacon_Stage_p1 := &Beacon_Stage_p1{}
Expand All @@ -89,9 +108,9 @@ func GenerateOptions(stage, sleeptime, jitter, useragent, uri, customuri, custom

fmt.Println("[*] Preparing Varibles...")
HostStageMessage, Beacon_Com.Variables = GenerateComunication(stage, sleeptime, jitter, useragent, datajitter, tasks_max_size, tasks_proxy_max_size, tasks_dns_proxy_max_size, httplib)
Beacon_PostEX.Variables = GeneratePostProcessName(Post_EX_Process_Name, Keylogger, ThreadSpoof)
Beacon_PostEX.Variables = GeneratePostProcessName(Post_EX_Process_Name, Keylogger, ThreadSpoof, smartinject)
Beacon_GETPOST.Variables = GenerateHTTPVaribles(Host, metadata, uri, customuri, customuriGET, customuriPOST, CDN, CDN_Value, Profile, Forwarder)
Beacon_Stage_p1.Variables, Beacon_Stage_p2.Variables, syscall_method = GeneratePE(beacon_PE, syscall_method, beacongate, eaf_bypass, rdll_use_syscalls, copy_pe_header, rdll_loader, transform_obfuscate, smartinject, sleep_mask)
Beacon_Stage_p1.Variables, Beacon_Stage_p2.Variables, syscall_method = GeneratePE(beacon_PE, syscall_method, beacongate, eaf_bypass, rdll_use_syscalls, copy_pe_header, rdll_loader, transform_obfuscate, sleep_mask)
Process_Inject.Variables = GenerateProcessInject(processinject_min_alloc, injector)
Beacon_GETPOST_Profile.Variables, Beacon_SSL.Variables = GenerateProfile(Profile, CDN, CDN_Value, cert_password, custom_cert, ProfilePath, Host)
fmt.Println("[*] Building Profile...")
Expand All @@ -112,8 +131,10 @@ func GenerateOptions(stage, sleeptime, jitter, useragent, uri, customuri, custom
} else {
fmt.Println("[!] " + syscall_method + " syscall method selected")
}
Name, _ := strconv.Atoi(Profile)
fmt.Println("[*] Seleted Profile: " + Struct.Profile_Names[Name])
// num_Profile holds the resolved profile, including the one picked at
// random when -Profile was not supplied. Re-parsing the raw flag here meant
// a randomly selected profile always printed as an empty name.
fmt.Println("[*] Selected Profile: " + Struct.Profile_Names[num_Profile])
fmt.Println("[+] Profile Generated: " + outFile)
fmt.Println("[+] Happy Hacking")
}
Expand All @@ -130,42 +151,49 @@ func GenerateComunication(stage, sleeptime, jitter, useragent, datajitter string
HostStageMessage = "[!] Host Staging Is Enabled - Staged Payloads Are Available But Your Beacon Payload Is Available To Anyone That Connects To Your Server To Request It"
}
if sleeptime != "" {
validateNumber("-Sleep", sleeptime, 0, 0)
Beacon_Com.Variables["sleep"] = sleeptime + "000"
} else if sleeptime == "" {
Beacon_Com.Variables["sleep"] = Utils.GenerateNumer(30, 75) + "000"
}
if jitter != "" {
// Cobalt Strike requires jitter to be a percentage in the range 0-99.
validateNumber("-Jitter", jitter, 0, 99)
Beacon_Com.Variables["jitter"] = jitter
}
if jitter == "" {
Beacon_Com.Variables["jitter"] = Utils.GenerateNumer(10, 40)
}
if datajitter != "" {
validateNumber("-Datajitter", datajitter, 0, 0)
Beacon_Com.Variables["datajitter"] = datajitter
}
if datajitter == "" {
Beacon_Com.Variables["datajitter"] = Utils.GenerateNumer(10, 60)
}

if tasks_max_size != "" {
validateNumber("-TasksMaxSize", tasks_max_size, 1, 0)
Beacon_Com.Variables["tasks_max_size"] = tasks_max_size
} else {
Beacon_Com.Variables["tasks_max_size"] = "1048576"
}
if tasks_proxy_max_size != "" {
validateNumber("-TasksProxyMaxSize", tasks_proxy_max_size, 1, 0)
Beacon_Com.Variables["tasks_proxy_max_size"] = tasks_proxy_max_size
} else {
Beacon_Com.Variables["tasks_proxy_max_size"] = "921600"
}
if tasks_dns_proxy_max_size != "" {
validateNumber("-TasksDnsProxyMaxSize", tasks_dns_proxy_max_size, 1, 0)
Beacon_Com.Variables["tasks_dns_proxy_max_size"] = tasks_dns_proxy_max_size
} else {
Beacon_Com.Variables["tasks_dns_proxy_max_size"] = "71680"
}
SSH_Numb, _ := strconv.Atoi(Utils.GenerateNumer(0, 4))
SSH_Numb := Utils.RandIndex(len(Struct.SSH_Banner))
Beacon_Com.Variables["SSH_Banner"] = Struct.SSH_Banner[SSH_Numb]

pipe_number, _ := strconv.Atoi(Utils.GenerateNumer(0, 7))
pipe_number := Utils.RandIndex(len(Struct.Pipename_list))
Beacon_Com.Variables["pipename"] = Struct.Pipename_list[pipe_number] + Utils.GenerateNumer(3000, 9000)
Beacon_Com.Variables["pipename_stager"] = Struct.Pipename_list[pipe_number] + Utils.GenerateNumer(1000, 9000)
Beacon_Com.Variables["SSH_pipename"] = Struct.Pipename_list[pipe_number]
Expand Down Expand Up @@ -204,7 +232,7 @@ func GenerateComunication(stage, sleeptime, jitter, useragent, datajitter string
}
}
if useragent == "" {
num_agent, _ := strconv.Atoi(Utils.GenerateNumer(0, 64))
num_agent := Utils.RandIndex(len(Struct.Useragent_list))
Beacon_Com.Variables["useragent"] = Struct.Useragent_list[num_agent]

}
Expand All @@ -218,26 +246,41 @@ func GenerateComunication(stage, sleeptime, jitter, useragent, datajitter string
return HostStageMessage, Beacon_Com.Variables
}

func GeneratePostProcessName(Post_EX_Process_Name, Keylogger string, ThreadSpoof bool) map[string]string {
func GeneratePostProcessName(Post_EX_Process_Name, Keylogger string, ThreadSpoof bool, smartinject bool) map[string]string {
Beacon_PostEX := &Beacon_PostEX{}
Beacon_PostEX.Variables = make(map[string]string)
// smartinject is a post-ex option. It used to be emitted into the stage
// block instead, where Cobalt Strike rejects it, while the post-ex copy was
// hardcoded to "true" - so -SmartInject drove the invalid one and had no
// effect on the profile that was actually meant to carry it.
if smartinject {
Beacon_PostEX.Variables["smartinject"] = "true"
} else {
Beacon_PostEX.Variables["smartinject"] = "false"
}
if Post_EX_Process_Name != "" {
num_PSPN, _ := strconv.Atoi(Post_EX_Process_Name)
num_PSPN, err := strconv.Atoi(Post_EX_Process_Name)
if err != nil || num_PSPN < 1 || num_PSPN > len(Struct.Post_EX_Process_Name) {
log.Fatalf("Error: PostEX_Name must be a number between 1 and %d", len(Struct.Post_EX_Process_Name))
}
Beacon_PostEX.Variables["Post_EX_Process_Name"] = Struct.Post_EX_Process_Name[(num_PSPN - 1)]
}
if Post_EX_Process_Name == "" {
num_Post_EX_Process_Name, _ := strconv.Atoi(Utils.GenerateNumer(0, 14))
num_Post_EX_Process_Name := Utils.RandIndex(len(Struct.Post_EX_Process_Name))
Beacon_PostEX.Variables["Post_EX_Process_Name"] = Struct.Post_EX_Process_Name[num_Post_EX_Process_Name]
}
if Keylogger == "GetAsyncKeyState" || Keylogger == "SetWindowsHookEx" {
Beacon_PostEX.Variables["Keylogger"] = Keylogger
} else if Keylogger == "" {
Beacon_PostEX.Variables["Keylogger"] = "SetWindowsHookEx"
} else {
// Previously an empty branch, which left the keylogger unset and
// emitted a profile Cobalt Strike rejects at load time.
log.Fatal("Error: Keylogger must be either GetAsyncKeyState or SetWindowsHookEx")
}

if ThreadSpoof == true {
threadhint_num, _ := strconv.Atoi(Utils.GenerateNumer(0, 8))
threadhint_num := Utils.RandIndex(len(Struct.Thread_list))
Beacon_PostEX.Variables["thread_hint"] = "set thread_hint \"" + Struct.Thread_list[(threadhint_num)] + Utils.GenHex() + "\";"
} else {
Beacon_PostEX.Variables["thread_hint"] = ""
Expand All @@ -251,9 +294,15 @@ func GenerateHTTPVaribles(Host, metadata, uri, customuri, customuriGET, customur
Beacon_GETPOST.Variables = make(map[string]string)
Beacon_GETPOST.Variables["Host"] = Host
if Profile == "" {
// Profiles 5-7 need a keystore/CDN and 8 needs a ProfilePath, so the
// random pick stays within the self-contained profiles.
num_Profile, _ = strconv.Atoi(Utils.GenerateNumer(1, 5))
} else {
num_Profile, _ = strconv.Atoi(Profile)
var err error
num_Profile, err = strconv.Atoi(Profile)
if err != nil || num_Profile < 1 || num_Profile >= len(Struct.Profile_Names) {
log.Fatalf("Error: Profile must be a number between 1 and %d", len(Struct.Profile_Names)-1)
}
}
if metadata == "base64" {
Beacon_GETPOST.Variables["metadata_mode"] = metadata
Expand Down Expand Up @@ -319,6 +368,14 @@ func GenerateHTTPVaribles(Host, metadata, uri, customuri, customuriGET, customur
Beacon_GETPOST.Variables["UValue"] = Utils.GenerateValue(6, 15)
Beacon_GETPOST.Variables["CSMValue"] = Utils.GenerateValue(6, 15)

// Stager URIs are generated per architecture, and deliberately not from
// UValue: UValue also appears in the beacon's own check-in traffic (the
// "U="/"REF=ID=" prepends and the wla42 cookie), so reusing it here would
// tie the staging request and the check-ins together with one shared
// token. Length is varied so the segment isn't a fixed-width tell.
Beacon_GETPOST.Variables["stager_x86"] = Utils.GenerateSingleValue(8 + Utils.RandIndex(5))
Beacon_GETPOST.Variables["stager_x64"] = Utils.GenerateSingleValue(8 + Utils.RandIndex(5))

//needs to be put stacic
if Forwarder == true {
Beacon_GETPOST.Variables["forward"] = "true"
Expand All @@ -329,7 +386,7 @@ func GenerateHTTPVaribles(Host, metadata, uri, customuri, customuriGET, customur
return Beacon_GETPOST.Variables
}

func GeneratePE(beacon_PE string, syscall_method string, beacongate string, eaf_bypass bool, rdll_use_syscalls bool, copy_pe_header bool, rdll_loader string, transform_obfuscate string, smartinject bool, sleep_mask bool) (map[string]string, map[string]string, string) {
func GeneratePE(beacon_PE string, syscall_method string, beacongate string, eaf_bypass bool, rdll_use_syscalls bool, copy_pe_header bool, rdll_loader string, transform_obfuscate string, sleep_mask bool) (map[string]string, map[string]string, string) {
Beacon_Stage_p1 := &Beacon_Stage_p1{}
Beacon_Stage_p1.Variables = make(map[string]string)

Expand Down Expand Up @@ -371,17 +428,12 @@ func GeneratePE(beacon_PE string, syscall_method string, beacongate string, eaf_
} else {
Beacon_Stage_p1.Variables["copy_pe_header"] = "false"
}
if smartinject == true {
Beacon_Stage_p1.Variables["smartinject"] = "true"
} else {
Beacon_Stage_p1.Variables["smartinject"] = "false"
}
if sleep_mask == true {
Beacon_Stage_p1.Variables["sleep_mask"] = "true"
} else {
Beacon_Stage_p1.Variables["sleep_mask"] = "false"
}
gen_number, _ := strconv.Atoi(Utils.GenerateNumer(0, 6))
gen_number := Utils.RandIndex(len(Struct.Magic_PE))
Beacon_Stage_p1.Variables["magic_mz_x64"] = Struct.Magic_PE[gen_number]
Beacon_Stage_p1.Variables["magic_pe"] = strings.ToUpper(Utils.GenerateSingleValue(2))

Expand Down Expand Up @@ -410,13 +462,13 @@ func GeneratePE(beacon_PE string, syscall_method string, beacongate string, eaf_
}

if beacon_PE == "" {
PE_Num, _ := strconv.Atoi(Utils.GenerateNumer(0, 30))
PE_Num := Utils.RandIndex(len(Struct.Peclone_list))
Beacon_Stage_p2.Variables["pe"] = Struct.Peclone_list[PE_Num]
}
if beacon_PE != "" {
PE_Num, _ := strconv.Atoi(beacon_PE)
if PE_Num > 30 {
log.Fatal("Error: Please provide a valid PE number less the 31 option")
PE_Num, err := strconv.Atoi(beacon_PE)
if err != nil || PE_Num < 1 || PE_Num > len(Struct.Peclone_list) {
log.Fatalf("Error: PE_Clone must be a number between 1 and %d", len(Struct.Peclone_list))
}
Beacon_Stage_p2.Variables["pe"] = Struct.Peclone_list[(PE_Num - 1)]
}
Expand Down Expand Up @@ -467,15 +519,20 @@ func GenerateProcessInject(processinject_min_alloc, injector string) map[string]
Process_Inject.Variables["processinject_min_alloc"] = Utils.GenerateNumer(4096, 57841)
}
if processinject_min_alloc != "" {
processinject_min_alloc_int, _ := strconv.Atoi(processinject_min_alloc)
if processinject_min_alloc_int < 4096 {
log.Fatal("Error: Minimum amount of memory to request for injected content needs to be greater than 4096")
} else {
Process_Inject.Variables["processinject_min_alloc"] = processinject_min_alloc
}
// The Atoi error was discarded here, so "-Allocation abc" parsed as 0
// and reported the misleading "needs to be greater than 4096".
validateNumber("-Allocation", processinject_min_alloc, 4096, 0)
Process_Inject.Variables["processinject_min_alloc"] = processinject_min_alloc
}
Process_Inject.Variables["ThreadStartNum"] = Utils.GenerateNumer(500, 2500)
Process_Inject.Variables["ThreadStartNumv2"] = Utils.GenerateNumer(500, 2500)
if injector == "" {
// Every other optional flag either defaults or picks at random when
// left blank. Without a default here the else branch below fires and
// SourcePoint cannot generate a profile at all unless -Injector is
// passed, even though nothing documents it as required.
injector = "VirtualAllocEx"
}
if injector == "NtMapViewOfSection" {
Process_Inject.Variables["injector"] = injector
} else if injector == "VirtualAllocEx" {
Expand Down Expand Up @@ -508,7 +565,10 @@ func GenerateProfile(Profile, CDN, CDN_Value, cert_password, custom_cert, Profil
fmt.Println("[!] Self Signed SSL Cerificate Used")
} else if num_Profile == 6 {
if CDN == "" {
log.Fatal("Error: Please provide a CDN value in order to use AzureEdge profiles")
log.Fatal("Error: Please provide a CDN cookie name (-CDN) in order to use AzureEdge profiles")
}
if CDN_Value == "" {
log.Fatal("Error: Please provide a CDN cookie value (-CDN-Value) in order to use AzureEdge profiles")
}
if cert_password == "" {
log.Fatal("Error: Please provide a Password value to use this profile")
Expand Down
54 changes: 54 additions & 0 deletions Loader/Loader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package Loader

import "testing"

func httpVars(t *testing.T, profile string) map[string]string {
t.Helper()
return GenerateHTTPVaribles("acme-email.com", "base64url", "", "", "", "", "", "", profile, false)
}

// The Slack profile hardcoded its stager URIs ("/messages/DALBNSf25" and
// "/messages/DALBNSF25"), so every profile SourcePoint ever produced for that
// template carried the same two paths. They must now vary per run.
func TestStagerURIsVaryBetweenProfiles(t *testing.T) {
const runs = 50
seen := make(map[string]bool, runs*2)
for i := 0; i < runs; i++ {
v := httpVars(t, "2")
for _, key := range []string{"stager_x86", "stager_x64"} {
got := v[key]
if got == "" {
t.Fatalf("%s was not generated", key)
}
if seen[got] {
t.Errorf("%s repeated across generated profiles: %q", key, got)
}
seen[got] = true
}
}
}

// The GoToMeeting profile derived both stager URIs from a single value, so the
// x86 and x64 staging paths were byte-identical.
func TestStagerURIsDifferPerArchitecture(t *testing.T) {
for i := 0; i < 50; i++ {
v := httpVars(t, "3")
if v["stager_x86"] == v["stager_x64"] {
t.Fatalf("stager URIs identical across architectures: %q", v["stager_x86"])
}
}
}

// UValue appears in the beacon's own check-in traffic (the "U="/"REF=ID="
// prepends and the wla42 cookie). Deriving a stager URI from it linked the
// staging request and the check-ins by a shared unique token.
func TestStagerURIsAreIndependentOfCheckinToken(t *testing.T) {
for i := 0; i < 50; i++ {
v := httpVars(t, "3")
for _, key := range []string{"stager_x86", "stager_x64"} {
if v[key] == v["UValue"] {
t.Fatalf("%s reuses UValue, which also appears in check-in traffic: %q", key, v[key])
}
}
}
}
5 changes: 2 additions & 3 deletions Sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ CDN:
CDN_Value:
ProfilePath:
Syscall_method:
Httplib:
Httplib:
ThreadSpoof: True
Customuri:
CustomuriGET:
CustomuriGET:
CustomuriPOST:
Forwarder: False
TasksMaxSize:
Expand Down
Loading