mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-04 14:36:50 +02:00
add request() to remote browser
Signed-off-by: RonniSkansing <rskansing@gmail.com>
This commit is contained in:
4 files changed
+208
No files matched your search
@@ -12,8 +12,10 @@ import (
|
||||
"image/png"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -28,6 +30,7 @@ import (
|
||||
"github.com/phishingclub/phishingclub/cache"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/database"
|
||||
"github.com/phishingclub/phishingclub/ipdata"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/remotebrowser"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
@@ -548,6 +551,42 @@ func (m *RemoteBrowserController) RunByID(g *gin.Context) {
|
||||
// The handler bridges victim WebSocket messages into the runner's Incoming channel and
|
||||
// forwards runner events back to the victim. When the runner emits a "capture" event
|
||||
// the cookies are saved as a CampaignEvent so they appear alongside AITM captures.
|
||||
// buildRequestInfo captures the victim connection for the script's request()
|
||||
// binding: the trusted proxy aware IP, its country and ASNs from the ipdata
|
||||
// store, the JA4 fingerprint, and the request headers.
|
||||
func (m *RemoteBrowserController) buildRequestInfo(g *gin.Context) *remotebrowser.RequestInfo {
|
||||
ip := utils.ExtractClientIP(g.Request, m.TrustedProxies)
|
||||
if host, _, err := net.SplitHostPort(ip); err == nil {
|
||||
ip = host
|
||||
}
|
||||
// read the JA4 fingerprint the same way middleware.GetJA4FromContext does,
|
||||
// by the literal context key and header. The constants are not imported
|
||||
// because the middleware package imports controller (import cycle).
|
||||
ja4 := g.GetString("ja4_fingerprint")
|
||||
if ja4 == "" {
|
||||
ja4 = g.Request.Header.Get("X-JA4")
|
||||
}
|
||||
info := &remotebrowser.RequestInfo{
|
||||
IP: ip,
|
||||
JA4: ja4,
|
||||
UserAgent: g.Request.UserAgent(),
|
||||
AcceptLanguage: g.GetHeader("Accept-Language"),
|
||||
Headers: map[string]string{},
|
||||
}
|
||||
for k := range g.Request.Header {
|
||||
info.Headers[strings.ToLower(k)] = g.Request.Header.Get(k)
|
||||
}
|
||||
if store := ipdata.Get(); store != nil {
|
||||
if country, ok := store.LookupCountry(ip); ok {
|
||||
info.Country = country
|
||||
}
|
||||
for _, a := range store.LookupASNDetails(ip) {
|
||||
info.ASNs = append(info.ASNs, remotebrowser.RequestASN{Number: a.ASN, Name: a.Name})
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func (m *RemoteBrowserController) ServeVictim(g *gin.Context) {
|
||||
if !m.isEnabled(g) {
|
||||
return
|
||||
@@ -612,6 +651,9 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) {
|
||||
runner := remotebrowser.NewRunner(scriptVal.String(), cfg)
|
||||
runner.ExecPath = m.ExecPath
|
||||
runner.Logger = m.Logger
|
||||
// describe the victim connection so the script can read it via request()
|
||||
// before newSession(), e.g. to pick a proxy by country
|
||||
runner.Request = m.buildRequestInfo(g)
|
||||
|
||||
campaignID, err1 := cr.CampaignID.Get()
|
||||
recipientID, err2 := cr.RecipientID.Get()
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package remotebrowser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
// TestRequestToMapNil proves a nil Request still yields a fully shaped object so
|
||||
// a script reading request().country never hits undefined.
|
||||
func TestRequestToMapNil(t *testing.T) {
|
||||
m := requestToMap(nil)
|
||||
for _, k := range []string{"ip", "country", "asns", "ja4", "userAgent", "acceptLanguage", "headers"} {
|
||||
if _, ok := m[k]; !ok {
|
||||
t.Fatalf("nil request map missing key %q", k)
|
||||
}
|
||||
}
|
||||
if m["country"] != "" {
|
||||
t.Fatalf("expected empty country, got %v", m["country"])
|
||||
}
|
||||
if got := m["asns"].([]interface{}); len(got) != 0 {
|
||||
t.Fatalf("expected empty asns, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequestToMapValues proves the fields map to the lowercase JS keys.
|
||||
func TestRequestToMapValues(t *testing.T) {
|
||||
ri := &RequestInfo{
|
||||
IP: "1.2.3.4",
|
||||
Country: "DE",
|
||||
ASNs: []RequestASN{{Number: 16509, Name: "AMAZON-02"}},
|
||||
JA4: "t13d1516h2_x",
|
||||
Headers: map[string]string{"user-agent": "UA"},
|
||||
}
|
||||
m := requestToMap(ri)
|
||||
if m["ip"] != "1.2.3.4" || m["country"] != "DE" || m["ja4"] != "t13d1516h2_x" {
|
||||
t.Fatalf("scalar fields wrong: %v", m)
|
||||
}
|
||||
asn := m["asns"].([]interface{})[0].(map[string]interface{})
|
||||
if asn["number"] != uint32(16509) || asn["name"] != "AMAZON-02" {
|
||||
t.Fatalf("asn mapping wrong: %v", asn)
|
||||
}
|
||||
if m["headers"].(map[string]interface{})["user-agent"] != "UA" {
|
||||
t.Fatalf("headers mapping wrong: %v", m["headers"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequestBindingGoja proves the request() binding round-trips into JS the
|
||||
// way the runner wires it, so request().country and request().asns[0].name are
|
||||
// readable from a script.
|
||||
func TestRequestBindingGoja(t *testing.T) {
|
||||
r := &Runner{Request: &RequestInfo{
|
||||
IP: "9.9.9.9",
|
||||
Country: "DK",
|
||||
ASNs: []RequestASN{{Number: 15169, Name: "GOOGLE"}},
|
||||
Headers: map[string]string{"accept-language": "da-DK"},
|
||||
}}
|
||||
vm := goja.New()
|
||||
vm.Set("request", func(call goja.FunctionCall) goja.Value {
|
||||
return vm.ToValue(requestToMap(r.Request))
|
||||
})
|
||||
v, err := vm.RunString(`(function(){
|
||||
var r = request();
|
||||
return r.country + "|" + r.asns[0].name + "|" + r.headers["accept-language"];
|
||||
})()`)
|
||||
if err != nil {
|
||||
t.Fatalf("script error: %v", err)
|
||||
}
|
||||
if got := v.String(); got != "DK|GOOGLE|da-DK" {
|
||||
t.Fatalf("unexpected: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -393,6 +393,63 @@ type Runner struct {
|
||||
// keepAliveActive is set by s.keepAlive() so Run() parks after the script
|
||||
// finishes, waiting for the operator to explicitly end the session.
|
||||
keepAliveActive atomic.Bool
|
||||
// Request describes the victim connection that started this session. The
|
||||
// controller populates it before Run so the script can read it via request()
|
||||
// before newSession(), for example to pick a proxy by country. Nil for
|
||||
// operator test runs.
|
||||
Request *RequestInfo
|
||||
}
|
||||
|
||||
// RequestInfo is the victim request context exposed to the script via request().
|
||||
// It is filled by the caller (the controller) from the incoming connection.
|
||||
type RequestInfo struct {
|
||||
IP string `json:"ip"`
|
||||
Country string `json:"country"`
|
||||
ASNs []RequestASN `json:"asns"`
|
||||
JA4 string `json:"ja4"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
AcceptLanguage string `json:"acceptLanguage"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
// RequestASN is one autonomous system the request IP belongs to.
|
||||
type RequestASN struct {
|
||||
Number uint32 `json:"number"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// requestToMap converts the request info into a plain map so goja exposes the
|
||||
// exact lowercase JS keys. A nil Request yields an empty shaped object so a
|
||||
// script reading request().country never hits undefined.
|
||||
func requestToMap(ri *RequestInfo) map[string]interface{} {
|
||||
if ri == nil {
|
||||
return map[string]interface{}{
|
||||
"ip": "",
|
||||
"country": "",
|
||||
"asns": []interface{}{},
|
||||
"ja4": "",
|
||||
"userAgent": "",
|
||||
"acceptLanguage": "",
|
||||
"headers": map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
asns := make([]interface{}, 0, len(ri.ASNs))
|
||||
for _, a := range ri.ASNs {
|
||||
asns = append(asns, map[string]interface{}{"number": a.Number, "name": a.Name})
|
||||
}
|
||||
headers := make(map[string]interface{}, len(ri.Headers))
|
||||
for k, v := range ri.Headers {
|
||||
headers[k] = v
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"ip": ri.IP,
|
||||
"country": ri.Country,
|
||||
"asns": asns,
|
||||
"ja4": ri.JA4,
|
||||
"userAgent": ri.UserAgent,
|
||||
"acceptLanguage": ri.AcceptLanguage,
|
||||
"headers": headers,
|
||||
}
|
||||
}
|
||||
|
||||
// IncomingMsg is an event sent from the client into the running script.
|
||||
@@ -478,6 +535,13 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
panic(vm.NewGoError(scriptStopError{}))
|
||||
})
|
||||
|
||||
// request() returns the victim connection that started this session (IP,
|
||||
// country, ASNs, JA4, headers). Available before newSession() so a script can
|
||||
// gate or pick a proxy by country.
|
||||
vm.Set("request", func(call goja.FunctionCall) goja.Value {
|
||||
return vm.ToValue(requestToMap(r.Request))
|
||||
})
|
||||
|
||||
vm.Set("emit", func(call goja.FunctionCall) goja.Value {
|
||||
key := vmArgStr(call.Argument(0))
|
||||
value := call.Argument(1).Export()
|
||||
|
||||
@@ -735,6 +735,36 @@ interface FrameSession {
|
||||
|
||||
/** Open a new browser session */
|
||||
declare function newSession(options?: SessionOptions): Session;
|
||||
|
||||
interface RequestASN {
|
||||
/** Autonomous system number, e.g. 16509 */
|
||||
number: number;
|
||||
/** Autonomous system name / org, e.g. "AMAZON-02" */
|
||||
name: string;
|
||||
}
|
||||
interface RequestInfo {
|
||||
/** The victim's request IP (trusted proxy aware). */
|
||||
ip: string;
|
||||
/** ISO country code from the GeoIP database, e.g. "DE". Empty if unknown. */
|
||||
country: string;
|
||||
/** Autonomous systems the IP belongs to. Empty unless the ASN package is downloaded. */
|
||||
asns: RequestASN[];
|
||||
/** JA4 TLS fingerprint of the connection. Empty if not captured. */
|
||||
ja4: string;
|
||||
/** The User-Agent header. */
|
||||
userAgent: string;
|
||||
/** The Accept-Language header. */
|
||||
acceptLanguage: string;
|
||||
/** All request headers, keys lowercased. */
|
||||
headers: { [name: string]: string };
|
||||
}
|
||||
/**
|
||||
* The victim connection that started this session. Available before
|
||||
* newSession(), for example to pick a proxy by country:
|
||||
* var r = request();
|
||||
* var s = newSession({ proxy: r.country === 'DE' ? 'de-proxy' : 'us-proxy' });
|
||||
*/
|
||||
declare function request(): RequestInfo;
|
||||
/** Send an event to the victim page (visible to the victim's JS) */
|
||||
declare function emit(key: string, value?: any): void;
|
||||
/** Log a message to the test runner */
|
||||
|
||||
Reference in new issue
Block a user