Files
hadrian/proxychain/requestmodifers/spoof_ja3_fingerprint.go
2023-11-27 16:30:13 -06:00

452 lines
14 KiB
Go

package requestmodifers
import (
"ladder/proxychain"
"strconv"
"crypto/sha256"
// "crypto/tls"
"context"
"errors"
"fmt"
"net"
"strings"
"sync"
http "github.com/Danny-Dasilva/fhttp"
"github.com/Danny-Dasilva/fhttp/http2"
utls "github.com/Danny-Dasilva/utls"
"golang.org/x/net/proxy"
)
// SpoofJA3fingerprint modifies the TLS client and user agent to spoof a particular JA3 fingerprint
// Some anti-bot WAFs such as cloudflare can fingerprint the fields of the TLS hello packet, and the order in which they appear
// https://web.archive.org/web/20231126224326/https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/
// https://web.archive.org/web/20231119065253/https://developers.cloudflare.com/bots/concepts/ja3-fingerprint/
func SpoofJA3fingerprint(ja3 string, userAgent string) proxychain.RequestModification {
//fmt.Println(ja3)
return func(chain *proxychain.ProxyChain) error {
spoofConfig := browser{
UserAgent: userAgent,
JA3: ja3,
InsecureSkipVerify: false,
}
// deep copy existing client while modifying http transport
ja3SpoofClient := &http.Client{
//Transport: newJA3SpooferTransport(spoofConfig, proxy),
Transport: newJA3SpooferTransport(spoofConfig),
Timeout: chain.Client.Timeout,
CheckRedirect: chain.Client.CheckRedirect,
}
chain.SetOnceHTTPClient(ja3SpoofClient)
return nil
}
}
// SpoofJA3fingerprintWithProxy modifies the TLS client and user agent to spoof a particular JA3 fingerprint and use a proxy.ContextDialer from the "golang.org/x/net/proxy"
// Some anti-bot WAFs such as cloudflare can fingerprint the fields of the TLS hello packet, and the order in which they appear
// https://web.archive.org/web/20231126224326/https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/
// https://web.archive.org/web/20231119065253/https://developers.cloudflare.com/bots/concepts/ja3-fingerprint/
func SpoofJA3fingerprintWithProxy(ja3 string, userAgent string, proxy proxy.ContextDialer) proxychain.RequestModification {
return func(chain *proxychain.ProxyChain) error {
spoofConfig := browser{
UserAgent: userAgent,
JA3: ja3,
InsecureSkipVerify: false,
}
// deep copy existing client while modifying http transport
ja3SpoofClient := &http.Client{
//Transport: newJA3SpooferTransport(spoofConfig, proxy),
Transport: newJA3SpooferTransport(spoofConfig, proxy),
Timeout: chain.Client.Timeout,
CheckRedirect: chain.Client.CheckRedirect,
}
chain.SetOnceHTTPClient(ja3SpoofClient)
return nil
}
}
// ============================================================================================== //
// The following code is adapated from https://github.com/Danny-Dasilva/CycleTLS with GPL3 license
// ============================================================================================== //
var errProtocolNegotiated = errors.New("protocol negotiated")
type roundTripper struct {
sync.Mutex
// fix typing
JA3 string
UserAgent string
InsecureSkipVerify bool
cachedConnections map[string]net.Conn
cachedTransports map[string]http.RoundTripper
dialer proxy.ContextDialer
}
func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", rt.UserAgent)
addr := rt.getDialTLSAddr(req)
if _, ok := rt.cachedTransports[addr]; !ok {
if err := rt.getTransport(req, addr); err != nil {
return nil, err
}
}
return rt.cachedTransports[addr].RoundTrip(req)
}
func (rt *roundTripper) getTransport(req *http.Request, addr string) error {
switch strings.ToLower(req.URL.Scheme) {
case "http":
rt.cachedTransports[addr] = &http.Transport{DialContext: rt.dialer.DialContext, DisableKeepAlives: true}
return nil
case "https":
default:
return fmt.Errorf("invalid URL scheme: [%v]", req.URL.Scheme)
}
_, err := rt.dialTLS(req.Context(), "tcp", addr)
switch err {
case errProtocolNegotiated:
case nil:
// Should never happen.
panic("dialTLS returned no error when determining cachedTransports")
default:
return err
}
return nil
}
func (rt *roundTripper) dialTLS(ctx context.Context, network, addr string) (net.Conn, error) {
rt.Lock()
defer rt.Unlock()
// If we have the connection from when we determined the HTTPS
// cachedTransports to use, return that.
if conn := rt.cachedConnections[addr]; conn != nil {
return conn, nil
}
rawConn, err := rt.dialer.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
var host string
if host, _, err = net.SplitHostPort(addr); err != nil {
host = addr
}
//////////////////
spec, err := StringToSpec(rt.JA3, rt.UserAgent)
if err != nil {
return nil, err
}
conn := utls.UClient(rawConn, &utls.Config{ServerName: host, InsecureSkipVerify: rt.InsecureSkipVerify}, // MinVersion: tls.VersionTLS10,
// MaxVersion: tls.VersionTLS13,
utls.HelloCustom)
if err := conn.ApplyPreset(spec); err != nil {
return nil, err
}
if err = conn.Handshake(); err != nil {
_ = conn.Close()
if err.Error() == "tls: CurvePreferences includes unsupported curve" {
//fix this
return nil, fmt.Errorf("conn.Handshake() error for tls 1.3 (please retry request): %+v", err)
}
return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err)
}
//////////
if rt.cachedTransports[addr] != nil {
return conn, nil
}
// No http.Transport constructed yet, create one based on the results
// of ALPN.
switch conn.ConnectionState().NegotiatedProtocol {
case http2.NextProtoTLS:
parsedUserAgent := parseUserAgent(rt.UserAgent).UserAgent
t2 := http2.Transport{DialTLS: rt.dialTLSHTTP2,
PushHandler: &http2.DefaultPushHandler{},
Navigator: parsedUserAgent,
}
rt.cachedTransports[addr] = &t2
default:
// Assume the remote peer is speaking HTTP 1.x + TLS.
rt.cachedTransports[addr] = &http.Transport{DialTLSContext: rt.dialTLS}
}
// Stash the connection just established for use servicing the
// actual request (should be near-immediate).
rt.cachedConnections[addr] = conn
return nil, errProtocolNegotiated
}
func (rt *roundTripper) dialTLSHTTP2(network, addr string, _ *utls.Config) (net.Conn, error) {
return rt.dialTLS(context.Background(), network, addr)
}
func (rt *roundTripper) getDialTLSAddr(req *http.Request) string {
host, port, err := net.SplitHostPort(req.URL.Host)
if err == nil {
return net.JoinHostPort(host, port)
}
return net.JoinHostPort(req.URL.Host, "443") // we can assume port is 443 at this point
}
func (rt *roundTripper) CloseIdleConnections() {
for addr, conn := range rt.cachedConnections {
_ = conn.Close()
delete(rt.cachedConnections, addr)
}
}
func newJA3SpooferTransport(browser browser, dialer ...proxy.ContextDialer) http.RoundTripper {
if dialer != nil && len(dialer) > 0 {
return &roundTripper{
dialer: dialer[0],
JA3: browser.JA3,
UserAgent: browser.UserAgent,
cachedTransports: make(map[string]http.RoundTripper),
cachedConnections: make(map[string]net.Conn),
InsecureSkipVerify: browser.InsecureSkipVerify,
}
}
return &roundTripper{
dialer: proxy.Direct,
JA3: browser.JA3,
UserAgent: browser.UserAgent,
cachedTransports: make(map[string]http.RoundTripper),
cachedConnections: make(map[string]net.Conn),
InsecureSkipVerify: browser.InsecureSkipVerify,
}
}
type browser struct {
// Return a greeting that embeds the name in a message.
JA3 string
UserAgent string
InsecureSkipVerify bool
}
// adapted from
// https://github.com/Danny-Dasilva/CycleTLS/blob/963daacd92414dc84a0ec6930dd252381cb1247a/cycletls/utils.go#L27
const (
chrome = "chrome" //chrome User agent enum
firefox = "firefox" //firefox User agent enum
)
type UserAgent struct {
UserAgent string
HeaderOrder []string
}
// ParseUserAgent returns the pseudo header order and user agent string for chrome/firefox
func parseUserAgent(userAgent string) UserAgent {
switch {
case strings.Contains(strings.ToLower(userAgent), "chrome"):
return UserAgent{chrome, []string{":method", ":authority", ":scheme", ":path"}}
case strings.Contains(strings.ToLower(userAgent), "firefox"):
return UserAgent{firefox, []string{":method", ":path", ":authority", ":scheme"}}
default:
return UserAgent{chrome, []string{":method", ":authority", ":scheme", ":path"}}
}
}
// ==============
// StringToSpec creates a ClientHelloSpec based on a JA3 string
func StringToSpec(ja3 string, userAgent string) (*utls.ClientHelloSpec, error) {
parsedUserAgent := parseUserAgent(userAgent).UserAgent
extMap := genMap()
tokens := strings.Split(ja3, ",")
version := tokens[0]
ciphers := strings.Split(tokens[1], "-")
extensions := strings.Split(tokens[2], "-")
curves := strings.Split(tokens[3], "-")
if len(curves) == 1 && curves[0] == "" {
curves = []string{}
}
pointFormats := strings.Split(tokens[4], "-")
if len(pointFormats) == 1 && pointFormats[0] == "" {
pointFormats = []string{}
}
// parse curves
var targetCurves []utls.CurveID
targetCurves = append(targetCurves, utls.CurveID(utls.GREASE_PLACEHOLDER)) //append grease for Chrome browsers
for _, c := range curves {
cid, err := strconv.ParseUint(c, 10, 16)
if err != nil {
return nil, err
}
targetCurves = append(targetCurves, utls.CurveID(cid))
// if cid != uint64(utls.CurveP521) {
// CurveP521 sometimes causes handshake errors
// }
}
extMap["10"] = &utls.SupportedCurvesExtension{Curves: targetCurves}
// parse point formats
var targetPointFormats []byte
for _, p := range pointFormats {
pid, err := strconv.ParseUint(p, 10, 8)
if err != nil {
return nil, err
}
targetPointFormats = append(targetPointFormats, byte(pid))
}
extMap["11"] = &utls.SupportedPointsExtension{SupportedPoints: targetPointFormats}
// set extension 43
vid64, err := strconv.ParseUint(version, 10, 16)
if err != nil {
return nil, err
}
vid := uint16(vid64)
// extMap["43"] = &utls.SupportedVersionsExtension{
// Versions: []uint16{
// utls.VersionTLS12,
// },
// }
// build extenions list
var exts []utls.TLSExtension
//Optionally Add Chrome Grease Extension
if parsedUserAgent == chrome {
exts = append(exts, &utls.UtlsGREASEExtension{})
}
for _, e := range extensions {
te, ok := extMap[e]
if !ok {
return nil, errors.New("string2spec extension error")
}
// //Optionally add Chrome Grease Extension
if e == "21" && parsedUserAgent == chrome {
exts = append(exts, &utls.UtlsGREASEExtension{})
}
exts = append(exts, te)
}
//Add this back in if user agent is chrome and no padding extension is given
// if parsedUserAgent == chrome {
// exts = append(exts, &utls.UtlsGREASEExtension{})
// exts = append(exts, &utls.UtlsPaddingExtension{GetPaddingLen: utls.BoringPaddingStyle})
// }
// build SSLVersion
// vid64, err := strconv.ParseUint(version, 10, 16)
// if err != nil {
// return nil, err
// }
// build CipherSuites
var suites []uint16
//Optionally Add Chrome Grease Extension
if parsedUserAgent == chrome {
suites = append(suites, utls.GREASE_PLACEHOLDER)
}
for _, c := range ciphers {
cid, err := strconv.ParseUint(c, 10, 16)
if err != nil {
return nil, err
}
suites = append(suites, uint16(cid))
}
_ = vid
return &utls.ClientHelloSpec{
// TLSVersMin: vid,
// TLSVersMax: vid,
CipherSuites: suites,
CompressionMethods: []byte{0},
Extensions: exts,
GetSessionID: sha256.Sum256,
}, nil
}
func genMap() (extMap map[string]utls.TLSExtension) {
extMap = map[string]utls.TLSExtension{
"0": &utls.SNIExtension{},
"5": &utls.StatusRequestExtension{},
// These are applied later
// "10": &tls.SupportedCurvesExtension{...}
// "11": &tls.SupportedPointsExtension{...}
"13": &utls.SignatureAlgorithmsExtension{
SupportedSignatureAlgorithms: []utls.SignatureScheme{
utls.ECDSAWithP256AndSHA256,
utls.ECDSAWithP384AndSHA384,
utls.ECDSAWithP521AndSHA512,
utls.PSSWithSHA256,
utls.PSSWithSHA384,
utls.PSSWithSHA512,
utls.PKCS1WithSHA256,
utls.PKCS1WithSHA384,
utls.PKCS1WithSHA512,
utls.ECDSAWithSHA1,
utls.PKCS1WithSHA1,
},
},
"16": &utls.ALPNExtension{
AlpnProtocols: []string{"h2", "http/1.1"},
},
"17": &utls.GenericExtension{Id: 17}, // status_request_v2
"18": &utls.SCTExtension{},
"21": &utls.UtlsPaddingExtension{GetPaddingLen: utls.BoringPaddingStyle},
"22": &utls.GenericExtension{Id: 22}, // encrypt_then_mac
"23": &utls.UtlsExtendedMasterSecretExtension{},
"27": &utls.CompressCertificateExtension{
Algorithms: []utls.CertCompressionAlgo{utls.CertCompressionBrotli},
},
"28": &utls.FakeRecordSizeLimitExtension{}, //Limit: 0x4001
"35": &utls.SessionTicketExtension{},
"34": &utls.GenericExtension{Id: 34},
"41": &utls.GenericExtension{Id: 41}, //FIXME pre_shared_key
"43": &utls.SupportedVersionsExtension{Versions: []uint16{
utls.GREASE_PLACEHOLDER,
utls.VersionTLS13,
utls.VersionTLS12,
utls.VersionTLS11,
utls.VersionTLS10}},
"44": &utls.CookieExtension{},
"45": &utls.PSKKeyExchangeModesExtension{Modes: []uint8{
utls.PskModeDHE,
}},
"49": &utls.GenericExtension{Id: 49}, // post_handshake_auth
"50": &utls.GenericExtension{Id: 50}, // signature_algorithms_cert
"51": &utls.KeyShareExtension{KeyShares: []utls.KeyShare{
{Group: utls.CurveID(utls.GREASE_PLACEHOLDER), Data: []byte{0}},
{Group: utls.X25519},
// {Group: utls.CurveP384}, known bug missing correct extensions for handshake
}},
"30032": &utls.GenericExtension{Id: 0x7550, Data: []byte{0}}, //FIXME
"13172": &utls.NPNExtension{},
"17513": &utls.ApplicationSettingsExtension{
SupportedALPNList: []string{
"h2",
},
},
"65281": &utls.RenegotiationInfoExtension{
Renegotiation: utls.RenegotiateOnceAsClient,
},
"65037": &utls.GenericExtension{Id: 65037},
}
return
}