diff --git a/.gitignore b/.gitignore index 6f5207db7..2c01468da 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ dist/** # Credentials .creds/** implants/imix/imix-test-config.json +.tavern-auth implants/golem/embed_files_golem_prod/* !implants/golem/embed_files_golem_prod/.gitkeep diff --git a/bin/pwnboard-go/auth.go b/bin/pwnboard-go/auth.go new file mode 100644 index 000000000..e3788c874 --- /dev/null +++ b/bin/pwnboard-go/auth.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "realm.pub/tavern/cli/auth" +) + +func getAuthToken(ctx context.Context, tavernURL, cachePath string) (auth.Token, error) { + tokenData, err := os.ReadFile(cachePath) + if os.IsNotExist(err) { + + token, err := auth.Authenticate( + ctx, + auth.BrowserFunc( + func(url string) error { + log.Printf("OPEN THIS: %s", url) + return nil + }, + ), + tavernURL, + ) + if err != nil { + return auth.Token(""), err + } + + if err := os.WriteFile(cachePath, []byte(token), 0640); err != nil { + log.Printf("[WARN] Failed to save token to credential cache (%q): %v", cachePath, err) + } + return token, nil + } + if err != nil { + return auth.Token(""), fmt.Errorf("failed to read credential cache (%q): %v", cachePath, err) + } + + log.Printf("Loaded authentication credentials from %q", cachePath) + return auth.Token(tokenData), nil +} diff --git a/bin/pwnboard-go/main.go b/bin/pwnboard-go/main.go new file mode 100644 index 000000000..206604a91 --- /dev/null +++ b/bin/pwnboard-go/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "time" + + "realm.pub/bin/pwnboard-go/pwnboard" + "realm.pub/bin/pwnboard-go/tavern" +) + +const ( + TAVERN_URL = "https://tavern.aws-metadata.com" + CREDENTIAL_PATH = ".tavern-auth" + + PWNBOARD_URL = "https://pwnboard.aws-metadata.com" + PWNBOARD_APP_NAME = "Realm" + + LOOKBACK_WINDOW = 3 * time.Minute + SLEEP_INTERVAL = 30 * time.Second + HTTP_TIMEOUT = 30 * time.Second +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + token, err := getAuthToken(ctx, TAVERN_URL, CREDENTIAL_PATH) + if err != nil { + log.Fatalf("failed to obtain authentication credentials: %v", err) + } + + tavern_client := &tavern.Client{ + Credential: token, + URL: fmt.Sprintf("%s/graphql", TAVERN_URL), + HTTP: &http.Client{ + Timeout: HTTP_TIMEOUT, + }, + } + + pwnboard_client := &pwnboard.Client{ + ApplicationName: PWNBOARD_APP_NAME, + URL: fmt.Sprintf("%s/pwn/boxaccess", PWNBOARD_URL), + HTTP: &http.Client{ + Timeout: HTTP_TIMEOUT, + }, + } + + for { + + log.Printf("Querying Tavern for any Hosts seen in the last %s...", LOOKBACK_WINDOW) + hosts, err := tavern_client.GetHostsSeenInLastDuration(LOOKBACK_WINDOW) + if err != nil { + log.Fatalf("failed to query hosts: %v", err) + } + + log.Printf("Found %d host(s)!", len(hosts)) + var ips []string + for _, host := range hosts { + ips = append(ips, host.PrimaryIP) + } + + log.Printf("Sending %d IP(s) to PWNboard...", len(ips)) + err = pwnboard_client.ReportIPs(ips) + if err != nil { + log.Fatalf("failed to send ips to PWNboard: %v", err) + } + + log.Printf("Sleeping for %s...", SLEEP_INTERVAL) + time.Sleep(SLEEP_INTERVAL) + + } + +} diff --git a/bin/pwnboard-go/pwnboard/client.go b/bin/pwnboard-go/pwnboard/client.go new file mode 100644 index 000000000..3c0813b7a --- /dev/null +++ b/bin/pwnboard-go/pwnboard/client.go @@ -0,0 +1,72 @@ +package pwnboard + +import ( + "bytes" + "fmt" + "io" + + "encoding/json" + "net/http" +) + +type Request struct { + Data map[string]any +} + +type Client struct { + ApplicationName string + URL string + HTTP *http.Client +} + +func (c *Client) ReportIPs(ips []string) error { + if len(ips) == 0 { + return nil + } + var r Request + if len(ips) == 1 { + r = Request{ + Data: map[string]any{ + "ip": ips[0], + "application": c.ApplicationName, + }, + } + } else { + r = Request{ + Data: map[string]any{ + "ip": ips[0], + "application": c.ApplicationName, + "ips": ips[1:], + }, + } + } + return c.do(r) +} + +func (c *Client) do(r Request) error { + data, err := json.Marshal(r.Data) + if err != nil { + return fmt.Errorf("failed to marshal json request to json: %w", err) + } + + req, err := http.NewRequest(http.MethodPost, c.URL, bytes.NewBuffer(data)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.HTTP.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %v", err) + } + + // if pwnboard issue + if resp.StatusCode != 202 { + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + return fmt.Errorf("pwnboard error: %s", body) + } + return nil +} diff --git a/bin/pwnboard-go/tavern/client.go b/bin/pwnboard-go/tavern/client.go new file mode 100644 index 000000000..34fed7125 --- /dev/null +++ b/bin/pwnboard-go/tavern/client.go @@ -0,0 +1,115 @@ +package tavern + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "realm.pub/tavern/cli/auth" +) + +// Request represents an outgoing GraphQL request +type Request struct { + Query string `json:"query"` + Variables map[string]any `json:"variables,omitempty"` + OperationName string `json:"operationName,omitempty"` + Extensions map[string]any `json:"extensions,omitempty"` +} + +// Response is a GraphQL layer response from a handler. +type Response struct { + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + Extensions map[string]any +} + +func (r Response) Error() string { + msg := "" + for _, err := range r.Errors { + msg = fmt.Sprintf("%s\n%s;", msg, err.Message) + } + return msg +} + +type Host struct { + ID string `json:"id"` + Name string `json:"name"` + PrimaryIP string `json:"primaryIP"` +} + +type Client struct { + Credential auth.Token + URL string + HTTP *http.Client +} + +func (c *Client) GetHostsSeenInLastDuration(timeAgo time.Duration) ([]Host, error) { + now := time.Now().UTC() + timeAgoFromNow := now.Add(-timeAgo) + formattedTime := timeAgoFromNow.Format(time.RFC3339) + req := Request{ + OperationName: "getHosts", + Query: `query getHosts($input: HostWhereInput) { + hosts(where: $input) { + id + primaryIP + name + } + }`, + Variables: map[string]any{ + "input": map[string]string{"lastSeenAtGT": formattedTime}, + }, + } + + type GetHostsResponse struct { + Response + Data struct { + Hosts []Host `json:"hosts"` + } `json:"data"` + } + var resp GetHostsResponse + if err := c.do(req, &resp); err != nil { + return nil, fmt.Errorf("http request failed: %w", err) + } + + if resp.Errors != nil { + return nil, fmt.Errorf("graphql error: %s", resp.Error()) + } + + return resp.Data.Hosts, nil +} + +// do sends a GraphQL request and returns the response +func (c *Client) do(gqlReq Request, gqlResp any) error { + + data, err := json.Marshal(gqlReq) + if err != nil { + return fmt.Errorf("failed to marshal json request to json: %w", err) + } + + req, err := http.NewRequest(http.MethodPost, c.URL, bytes.NewBuffer(data)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + c.Credential.Authenticate(req) + + resp, err := c.HTTP.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %v", err) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if err := json.Unmarshal(body, gqlResp); err != nil { + return fmt.Errorf("failed to unmarshal body to json: %v", err) + } + + return nil +}