[SHEHZADE//ABDULLAH ANSARI]
Red Team Consultant @ Google | MS in Cybersecurity @ NYU
● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0
Multi-protocol network egress assessment and DLP verification framework featuring automated L4 sweeps, corporate proxy traversal, and covert protocol smuggling across 20+ channels.
$
git clone https://shehzade.io/repos/contraband.git
Copy
Source ZIP (1.93 MB)
438 lines · 11.9 KB
| 1 | package exfil |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/md5" |
| 6 | "crypto/sha256" |
| 7 | "crypto/tls" |
| 8 | "crypto/x509" |
| 9 | "encoding/hex" |
| 10 | "errors" |
| 11 | "fmt" |
| 12 | "net" |
| 13 | "net/http" |
| 14 | "sort" |
| 15 | "strings" |
| 16 | "time" |
| 17 | |
| 18 | utls "github.com/refraction-networking/utls" |
| 19 | "golang.org/x/net/http2" |
| 20 | "google.golang.org/grpc/credentials" |
| 21 | ) |
| 22 | |
| 23 | // TLSConfig configures uTLS client masquerading. |
| 24 | type TLSConfig struct { |
| 25 | ServerName string |
| 26 | InsecureSkipVerify bool |
| 27 | Fingerprint string // chrome, edge, safari, firefox, curl, default |
| 28 | ALPN []string |
| 29 | RootCAs *x509.CertPool |
| 30 | MinVersion uint16 |
| 31 | MaxVersion uint16 |
| 32 | } |
| 33 | |
| 34 | // MasqueradeConn wraps a uTLS connection and exposes computed JA3/JA4 fingerprints. |
| 35 | type MasqueradeConn struct { |
| 36 | net.Conn |
| 37 | uconn *utls.UConn |
| 38 | JA3Raw string |
| 39 | JA3Hash string |
| 40 | JA4 string |
| 41 | Profile string |
| 42 | } |
| 43 | |
| 44 | // ConnectionState maps utls.ConnectionState to crypto/tls.ConnectionState for standard library callers. |
| 45 | func (m *MasqueradeConn) ConnectionState() tls.ConnectionState { |
| 46 | if m.uconn == nil { |
| 47 | return tls.ConnectionState{} |
| 48 | } |
| 49 | return convertUTLSToCryptoTLS(m.uconn.ConnectionState()) |
| 50 | } |
| 51 | |
| 52 | // NegotiatedProtocol returns the ALPN string negotiated during handshake. |
| 53 | func (m *MasqueradeConn) NegotiatedProtocol() string { |
| 54 | if m.uconn == nil { |
| 55 | return "" |
| 56 | } |
| 57 | return m.uconn.ConnectionState().NegotiatedProtocol |
| 58 | } |
| 59 | |
| 60 | // GetJA3 returns raw JA3 and MD5 hash. |
| 61 | func (m *MasqueradeConn) GetJA3() (string, string) { |
| 62 | return m.JA3Raw, m.JA3Hash |
| 63 | } |
| 64 | |
| 65 | // GetJA4 returns the computed JA4 fingerprint. |
| 66 | func (m *MasqueradeConn) GetJA4() string { |
| 67 | return m.JA4 |
| 68 | } |
| 69 | |
| 70 | // GetUConn returns underlying utls.UConn. |
| 71 | func (m *MasqueradeConn) GetUConn() *utls.UConn { |
| 72 | return m.uconn |
| 73 | } |
| 74 | |
| 75 | // ResolveClientHelloID maps string presets to utls.ClientHelloID. |
| 76 | func ResolveClientHelloID(profile string) (utls.ClientHelloID, bool) { |
| 77 | switch strings.ToLower(profile) { |
| 78 | case "chrome": |
| 79 | return utls.HelloChrome_120, true |
| 80 | case "edge": |
| 81 | return utls.HelloEdge_Auto, true |
| 82 | case "safari": |
| 83 | return utls.HelloSafari_Auto, true |
| 84 | case "firefox": |
| 85 | return utls.HelloFirefox_Auto, true |
| 86 | case "curl": |
| 87 | return utls.HelloChrome_58, true |
| 88 | case "default": |
| 89 | return utls.HelloGolang, true |
| 90 | default: |
| 91 | return utls.HelloChrome_120, false |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | // isGREASE checks if a 16-bit value is a TLS GREASE identifier. |
| 96 | func isGREASE(val uint16) bool { |
| 97 | return (val&0x0f0f) == 0x0a0a && (val>>8) == (val&0xff) |
| 98 | } |
| 99 | |
| 100 | func extractExtensionID(ext utls.TLSExtension) (id uint16, ok bool, sigAlgs []uint16) { |
| 101 | switch e := ext.(type) { |
| 102 | case *utls.SNIExtension: |
| 103 | return 0x0000, true, nil |
| 104 | case *utls.StatusRequestExtension: |
| 105 | return 0x0005, true, nil |
| 106 | case *utls.SupportedCurvesExtension: |
| 107 | return 0x000a, true, nil |
| 108 | case *utls.SupportedPointsExtension: |
| 109 | return 0x000b, true, nil |
| 110 | case *utls.SignatureAlgorithmsExtension: |
| 111 | var sa []uint16 |
| 112 | for _, s := range e.SupportedSignatureAlgorithms { |
| 113 | sa = append(sa, uint16(s)) |
| 114 | } |
| 115 | return 0x000d, true, sa |
| 116 | case *utls.ALPNExtension: |
| 117 | return 0x0010, true, nil |
| 118 | case *utls.SCTExtension: |
| 119 | return 0x0012, true, nil |
| 120 | case *utls.ExtendedMasterSecretExtension: |
| 121 | return 0x0017, true, nil |
| 122 | case *utls.SessionTicketExtension: |
| 123 | return 0x0023, true, nil |
| 124 | case *utls.PSKKeyExchangeModesExtension: |
| 125 | return 0x002d, true, nil |
| 126 | case *utls.KeyShareExtension: |
| 127 | return 0x0033, true, nil |
| 128 | case *utls.SupportedVersionsExtension: |
| 129 | return 0x002b, true, nil |
| 130 | case *utls.RenegotiationInfoExtension: |
| 131 | return 0xff01, true, nil |
| 132 | case *utls.UtlsCompressCertExtension: |
| 133 | return 0x001b, true, nil |
| 134 | case *utls.ApplicationSettingsExtension: |
| 135 | return 0x44cd, true, nil |
| 136 | case *utls.UtlsPaddingExtension: |
| 137 | return 0x0015, true, nil |
| 138 | case *utls.GenericExtension: |
| 139 | return e.Id, true, nil |
| 140 | case *utls.UtlsGREASEExtension: |
| 141 | return 0, false, nil |
| 142 | case *utls.GREASEEncryptedClientHelloExtension: |
| 143 | return 0, false, nil |
| 144 | default: |
| 145 | return 0, false, nil |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | // CalculateJA3 computes raw JA3 and its MD5 hash from a configured uTLS connection. |
| 150 | func CalculateJA3(uconn *utls.UConn) (string, string) { |
| 151 | if uconn == nil || uconn.HandshakeState.Hello == nil { |
| 152 | return "", "" |
| 153 | } |
| 154 | hello := uconn.HandshakeState.Hello |
| 155 | ver := hello.Vers |
| 156 | |
| 157 | var ciphers []string |
| 158 | for _, c := range hello.CipherSuites { |
| 159 | if !isGREASE(c) { |
| 160 | ciphers = append(ciphers, fmt.Sprintf("%d", c)) |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | var exts []string |
| 165 | for _, ext := range uconn.Extensions { |
| 166 | id, ok, _ := extractExtensionID(ext) |
| 167 | if ok && !isGREASE(id) { |
| 168 | exts = append(exts, fmt.Sprintf("%d", id)) |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | var curves []string |
| 173 | for _, cv := range hello.SupportedCurves { |
| 174 | if !isGREASE(uint16(cv)) { |
| 175 | curves = append(curves, fmt.Sprintf("%d", cv)) |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | var points []string |
| 180 | for _, pt := range hello.SupportedPoints { |
| 181 | points = append(points, fmt.Sprintf("%d", pt)) |
| 182 | } |
| 183 | |
| 184 | raw := fmt.Sprintf("%d,%s,%s,%s,%s", |
| 185 | ver, |
| 186 | strings.Join(ciphers, "-"), |
| 187 | strings.Join(exts, "-"), |
| 188 | strings.Join(curves, "-"), |
| 189 | strings.Join(points, "-"), |
| 190 | ) |
| 191 | h := md5.Sum([]byte(raw)) |
| 192 | return raw, hex.EncodeToString(h[:]) |
| 193 | } |
| 194 | |
| 195 | // CalculateJA4 computes the FoxIO standard JA4 string (JA4_a_JA4_b_JA4_c). |
| 196 | func CalculateJA4(uconn *utls.UConn) string { |
| 197 | if uconn == nil || uconn.HandshakeState.Hello == nil { |
| 198 | return "" |
| 199 | } |
| 200 | hello := uconn.HandshakeState.Hello |
| 201 | |
| 202 | proto := "t" // TCP |
| 203 | ver := "12" |
| 204 | for _, v := range hello.SupportedVersions { |
| 205 | if v == 0x0304 { |
| 206 | ver = "13" |
| 207 | break |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | sni := "i" |
| 212 | if hello.ServerName != "" && net.ParseIP(hello.ServerName) == nil { |
| 213 | sni = "d" |
| 214 | } |
| 215 | |
| 216 | var cipherList []string |
| 217 | for _, c := range hello.CipherSuites { |
| 218 | if !isGREASE(c) { |
| 219 | cipherList = append(cipherList, fmt.Sprintf("%04x", c)) |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | var ja4ExtList []string |
| 224 | var allExtList []string |
| 225 | var sigAlgs []string |
| 226 | |
| 227 | for _, ext := range uconn.Extensions { |
| 228 | id, ok, sa := extractExtensionID(ext) |
| 229 | if !ok || isGREASE(id) { |
| 230 | continue |
| 231 | } |
| 232 | allExtList = append(allExtList, fmt.Sprintf("%04x", id)) |
| 233 | if id != 0x0000 && id != 0x0010 { // JA4 excludes SNI (0000) and ALPN (0010) |
| 234 | ja4ExtList = append(ja4ExtList, fmt.Sprintf("%04x", id)) |
| 235 | } |
| 236 | for _, s := range sa { |
| 237 | if !isGREASE(s) { |
| 238 | sigAlgs = append(sigAlgs, fmt.Sprintf("%04x", s)) |
| 239 | } |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | alpn := "00" |
| 244 | if len(hello.AlpnProtocols) > 0 && len(hello.AlpnProtocols[0]) >= 2 { |
| 245 | first := hello.AlpnProtocols[0] |
| 246 | alpn = string(first[0]) + string(first[len(first)-1]) |
| 247 | } |
| 248 | |
| 249 | partA := fmt.Sprintf("%s%s%s%02d%02d%s", proto, ver, sni, len(cipherList), len(allExtList), alpn) |
| 250 | |
| 251 | sort.Strings(cipherList) |
| 252 | bHash := sha256.Sum256([]byte(strings.Join(cipherList, ","))) |
| 253 | partB := hex.EncodeToString(bHash[:])[:12] |
| 254 | |
| 255 | sort.Strings(ja4ExtList) |
| 256 | sort.Strings(sigAlgs) |
| 257 | cRaw := strings.Join(ja4ExtList, ",") |
| 258 | if len(sigAlgs) > 0 { |
| 259 | cRaw += "_" + strings.Join(sigAlgs, ",") |
| 260 | } |
| 261 | cHash := sha256.Sum256([]byte(cRaw)) |
| 262 | partC := hex.EncodeToString(cHash[:])[:12] |
| 263 | |
| 264 | return fmt.Sprintf("%s_%s_%s", partA, partB, partC) |
| 265 | } |
| 266 | |
| 267 | // DialTLS dials a target address over TCP and completes a uTLS masquerade handshake. |
| 268 | func DialTLS(ctx context.Context, network, addr string, cfg TLSConfig) (*MasqueradeConn, error) { |
| 269 | var d net.Dialer |
| 270 | rawConn, err := d.DialContext(ctx, network, addr) |
| 271 | if err != nil { |
| 272 | return nil, fmt.Errorf("tcp dial failed: %w", err) |
| 273 | } |
| 274 | return WrapConn(ctx, rawConn, addr, cfg) |
| 275 | } |
| 276 | |
| 277 | // WrapConn wraps an established net.Conn with uTLS masquerader. |
| 278 | func WrapConn(ctx context.Context, rawConn net.Conn, addr string, cfg TLSConfig) (*MasqueradeConn, error) { |
| 279 | helloID, _ := ResolveClientHelloID(cfg.Fingerprint) |
| 280 | |
| 281 | serverName := cfg.ServerName |
| 282 | if serverName == "" { |
| 283 | host, _, err := net.SplitHostPort(addr) |
| 284 | if err == nil { |
| 285 | serverName = host |
| 286 | } else { |
| 287 | serverName = addr |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | utlsCfg := &utls.Config{ |
| 292 | ServerName: serverName, |
| 293 | InsecureSkipVerify: cfg.InsecureSkipVerify, |
| 294 | RootCAs: cfg.RootCAs, |
| 295 | MinVersion: cfg.MinVersion, |
| 296 | MaxVersion: cfg.MaxVersion, |
| 297 | NextProtos: cfg.ALPN, |
| 298 | } |
| 299 | |
| 300 | uconn := utls.UClient(rawConn, utlsCfg, helloID) |
| 301 | _ = uconn.BuildHandshakeState() |
| 302 | |
| 303 | if len(cfg.ALPN) > 0 { |
| 304 | found := false |
| 305 | for _, ext := range uconn.Extensions { |
| 306 | if alpnExt, ok := ext.(*utls.ALPNExtension); ok { |
| 307 | alpnExt.AlpnProtocols = cfg.ALPN |
| 308 | found = true |
| 309 | break |
| 310 | } |
| 311 | } |
| 312 | if !found { |
| 313 | uconn.Extensions = append(uconn.Extensions, &utls.ALPNExtension{AlpnProtocols: cfg.ALPN}) |
| 314 | } |
| 315 | _ = uconn.BuildHandshakeState() |
| 316 | } |
| 317 | |
| 318 | ja3Raw, ja3Hash := CalculateJA3(uconn) |
| 319 | ja4 := CalculateJA4(uconn) |
| 320 | |
| 321 | if err := uconn.HandshakeContext(ctx); err != nil { |
| 322 | rawConn.Close() |
| 323 | return nil, fmt.Errorf("utls handshake failed: %w", err) |
| 324 | } |
| 325 | |
| 326 | return &MasqueradeConn{ |
| 327 | Conn: uconn, |
| 328 | uconn: uconn, |
| 329 | JA3Raw: ja3Raw, |
| 330 | JA3Hash: ja3Hash, |
| 331 | JA4: ja4, |
| 332 | Profile: cfg.Fingerprint, |
| 333 | }, nil |
| 334 | } |
| 335 | |
| 336 | // NewHTTPTransport creates an *http.Transport configured with uTLS DialTLSContext. |
| 337 | func NewHTTPTransport(cfg TLSConfig) *http.Transport { |
| 338 | c1 := cfg |
| 339 | if len(c1.ALPN) == 0 { |
| 340 | c1.ALPN = []string{"http/1.1"} |
| 341 | } |
| 342 | return &http.Transport{ |
| 343 | DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { |
| 344 | return DialTLS(ctx, network, addr, c1) |
| 345 | }, |
| 346 | ForceAttemptHTTP2: false, |
| 347 | MaxIdleConns: 100, |
| 348 | IdleConnTimeout: 90 * time.Second, |
| 349 | TLSHandshakeTimeout: 10 * time.Second, |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | // NewHTTP2Transport creates an *http2.Transport configured with uTLS DialTLSContext. |
| 354 | func NewHTTP2Transport(cfg TLSConfig) *http2.Transport { |
| 355 | c2 := cfg |
| 356 | c2.ALPN = []string{"h2"} |
| 357 | return &http2.Transport{ |
| 358 | DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { |
| 359 | return DialTLS(ctx, network, addr, c2) |
| 360 | }, |
| 361 | AllowHTTP: false, |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | // convertUTLSToCryptoTLS converts utls.ConnectionState into standard crypto/tls.ConnectionState. |
| 366 | func convertUTLSToCryptoTLS(s utls.ConnectionState) tls.ConnectionState { |
| 367 | return tls.ConnectionState{ |
| 368 | Version: s.Version, |
| 369 | HandshakeComplete: s.HandshakeComplete, |
| 370 | DidResume: s.DidResume, |
| 371 | CipherSuite: s.CipherSuite, |
| 372 | NegotiatedProtocol: s.NegotiatedProtocol, |
| 373 | NegotiatedProtocolIsMutual: s.NegotiatedProtocolIsMutual, |
| 374 | ServerName: s.ServerName, |
| 375 | PeerCertificates: s.PeerCertificates, |
| 376 | VerifiedChains: s.VerifiedChains, |
| 377 | SignedCertificateTimestamps: s.SignedCertificateTimestamps, |
| 378 | OCSPResponse: s.OCSPResponse, |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | // utlsGRPCWrap implements credentials.TransportCredentials for gRPC client dialing over uTLS. |
| 383 | type utlsGRPCWrap struct { |
| 384 | helloID utls.ClientHelloID |
| 385 | config *utls.Config |
| 386 | } |
| 387 | |
| 388 | // NewGRPCTransportCredentials creates gRPC transport credentials using uTLS masquerading. |
| 389 | func NewGRPCTransportCredentials(cfg TLSConfig) credentials.TransportCredentials { |
| 390 | helloID, _ := ResolveClientHelloID(cfg.Fingerprint) |
| 391 | utlsCfg := &utls.Config{ |
| 392 | ServerName: cfg.ServerName, |
| 393 | InsecureSkipVerify: cfg.InsecureSkipVerify, |
| 394 | RootCAs: cfg.RootCAs, |
| 395 | MinVersion: cfg.MinVersion, |
| 396 | MaxVersion: cfg.MaxVersion, |
| 397 | NextProtos: []string{"h2"}, |
| 398 | } |
| 399 | return &utlsGRPCWrap{ |
| 400 | helloID: helloID, |
| 401 | config: utlsCfg, |
| 402 | } |
| 403 | } |
| 404 | |
| 405 | func (c *utlsGRPCWrap) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { |
| 406 | cfg := c.config.Clone() |
| 407 | if cfg.ServerName == "" { |
| 408 | host, _, err := net.SplitHostPort(authority) |
| 409 | if err == nil { |
| 410 | cfg.ServerName = host |
| 411 | } else { |
| 412 | cfg.ServerName = authority |
| 413 | } |
| 414 | } |
| 415 | uconn := utls.UClient(rawConn, cfg, c.helloID) |
| 416 | if err := uconn.HandshakeContext(ctx); err != nil { |
| 417 | rawConn.Close() |
| 418 | return nil, nil, fmt.Errorf("utls grpc handshake failed: %w", err) |
| 419 | } |
| 420 | return uconn, credentials.TLSInfo{State: convertUTLSToCryptoTLS(uconn.ConnectionState())}, nil |
| 421 | } |
| 422 | |
| 423 | func (c *utlsGRPCWrap) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { |
| 424 | return nil, nil, errors.New("utlsGRPCWrap is client-only") |
| 425 | } |
| 426 | |
| 427 | func (c *utlsGRPCWrap) Info() credentials.ProtocolInfo { |
| 428 | return credentials.ProtocolInfo{SecurityProtocol: "tls", ServerName: c.config.ServerName} |
| 429 | } |
| 430 | |
| 431 | func (c *utlsGRPCWrap) Clone() credentials.TransportCredentials { |
| 432 | return &utlsGRPCWrap{helloID: c.helloID, config: c.config.Clone()} |
| 433 | } |
| 434 | |
| 435 | func (c *utlsGRPCWrap) OverrideServerName(serverNameOverride string) error { |
| 436 | c.config.ServerName = serverNameOverride |
| 437 | return nil |
| 438 | } |