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:
michaelweber
2026-06-16 16:17:10 -04:00
committed by GitHub
parent 541b4f001c
commit a983ca221e
13 changed files with 13 additions and 478 deletions
+2 -3
View File
@@ -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
-3
View File
@@ -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
+1 -1
View File
@@ -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.
+5 -4
View File
@@ -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
+1 -1
View File
@@ -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 &&
+2 -1
View File
@@ -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
-3
View File
@@ -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
-252
View File
@@ -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)
}
-10
View File
@@ -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)
-186
View File
@@ -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())
})
}
-12
View File
@@ -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
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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