mirror of
https://github.com/praetorian-inc/wasmforge
synced 2026-06-22 22:52:31 +00:00
removing references to projects we haven't open sourced yet (#3)
* removing references to projects we haven't open sourced yet * remove last Tribunus reference from test/.gitignore (reviewer nit)
This commit is contained in:
@@ -82,8 +82,8 @@ GOWORK=off GOOS=darwin GOARCH=amd64 /tmp/wasmforge-bin build \
|
||||
- **Platform-specific source.** Windows and macOS Sliver sources are NOT interchangeable. Windows `runner.go` references `handlers.WrapperHandler` (token impersonation) which only exists in `handlers_windows.go`. Always generate source targeting the correct OS.
|
||||
- **Beacon vs Session.** Beacons check in periodically (tasks are queued). Sessions are interactive and support SOCKS5 proxying. Choose based on your use case.
|
||||
- **`--skip-symbols`.** Recommended to reduce compile time. Sliver's symbol obfuscation adds significant build time and isn't needed since WasmForge already provides its own obfuscation layers.
|
||||
- **`--tags`.** Use for programs that gate features behind build tags (e.g., [Tribunus](https://github.com/praetorian-inc/tribunus) uses `-tags "shell,ps,netstat"` to enable specific command handlers).
|
||||
- **Binary sizes.** ~34.5MB (Windows), ~29.7MB (macOS), ~12.9MB ([Tribunus](https://github.com/praetorian-inc/tribunus)).
|
||||
- **`--tags`.** Use for programs that gate features behind build tags (e.g., `-tags "shell,ps,netstat"` to enable specific command handlers in an agent that supports it).
|
||||
- **Binary sizes.** ~34.5MB (Windows), ~29.7MB (macOS).
|
||||
|
||||
## Verified Commands
|
||||
|
||||
@@ -92,7 +92,6 @@ GOWORK=off GOOS=darwin GOARCH=amd64 /tmp/wasmforge-bin build \
|
||||
| Windows | Sliver | Beacon | `whoami`, `ls`, `pwd`, `execute`, `info`, `getprivs`, `ps`, `netstat`, `ifconfig`, `execute-assembly` (Rubeus, Seatbelt) |
|
||||
| macOS | Sliver | Beacon | `pwd`, `ls`, `download`, `mkdir`, `execute` |
|
||||
| macOS | Sliver | Session | `pwd`, `ls`, `download`, `mkdir`, `execute`, SOCKS5 proxy |
|
||||
| Windows | [Tribunus](https://github.com/praetorian-inc/tribunus) (Mythic) | Agent | `shell` (whoami, dir, ipconfig, hostname, echo, ver), `whoami`, `ps`, `netstat` |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
||||
@@ -65,9 +65,6 @@ Go programs using [`ebitengine/purego`](https://github.com/ebitengine/purego) fo
|
||||
| ---------------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| **Sliver** (beacon) | C2 framework | `pwd`, `ls`, `download`, `mkdir`, `execute` |
|
||||
| **Sliver** (session) | C2 framework, interactive mode | `pwd`, `ls`, `download`, `mkdir`, `execute`, SOCKS5 proxy |
|
||||
| **[Sibyl](https://github.com/praetorian-inc/sibyl)** | Mythic C2 agent using `purego/objc` | NSURLSession transport, ObjC class registration, TLS delegate, `dispatch_semaphore`, full agent init |
|
||||
|
||||
[Sibyl](https://github.com/praetorian-inc/sibyl) demonstrates the most complex purego/ObjC integration validated to date — a fully native-looking macOS agent that compiles through WasmForge with zero source modifications.
|
||||
|
||||
## Building Sliver for macOS
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ Registered in `internal/hostmod/pipe.go`.
|
||||
## --win32-apis Go-side surface (non-NativeAOT)
|
||||
|
||||
Functions used by `wasmforge build --win32-apis` with Go source input
|
||||
(Sliver, Tribunus, gogokatz, goffloader, etc.). Registered via
|
||||
(Sliver, goffloader, etc.). Registered via
|
||||
`internal/hostmod/win32.go`. Not exercised by the Rubeus/Seatbelt parity
|
||||
tests, but kept for the broader product surface.
|
||||
|
||||
|
||||
@@ -124,8 +124,9 @@ func CompileWASM(patchedGOROOT, pkg, tmpDir string, verbose, win32APIs bool, tar
|
||||
}
|
||||
|
||||
// Inject wasip1 overrides for unsafe host pointer dereference patterns.
|
||||
// Sibyl's transport/unsafe_helpers.go uses unsafe.Slice on host pointers
|
||||
// which traps in WASM. Replace with darwin.ReadHostMemory-based versions.
|
||||
// Guests that use unsafe.Slice on host pointers (common in transport
|
||||
// helpers around purego/ObjC) trap in WASM. Replace with
|
||||
// darwin.ReadHostMemory-based versions.
|
||||
if workDir != "" && workDir != originalWorkDir && targetGOOS == "darwin" {
|
||||
if n, err := injectUnsafeHelperOverrides(workDir, verbose); err != nil && verbose {
|
||||
fmt.Fprintf(os.Stderr, "wasmforge: unsafe helper override warning: %v\n", err)
|
||||
@@ -441,8 +442,8 @@ func injectSysshimVendored(shadowDir, sysshimDir string, verbose bool) error {
|
||||
}
|
||||
|
||||
// injectUnsafeHelperOverrides walks the shadow copy looking for Go files that
|
||||
// use unsafe.Slice to dereference host pointers (e.g., Sibyl's
|
||||
// transport/unsafe_helpers.go). For each match, it injects a _wasip1.go
|
||||
// use unsafe.Slice to dereference host pointers (a common pattern in transport
|
||||
// helpers built around purego/ObjC). For each match, it injects a _wasip1.go
|
||||
// override that uses darwin.ReadHostMemory instead.
|
||||
func injectUnsafeHelperOverrides(workDir string, verbose bool) (int, error) {
|
||||
count := 0
|
||||
|
||||
@@ -121,7 +121,7 @@ const maxRegAllocStructSize = 16
|
||||
|
||||
// Send is a convenience method for sending messages to objects that can return any type.
|
||||
// Note: objc_msgSend_stret is not registered in this sysshim. Struct returns >16 bytes
|
||||
// on amd64 will panic. Sibyl only uses pointer/integer returns via ObjC messaging.
|
||||
// on amd64 will panic. Validated guests only use pointer/integer returns via ObjC messaging.
|
||||
func Send[T any](id ID, sel SEL, args ...any) T {
|
||||
var zero T
|
||||
if reflect.ValueOf(zero).Kind() == reflect.Struct &&
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Package unix provides minimal stubs of golang.org/x/sys/unix for wasip1.
|
||||
// Only functions actually used by Sibyl are implemented.
|
||||
// Only functions actually exercised by validated guests are implemented;
|
||||
// adding more is mechanical when a new caller surfaces.
|
||||
|
||||
//go:build wasip1
|
||||
|
||||
|
||||
@@ -7,9 +7,6 @@ testdata/assemblies/
|
||||
# Sliver implant source snapshots.
|
||||
testdata/sliver-src/
|
||||
|
||||
# Tribunus agent source snapshots.
|
||||
testdata/tribunus-src/
|
||||
|
||||
# Temp files from test runs.
|
||||
*.zip
|
||||
*.exe
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
//go:build mythic
|
||||
|
||||
package c2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MythicClient wraps Mythic's Hasura GraphQL API for test automation.
|
||||
type MythicClient struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
token string
|
||||
}
|
||||
|
||||
// NewMythicClient authenticates to Mythic and returns a client.
|
||||
func NewMythicClient(apiURL, username, passwordEnv string) (*MythicClient, error) {
|
||||
password := os.Getenv(passwordEnv)
|
||||
if password == "" {
|
||||
return nil, fmt.Errorf("env var %s not set", passwordEnv)
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
// Authenticate to get JWT.
|
||||
authURL := strings.TrimSuffix(apiURL, "/graphql") + "/auth"
|
||||
authBody, _ := json.Marshal(map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
})
|
||||
|
||||
resp, err := client.Post(authURL, "application/json", bytes.NewReader(authBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authenticating: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var authResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := json.Unmarshal(body, &authResp); err != nil {
|
||||
return nil, fmt.Errorf("parsing auth response: %w (body: %s)", err, body)
|
||||
}
|
||||
if authResp.AccessToken == "" {
|
||||
return nil, fmt.Errorf("auth failed: %s", body)
|
||||
}
|
||||
|
||||
return &MythicClient{
|
||||
baseURL: apiURL,
|
||||
httpClient: client,
|
||||
token: authResp.AccessToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// graphqlQuery executes a GraphQL query and returns the parsed response.
|
||||
func (m *MythicClient) graphqlQuery(query string, variables map[string]any) (map[string]any, error) {
|
||||
reqBody, _ := json.Marshal(map[string]any{
|
||||
"query": query,
|
||||
"variables": variables,
|
||||
})
|
||||
|
||||
graphqlURL := m.baseURL
|
||||
if !strings.HasSuffix(graphqlURL, "/graphql") {
|
||||
graphqlURL += "/graphql"
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", graphqlURL, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+m.token)
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("parsing graphql response: %w (body: %s)", err, body)
|
||||
}
|
||||
|
||||
if errors, ok := result["errors"]; ok {
|
||||
return nil, fmt.Errorf("graphql errors: %v", errors)
|
||||
}
|
||||
|
||||
data, ok := result["data"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected response structure: %s", body)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// WaitForCallback polls for a callback matching the given payload UUID.
|
||||
// If payloadUUID is empty, matches the most recently created callback.
|
||||
func (m *MythicClient) WaitForCallback(payloadUUID string, timeout time.Duration) (*Beacon, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
// Use filtered query when UUID provided, unfiltered (latest) otherwise.
|
||||
var query string
|
||||
var variables map[string]any
|
||||
|
||||
if payloadUUID != "" {
|
||||
query = `query GetCallbacks($uuid: String!) {
|
||||
callback(where: {registered_payload: {uuid: {_eq: $uuid}}}, order_by: {id: desc}, limit: 1) {
|
||||
id
|
||||
host
|
||||
user
|
||||
pid
|
||||
os
|
||||
}
|
||||
}`
|
||||
variables = map[string]any{"uuid": payloadUUID}
|
||||
} else {
|
||||
query = `query GetLatestCallback {
|
||||
callback(order_by: {id: desc}, limit: 1) {
|
||||
id
|
||||
host
|
||||
user
|
||||
pid
|
||||
os
|
||||
}
|
||||
}`
|
||||
variables = nil
|
||||
}
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
data, err := m.graphqlQuery(query, variables)
|
||||
if err != nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
callbacks, ok := data["callback"].([]any)
|
||||
if ok && len(callbacks) > 0 {
|
||||
cb := callbacks[0].(map[string]any)
|
||||
return &Beacon{
|
||||
ID: fmt.Sprintf("%.0f", cb["id"]),
|
||||
Hostname: fmt.Sprintf("%v", cb["host"]),
|
||||
Username: fmt.Sprintf("%v", cb["user"]),
|
||||
OS: fmt.Sprintf("%v", cb["os"]),
|
||||
PID: int(cb["pid"].(float64)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no callback for payload %s within %v", payloadUUID, timeout)
|
||||
}
|
||||
|
||||
// CreateTask issues a command to a callback and returns the task ID.
|
||||
func (m *MythicClient) CreateTask(callbackID int, command, params string) (int, error) {
|
||||
query := `mutation CreateTask($callback_id: Int!, $command: String!, $params: String!) {
|
||||
createTask(callback_id: $callback_id, command: $command, params: $params) {
|
||||
id
|
||||
status
|
||||
error
|
||||
}
|
||||
}`
|
||||
|
||||
data, err := m.graphqlQuery(query, map[string]any{
|
||||
"callback_id": callbackID,
|
||||
"command": command,
|
||||
"params": params,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
task, ok := data["createTask"].(map[string]any)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected createTask response: %v", data)
|
||||
}
|
||||
|
||||
if errStr, ok := task["error"].(string); ok && errStr != "" {
|
||||
return 0, fmt.Errorf("task creation error: %s", errStr)
|
||||
}
|
||||
|
||||
taskID := int(task["id"].(float64))
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
// WaitForTaskResult polls until a task completes and returns its output.
|
||||
func (m *MythicClient) WaitForTaskResult(taskID int, timeout time.Duration) (string, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
query := `query GetTask($id: Int!) {
|
||||
task_by_pk(id: $id) {
|
||||
status
|
||||
completed
|
||||
responses {
|
||||
response
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
data, err := m.graphqlQuery(query, map[string]any{"id": taskID})
|
||||
if err != nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
task, ok := data["task_by_pk"].(map[string]any)
|
||||
if !ok {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
completed, _ := task["completed"].(bool)
|
||||
if !completed {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
responses, ok := task["responses"].([]any)
|
||||
if !ok || len(responses) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var output strings.Builder
|
||||
for _, r := range responses {
|
||||
resp := r.(map[string]any)
|
||||
if s, ok := resp["response"].(string); ok {
|
||||
output.WriteString(s)
|
||||
output.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(output.String()), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("task %d did not complete within %v", taskID, timeout)
|
||||
}
|
||||
@@ -14,7 +14,6 @@ type Config struct {
|
||||
WasmForge WasmForgeConfig `toml:"wasmforge"`
|
||||
Remote RemoteConfig `toml:"remote"`
|
||||
Sliver SliverConfig `toml:"sliver"`
|
||||
Mythic MythicConfig `toml:"mythic"`
|
||||
Proxy ProxyConfig `toml:"proxy"`
|
||||
Dotnet DotnetConfig `toml:"dotnet"`
|
||||
}
|
||||
@@ -62,14 +61,6 @@ type SliverConfig struct {
|
||||
NativeRubeusPath string `toml:"native_rubeus_path"`
|
||||
}
|
||||
|
||||
type MythicConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
APIURL string `toml:"api_url"`
|
||||
Username string `toml:"username"`
|
||||
PasswordEnv string `toml:"password_env"`
|
||||
TribunusSource string `toml:"tribunus_source"`
|
||||
}
|
||||
|
||||
type ProxyConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
ChiselSource string `toml:"chisel_source"`
|
||||
@@ -123,7 +114,6 @@ func LoadConfig() (*Config, error) {
|
||||
cfg.Sliver.RubeusPath = expandHome(cfg.Sliver.RubeusPath)
|
||||
cfg.Sliver.NativeSeatbeltPath = expandHome(cfg.Sliver.NativeSeatbeltPath)
|
||||
cfg.Sliver.NativeRubeusPath = expandHome(cfg.Sliver.NativeRubeusPath)
|
||||
cfg.Mythic.TribunusSource = expandHome(cfg.Mythic.TribunusSource)
|
||||
cfg.Proxy.ChiselSource = expandHome(cfg.Proxy.ChiselSource)
|
||||
cfg.Proxy.LigoloSource = expandHome(cfg.Proxy.LigoloSource)
|
||||
cfg.Dotnet.SeatbeltSource = expandHome(cfg.Dotnet.SeatbeltSource)
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
//go:build integration && mythic
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/praetorian-inc/wftest/c2"
|
||||
)
|
||||
|
||||
func TestMythic(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping C2 test in short mode")
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("loading config: %v", err)
|
||||
}
|
||||
if !cfg.Mythic.Enabled {
|
||||
t.Skip("mythic tests disabled in config")
|
||||
}
|
||||
if cfg.Mythic.TribunusSource == "" {
|
||||
t.Skip("mythic tribunus_source not configured")
|
||||
}
|
||||
if !cfg.Remote.Enabled || !labctlAvailable() {
|
||||
t.Skip("remote testing not available (labctl or remote.enabled)")
|
||||
}
|
||||
|
||||
// Connect to Mythic.
|
||||
mythicClient, err := c2.NewMythicClient(cfg.Mythic.APIURL, cfg.Mythic.Username, cfg.Mythic.PasswordEnv)
|
||||
if err != nil {
|
||||
t.Fatalf("connecting to Mythic: %v", err)
|
||||
}
|
||||
|
||||
// Copy source to temp dir for config patching.
|
||||
tmpDir, err := os.MkdirTemp("", "wftest-tribunus-*")
|
||||
if err != nil {
|
||||
t.Fatalf("creating temp dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { os.RemoveAll(tmpDir) })
|
||||
|
||||
if err := copyDir(cfg.Mythic.TribunusSource, tmpDir); err != nil {
|
||||
t.Fatalf("copying tribunus source: %v", err)
|
||||
}
|
||||
|
||||
// Build with WasmForge.
|
||||
t.Log("building Tribunus agent...")
|
||||
bin := WasmForgeBuild(t, cfg, tmpDir, BuildOpts{
|
||||
GOOS: "windows",
|
||||
GOARCH: "amd64",
|
||||
Win32APIs: true,
|
||||
Tags: "shell,ps,netstat,execute_assembly,execute_coff,execute_pe,injection,token,portscan,ldapsearch,socks",
|
||||
})
|
||||
t.Logf("built Tribunus agent in %v (%s)", bin.Duration, bin.Path)
|
||||
|
||||
// Deploy to Win11.
|
||||
machine := cfg.Remote.Win11.Machine
|
||||
remotePath := cfg.Remote.Win11.WorkDir + `\tribunus-test.exe`
|
||||
|
||||
labctlKill(t, machine, "tribunus-test.exe")
|
||||
labctlPush(t, bin.Path, machine, remotePath, true)
|
||||
labctlExecBackground(t, machine, remotePath)
|
||||
|
||||
// Wait for callback.
|
||||
t.Log("waiting for Mythic callback...")
|
||||
// Use empty UUID to match any callback (the payload UUID is compiled into the binary).
|
||||
callback, err := mythicClient.WaitForCallback("", 90*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("no Mythic callback: %v", err)
|
||||
}
|
||||
t.Logf("got callback: %s (host=%s, user=%s, pid=%d)",
|
||||
callback.ID, callback.Hostname, callback.Username, callback.PID)
|
||||
|
||||
callbackID := 0
|
||||
fmt.Sscanf(callback.ID, "%d", &callbackID)
|
||||
if callbackID == 0 {
|
||||
t.Fatalf("invalid callback ID: %s", callback.ID)
|
||||
}
|
||||
|
||||
// Cleanup on exit.
|
||||
t.Cleanup(func() {
|
||||
labctlKill(t, machine, "tribunus-test.exe")
|
||||
labctlCleanup(t, "kali")
|
||||
})
|
||||
|
||||
// Run command battery.
|
||||
mythicCommands := []struct {
|
||||
name string
|
||||
command string
|
||||
params string
|
||||
check func(t *testing.T, output string)
|
||||
}{
|
||||
{
|
||||
name: "shell/whoami",
|
||||
command: "shell",
|
||||
params: "whoami",
|
||||
check: func(t *testing.T, output string) {
|
||||
if output == "" {
|
||||
t.Fatal("whoami returned empty")
|
||||
}
|
||||
t.Logf("whoami: %s", strings.TrimSpace(output))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "shell/hostname",
|
||||
command: "shell",
|
||||
params: "hostname",
|
||||
check: func(t *testing.T, output string) {
|
||||
if output == "" {
|
||||
t.Fatal("hostname returned empty")
|
||||
}
|
||||
t.Logf("hostname: %s", strings.TrimSpace(output))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ps",
|
||||
command: "ps",
|
||||
params: "",
|
||||
check: func(t *testing.T, output string) {
|
||||
if output == "" {
|
||||
t.Fatal("ps returned empty")
|
||||
}
|
||||
t.Logf("ps output: %d bytes", len(output))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "netstat",
|
||||
command: "netstat",
|
||||
params: "",
|
||||
check: func(t *testing.T, output string) {
|
||||
if output == "" {
|
||||
t.Fatal("netstat returned empty")
|
||||
}
|
||||
t.Logf("netstat output: %d bytes", len(output))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, mc := range mythicCommands {
|
||||
mc := mc
|
||||
t.Run(mc.name, func(t *testing.T) {
|
||||
taskID, err := mythicClient.CreateTask(callbackID, mc.command, mc.params)
|
||||
if err != nil {
|
||||
t.Fatalf("creating task %q: %v", mc.command, err)
|
||||
}
|
||||
|
||||
output, err := mythicClient.WaitForTaskResult(taskID, 60*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("waiting for task %d: %v", taskID, err)
|
||||
}
|
||||
|
||||
mc.check(t, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// copyDir recursively copies src to dst.
|
||||
func copyDir(src, dst string) error {
|
||||
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := filepath.Join(dst, rel)
|
||||
|
||||
if info.IsDir() {
|
||||
return os.MkdirAll(target, 0o755)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(target, data, info.Mode())
|
||||
})
|
||||
}
|
||||
@@ -43,18 +43,6 @@ rubeus_path = ""
|
||||
native_seatbelt_path = ""
|
||||
native_rubeus_path = ""
|
||||
|
||||
[mythic]
|
||||
# Enable Mythic/Tribunus integration tests.
|
||||
enabled = false
|
||||
# Mythic API URL (Hasura GraphQL endpoint).
|
||||
api_url = "https://10.3.10.99:7443"
|
||||
# Mythic admin username.
|
||||
username = "mythic_admin"
|
||||
# Env var containing Mythic admin password.
|
||||
password_env = "WFTEST_MYTHIC_PASSWORD"
|
||||
# Path to Tribunus agent source.
|
||||
tribunus_source = ""
|
||||
|
||||
[proxy]
|
||||
# Enable proxy tool tests (chisel, ligolo-ng).
|
||||
enabled = false
|
||||
|
||||
Vendored
+1
-1
@@ -82,7 +82,7 @@ func main() {
|
||||
passed++
|
||||
}
|
||||
|
||||
// Test 7: Get NSURLSession class (Sibyl's transport)
|
||||
// Test 7: Get NSURLSession class (typical transport bootstrapping)
|
||||
nsURLSessionClass := objc.GetClass("NSURLSession")
|
||||
if nsURLSessionClass == 0 {
|
||||
fmt.Fprintf(os.Stderr, "FAIL: GetClass NSURLSession = 0\n")
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
// win32_clr_load tests _AppDomain::Load_3 with a real .NET assembly.
|
||||
// Uses ICorRuntimeHost + SyscallN (same as go-clr/Tribunus).
|
||||
// Uses ICorRuntimeHost + SyscallN (the standard go-clr pattern).
|
||||
//
|
||||
// Build: GOOS=windows GOARCH=amd64 wasmforge build --win32-apis -v -o clr_load.exe ./testdata/win32_clr_load
|
||||
// Run: Copy Seatbelt.exe to C:\Temp\ on Win11, then run clr_load.exe
|
||||
|
||||
Reference in New Issue
Block a user