Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions bin/pwnboard-go/auth.go
Original file line number Diff line number Diff line change
@@ -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
}
76 changes: 76 additions & 0 deletions bin/pwnboard-go/main.go
Original file line number Diff line number Diff line change
@@ -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)

}

}
72 changes: 72 additions & 0 deletions bin/pwnboard-go/pwnboard/client.go
Original file line number Diff line number Diff line change
@@ -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
}
115 changes: 115 additions & 0 deletions bin/pwnboard-go/tavern/client.go
Original file line number Diff line number Diff line change
@@ -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
}