[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)
309 lines · 7.3 KB
| 1 | package proxy |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/binary" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "io" |
| 9 | "net" |
| 10 | "net/url" |
| 11 | "strconv" |
| 12 | "strings" |
| 13 | "time" |
| 14 | ) |
| 15 | |
| 16 | const ( |
| 17 | socks5Version = 0x05 |
| 18 | |
| 19 | // Auth methods |
| 20 | socks5AuthNone = 0x00 |
| 21 | socks5AuthUserPass = 0x02 |
| 22 | socks5AuthNoAccept = 0xFF |
| 23 | |
| 24 | // Command |
| 25 | socks5CmdConnect = 0x01 |
| 26 | |
| 27 | // Address types |
| 28 | socks5AtypIPv4 = 0x01 |
| 29 | socks5AtypDomain = 0x03 |
| 30 | socks5AtypIPv6 = 0x04 |
| 31 | |
| 32 | // Reply codes |
| 33 | socks5RepSuccess = 0x00 |
| 34 | ) |
| 35 | |
| 36 | var socks5Errors = map[byte]string{ |
| 37 | 0x01: "general SOCKS server failure", |
| 38 | 0x02: "connection not allowed by ruleset", |
| 39 | 0x03: "network unreachable", |
| 40 | 0x04: "host unreachable", |
| 41 | 0x05: "connection refused", |
| 42 | 0x06: "TTL expired", |
| 43 | 0x07: "command not supported", |
| 44 | 0x08: "address type not supported", |
| 45 | } |
| 46 | |
| 47 | // SOCKS5Dialer implements ProxyDialer for RFC 1928 / RFC 1929 SOCKS5 proxies. |
| 48 | type SOCKS5Dialer struct { |
| 49 | proxyAddr string |
| 50 | username string |
| 51 | password string |
| 52 | timeout time.Duration |
| 53 | } |
| 54 | |
| 55 | // NewSOCKS5Dialer creates a new SOCKS5Dialer from Options. |
| 56 | func NewSOCKS5Dialer(opts Options) (*SOCKS5Dialer, error) { |
| 57 | addr := opts.Mode |
| 58 | if strings.Contains(addr, "://") { |
| 59 | u, err := url.Parse(addr) |
| 60 | if err != nil { |
| 61 | return nil, fmt.Errorf("invalid socks5 proxy URL: %w", err) |
| 62 | } |
| 63 | addr = u.Host |
| 64 | if u.User != nil { |
| 65 | if opts.Username == "" { |
| 66 | opts.Username = u.User.Username() |
| 67 | } |
| 68 | if opts.Password == "" { |
| 69 | opts.Password, _ = u.User.Password() |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | if !strings.Contains(addr, ":") { |
| 75 | addr = addr + ":1080" |
| 76 | } |
| 77 | |
| 78 | timeout := opts.Timeout |
| 79 | if timeout <= 0 { |
| 80 | timeout = 10 * time.Second |
| 81 | } |
| 82 | |
| 83 | return &SOCKS5Dialer{ |
| 84 | proxyAddr: addr, |
| 85 | username: opts.Username, |
| 86 | password: opts.Password, |
| 87 | timeout: timeout, |
| 88 | }, nil |
| 89 | } |
| 90 | |
| 91 | func (s *SOCKS5Dialer) ProxyHop() string { |
| 92 | return s.proxyAddr |
| 93 | } |
| 94 | |
| 95 | func (s *SOCKS5Dialer) Mode() string { |
| 96 | return "socks5" |
| 97 | } |
| 98 | |
| 99 | func (s *SOCKS5Dialer) DialContext(ctx context.Context, network, targetAddr string) (net.Conn, error) { |
| 100 | dialer := &net.Dialer{ |
| 101 | Timeout: s.timeout, |
| 102 | } |
| 103 | |
| 104 | conn, err := dialer.DialContext(ctx, "tcp", s.proxyAddr) |
| 105 | if err != nil { |
| 106 | return nil, fmt.Errorf("failed to connect to SOCKS5 proxy %s: %w", s.proxyAddr, err) |
| 107 | } |
| 108 | |
| 109 | // Apply deadline |
| 110 | if deadline, ok := ctx.Deadline(); ok { |
| 111 | _ = conn.SetDeadline(deadline) |
| 112 | } else if s.timeout > 0 { |
| 113 | _ = conn.SetDeadline(time.Now().Add(s.timeout)) |
| 114 | } |
| 115 | defer func() { |
| 116 | // Clear deadline after handshake |
| 117 | _ = conn.SetDeadline(time.Time{}) |
| 118 | }() |
| 119 | |
| 120 | // 1. Method negotiation |
| 121 | var methods []byte |
| 122 | if s.username != "" { |
| 123 | methods = []byte{socks5AuthNone, socks5AuthUserPass} |
| 124 | } else { |
| 125 | methods = []byte{socks5AuthNone} |
| 126 | } |
| 127 | |
| 128 | req := make([]byte, 2+len(methods)) |
| 129 | req[0] = socks5Version |
| 130 | req[1] = byte(len(methods)) |
| 131 | copy(req[2:], methods) |
| 132 | |
| 133 | if _, err := conn.Write(req); err != nil { |
| 134 | conn.Close() |
| 135 | return nil, fmt.Errorf("failed to send SOCKS5 method request: %w", err) |
| 136 | } |
| 137 | |
| 138 | resp := make([]byte, 2) |
| 139 | if _, err := io.ReadFull(conn, resp); err != nil { |
| 140 | conn.Close() |
| 141 | return nil, fmt.Errorf("failed to read SOCKS5 method response: %w", err) |
| 142 | } |
| 143 | |
| 144 | if resp[0] != socks5Version { |
| 145 | conn.Close() |
| 146 | return nil, fmt.Errorf("unsupported SOCKS version %d", resp[0]) |
| 147 | } |
| 148 | |
| 149 | chosenMethod := resp[1] |
| 150 | switch chosenMethod { |
| 151 | case socks5AuthNone: |
| 152 | // No authentication required |
| 153 | case socks5AuthUserPass: |
| 154 | if err := s.authenticateUserPass(conn); err != nil { |
| 155 | conn.Close() |
| 156 | return nil, err |
| 157 | } |
| 158 | case socks5AuthNoAccept: |
| 159 | conn.Close() |
| 160 | return nil, errors.New("SOCKS5 proxy rejected authentication methods") |
| 161 | default: |
| 162 | conn.Close() |
| 163 | return nil, fmt.Errorf("unknown SOCKS5 auth method %d chosen by server", chosenMethod) |
| 164 | } |
| 165 | |
| 166 | // 2. CONNECT Request |
| 167 | host, portStr, err := net.SplitHostPort(targetAddr) |
| 168 | if err != nil { |
| 169 | conn.Close() |
| 170 | return nil, fmt.Errorf("invalid target address %q: %w", targetAddr, err) |
| 171 | } |
| 172 | |
| 173 | port, err := strconv.Atoi(portStr) |
| 174 | if err != nil || port <= 0 || port > 65535 { |
| 175 | conn.Close() |
| 176 | return nil, fmt.Errorf("invalid target port %q: %w", portStr, err) |
| 177 | } |
| 178 | |
| 179 | var cmd []byte |
| 180 | if ip := net.ParseIP(host); ip != nil { |
| 181 | if ipv4 := ip.To4(); ipv4 != nil { |
| 182 | cmd = make([]byte, 4+4+2) |
| 183 | cmd[0] = socks5Version |
| 184 | cmd[1] = socks5CmdConnect |
| 185 | cmd[2] = 0x00 // Reserved |
| 186 | cmd[3] = socks5AtypIPv4 |
| 187 | copy(cmd[4:8], ipv4) |
| 188 | binary.BigEndian.PutUint16(cmd[8:10], uint16(port)) |
| 189 | } else { |
| 190 | ipv6 := ip.To16() |
| 191 | cmd = make([]byte, 4+16+2) |
| 192 | cmd[0] = socks5Version |
| 193 | cmd[1] = socks5CmdConnect |
| 194 | cmd[2] = 0x00 // Reserved |
| 195 | cmd[3] = socks5AtypIPv6 |
| 196 | copy(cmd[4:20], ipv6) |
| 197 | binary.BigEndian.PutUint16(cmd[20:22], uint16(port)) |
| 198 | } |
| 199 | } else { |
| 200 | // ATYP 0x03 Domain name |
| 201 | cmd = make([]byte, 4+1+len(host)+2) |
| 202 | cmd[0] = socks5Version |
| 203 | cmd[1] = socks5CmdConnect |
| 204 | cmd[2] = 0x00 // Reserved |
| 205 | cmd[3] = socks5AtypDomain |
| 206 | cmd[4] = byte(len(host)) |
| 207 | copy(cmd[5:5+len(host)], []byte(host)) |
| 208 | binary.BigEndian.PutUint16(cmd[5+len(host):], uint16(port)) |
| 209 | } |
| 210 | |
| 211 | if _, err := conn.Write(cmd); err != nil { |
| 212 | conn.Close() |
| 213 | return nil, fmt.Errorf("failed to send SOCKS5 CONNECT command: %w", err) |
| 214 | } |
| 215 | |
| 216 | // 3. Read Server Reply |
| 217 | replyHdr := make([]byte, 4) |
| 218 | if _, err := io.ReadFull(conn, replyHdr); err != nil { |
| 219 | conn.Close() |
| 220 | return nil, fmt.Errorf("failed to read SOCKS5 reply header: %w", err) |
| 221 | } |
| 222 | |
| 223 | if replyHdr[0] != socks5Version { |
| 224 | conn.Close() |
| 225 | return nil, fmt.Errorf("invalid SOCKS5 reply version %d", replyHdr[0]) |
| 226 | } |
| 227 | |
| 228 | if replyHdr[1] != socks5RepSuccess { |
| 229 | conn.Close() |
| 230 | errMsg, ok := socks5Errors[replyHdr[1]] |
| 231 | if !ok { |
| 232 | errMsg = fmt.Sprintf("error code 0x%02x", replyHdr[1]) |
| 233 | } |
| 234 | return nil, fmt.Errorf("SOCKS5 connection failed: %s", errMsg) |
| 235 | } |
| 236 | |
| 237 | // Consume bound address and port |
| 238 | atyp := replyHdr[3] |
| 239 | switch atyp { |
| 240 | case socks5AtypIPv4: |
| 241 | bnd := make([]byte, 4+2) |
| 242 | if _, err := io.ReadFull(conn, bnd); err != nil { |
| 243 | conn.Close() |
| 244 | return nil, err |
| 245 | } |
| 246 | case socks5AtypDomain: |
| 247 | lenBuf := make([]byte, 1) |
| 248 | if _, err := io.ReadFull(conn, lenBuf); err != nil { |
| 249 | conn.Close() |
| 250 | return nil, err |
| 251 | } |
| 252 | bnd := make([]byte, int(lenBuf[0])+2) |
| 253 | if _, err := io.ReadFull(conn, bnd); err != nil { |
| 254 | conn.Close() |
| 255 | return nil, err |
| 256 | } |
| 257 | case socks5AtypIPv6: |
| 258 | bnd := make([]byte, 16+2) |
| 259 | if _, err := io.ReadFull(conn, bnd); err != nil { |
| 260 | conn.Close() |
| 261 | return nil, err |
| 262 | } |
| 263 | default: |
| 264 | conn.Close() |
| 265 | return nil, fmt.Errorf("unsupported SOCKS5 bound address type 0x%02x", atyp) |
| 266 | } |
| 267 | |
| 268 | return conn, nil |
| 269 | } |
| 270 | |
| 271 | func (s *SOCKS5Dialer) authenticateUserPass(conn net.Conn) error { |
| 272 | // RFC 1929 subnegotiation: |
| 273 | // +----+------+----------+------+----------+ |
| 274 | // |VER | ULEN | UNAME | PLEN | PASSWD | |
| 275 | // +----+------+----------+------+----------+ |
| 276 | // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | |
| 277 | // +----+------+----------+------+----------+ |
| 278 | uLen := len(s.username) |
| 279 | pLen := len(s.password) |
| 280 | if uLen > 255 || pLen > 255 { |
| 281 | return errors.New("SOCKS5 username or password exceeds 255 bytes") |
| 282 | } |
| 283 | |
| 284 | authReq := make([]byte, 3+uLen+pLen) |
| 285 | authReq[0] = 0x01 |
| 286 | authReq[1] = byte(uLen) |
| 287 | copy(authReq[2:2+uLen], []byte(s.username)) |
| 288 | authReq[2+uLen] = byte(pLen) |
| 289 | copy(authReq[3+uLen:], []byte(s.password)) |
| 290 | |
| 291 | if _, err := conn.Write(authReq); err != nil { |
| 292 | return fmt.Errorf("failed to send SOCKS5 auth request: %w", err) |
| 293 | } |
| 294 | |
| 295 | authResp := make([]byte, 2) |
| 296 | if _, err := io.ReadFull(conn, authResp); err != nil { |
| 297 | return fmt.Errorf("failed to read SOCKS5 auth response: %w", err) |
| 298 | } |
| 299 | |
| 300 | if authResp[0] != 0x01 { |
| 301 | return fmt.Errorf("invalid SOCKS5 auth version %d", authResp[0]) |
| 302 | } |
| 303 | |
| 304 | if authResp[1] != 0x00 { |
| 305 | return errors.New("SOCKS5 username/password authentication failed") |
| 306 | } |
| 307 | |
| 308 | return nil |
| 309 | } |