diff --git a/integration_tests/socks5/3proxy.cfg b/integration_tests/socks5/3proxy.cfg new file mode 100644 index 00000000..e2970a62 --- /dev/null +++ b/integration_tests/socks5/3proxy.cfg @@ -0,0 +1,12 @@ +internal 0.0.0.0 +external 0.0.0.0 + +maxconn 10 + +auth none + +socks -p1080 + +allow * + +flush \ No newline at end of file diff --git a/integration_tests/socks5/cleanup.sh b/integration_tests/socks5/cleanup.sh new file mode 100755 index 00000000..7e039ded --- /dev/null +++ b/integration_tests/socks5/cleanup.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set +e + +echo "socks5/cleanup: Tests cleanup for socks5" + +CONTAINER_NAME=zgrab_socks5 + +docker stop $CONTAINER_NAME diff --git a/integration_tests/socks5/setup.sh b/integration_tests/socks5/setup.sh new file mode 100755 index 00000000..c391d756 --- /dev/null +++ b/integration_tests/socks5/setup.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +echo "socks5/setup: Tests setup for socks5" + +CONTAINER_TAG="3proxy/3proxy" +CONTAINER_NAME="zgrab_socks5" + +# If the container is already running, use it. +if docker ps --filter "name=$CONTAINER_NAME" | grep -q $CONTAINER_NAME; then + echo "socks5/setup: Container $CONTAINER_NAME already running -- nothing to setup" + exit 0 +fi + +DOCKER_RUN_FLAGS="--rm --name $CONTAINER_NAME -e "PROXY_USER=user" -e "PROXY_PASS=password" -v ./3proxy.cfg:/etc/3proxy/3proxy.cfg -td" + +# If it is not running, try launching it -- on success, use that. +echo "socks5/setup: Trying to launch $CONTAINER_NAME..." +if ! docker run $DOCKER_RUN_FLAGS $CONTAINER_TAG; then + echo "failed" + # echo "socks5/setup: Building docker image $CONTAINER_TAG..." + # # If it fails, build it from ./container/Dockerfile + # docker build -t $CONTAINER_TAG ./container + # # Try again + # echo "socks5/setup: Launching $CONTAINER_NAME..." + # docker run $DOCKER_RUN_FLAGS $CONTAINER_TAG +fi diff --git a/integration_tests/socks5/test.sh b/integration_tests/socks5/test.sh new file mode 100755 index 00000000..52df6eeb --- /dev/null +++ b/integration_tests/socks5/test.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -e +MODULE_DIR=$(dirname $0) +ZGRAB_ROOT=$(git rev-parse --show-toplevel) +ZGRAB_OUTPUT=$ZGRAB_ROOT/zgrab-output + +mkdir -p $ZGRAB_OUTPUT/socks5 + +CONTAINER_NAME=zgrab_socks5 + +OUTPUT_FILE=$ZGRAB_OUTPUT/socks5/socks5.json + +echo "socks5/test: Tests runner for socks5" +# TODO FIXME: Add any necessary flags or additional tests +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh socks5 > $OUTPUT_FILE + +# Dump the docker logs +echo "socks5/test: BEGIN docker logs from $CONTAINER_NAME [{(" +docker logs --tail all $CONTAINER_NAME +echo ")}] END docker logs from $CONTAINER_NAME" + +# TODO: If there are any other relevant log files, dump those to stdout here. diff --git a/modules/socks5.go b/modules/socks5.go new file mode 100644 index 00000000..629e05ce --- /dev/null +++ b/modules/socks5.go @@ -0,0 +1,7 @@ +package modules + +import "github.com/zmap/zgrab2/modules/socks5" + +func init() { + socks5.RegisterModule() +} diff --git a/modules/socks5/scanner.go b/modules/socks5/scanner.go new file mode 100644 index 00000000..885fa3a6 --- /dev/null +++ b/modules/socks5/scanner.go @@ -0,0 +1,255 @@ +// Package socks5 contains the zgrab2 Module implementation for SOCKS5. +package socks5 + +import ( + "fmt" + "net" + + log "github.com/sirupsen/logrus" + "github.com/zmap/zgrab2" +) + +// ScanResults is the output of the scan. +type ScanResults struct { + Version string `json:"version,omitempty"` + MethodSelection string `json:"method_selection,omitempty"` + ConnectionResponse string `json:"connection_response,omitempty"` + ConnectionResponseExplanation map[string]string `json:"connection_response_explanation,omitempty"` +} + +// Flags are the SOCKS5-specific command-line flags. +type Flags struct { + zgrab2.BaseFlags + Verbose bool `long:"verbose" description:"More verbose logging, include debug fields in the scan results"` +} + +// Module implements the zgrab2.Module interface. +type Module struct { +} + +// Scanner implements the zgrab2.Scanner interface, and holds the state +// for a single scan. +type Scanner struct { + config *Flags +} + +// Connection holds the state for a single connection to the SOCKS5 server. +type Connection struct { + buffer [10000]byte + config *Flags + results ScanResults + conn net.Conn +} + +// RegisterModule registers the socks5 zgrab2 module. +func RegisterModule() { + var module Module + _, err := zgrab2.AddCommand("socks5", "SOCKS5", module.Description(), 1080, &module) + if err != nil { + log.Fatal(err) + } +} + +// NewFlags returns the default flags object to be filled in with the +// command-line arguments. +func (m *Module) NewFlags() interface{} { + return new(Flags) +} + +// NewScanner returns a new Scanner instance. +func (m *Module) NewScanner() zgrab2.Scanner { + return new(Scanner) +} + +// Description returns an overview of this module. +func (m *Module) Description() string { + return "Perform a SOCKS5 scan" +} + +// Validate flags +func (f *Flags) Validate(args []string) (err error) { + return +} + +// Help returns this module's help string. +func (f *Flags) Help() string { + return "" +} + +// Protocol returns the protocol identifier for the scanner. +func (s *Scanner) Protocol() string { + return "socks5" +} + +// Init initializes the Scanner instance with the flags from the command line. +func (s *Scanner) Init(flags zgrab2.ScanFlags) error { + f, _ := flags.(*Flags) + s.config = f + return nil +} + +// InitPerSender does nothing in this module. +func (s *Scanner) InitPerSender(senderID int) error { + return nil +} + +// GetName returns the configured name for the Scanner. +func (s *Scanner) GetName() string { + return s.config.Name +} + +// GetTrigger returns the Trigger defined in the Flags. +func (scanner *Scanner) GetTrigger() string { + return scanner.config.Trigger +} + +// readResponse reads a response from the SOCKS5 server. +func (conn *Connection) readResponse(expectedLength int) ([]byte, error) { + resp := make([]byte, expectedLength) + _, err := conn.conn.Read(resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// sendCommand sends a command to the SOCKS5 server. +func (conn *Connection) sendCommand(cmd []byte) error { + _, err := conn.conn.Write(cmd) + return err +} + +// explainResponse converts the raw response into a human-readable explanation. +func explainResponse(resp []byte) map[string]string { + if len(resp) < 10 { + return map[string]string{"error": "response too short"} + } + + return map[string]string{ + "Version": fmt.Sprintf("0x%02x (SOCKS Version 5)", resp[0]), + "Reply": fmt.Sprintf("0x%02x (%s)", resp[1], getReplyDescription(resp[1])), + "Reserved": fmt.Sprintf("0x%02x", resp[2]), + "Address Type": fmt.Sprintf("0x%02x (%s)", resp[3], getAddressTypeDescription(resp[3])), + "Bound Address": fmt.Sprintf("%d.%d.%d.%d", resp[4], resp[5], resp[6], resp[7]), + "Bound Port": fmt.Sprintf("%d", int(resp[8])<<8|int(resp[9])), + } +} + +func getReplyDescription(code byte) string { + switch code { + case 0x00: + return "succeeded" + case 0x01: + return "general SOCKS server failure" + case 0x02: + return "connection not allowed by ruleset" + case 0x03: + return "network unreachable" + case 0x04: + return "host unreachable" + case 0x05: + return "connection refused" + case 0x06: + return "TTL expired" + case 0x07: + return "command not supported" + case 0x08: + return "address type not supported" + default: + return "unassigned" + } +} + +func getAddressTypeDescription(code byte) string { + switch code { + case 0x01: + return "IPv4 address" + case 0x03: + return "Domain name" + case 0x04: + return "IPv6 address" + default: + return "unknown" + } +} + +// PerformHandshake performs the SOCKS5 handshake. +func (conn *Connection) PerformHandshake() (bool, error) { + // Send version identifier/method selection message + verMethodSel := []byte{0x05, 0x01, 0x00} // VER = 0x05, NMETHODS = 1, METHODS = 0x00 (NO AUTHENTICATION REQUIRED) + err := conn.sendCommand(verMethodSel) + if err != nil { + return false, fmt.Errorf("error sending version identifier/method selection: %w", err) + } + conn.results.Version = "0x05" + + // Read method selection response + methodSelResp, err := conn.readResponse(2) + if err != nil { + return false, fmt.Errorf("error reading method selection response: %w", err) + } + conn.results.MethodSelection = fmt.Sprintf("%x", methodSelResp) + + if methodSelResp[1] == 0xFF { + return true, fmt.Errorf("no acceptable authentication methods") + } + + return false, nil +} + +// PerformConnectionRequest sends a connection request to the SOCKS5 server. +func (conn *Connection) PerformConnectionRequest() error { + // Send a connection request + req := []byte{0x05, 0x01, 0x00, 0x01, 0xA6, 0x6F, 0x04, 0x64, 0x00, 0x50} // VER = 0x05, CMD = CONNECT, RSV = 0x00, ATYP = IPv4, DST.ADDR = 166.111.4.100, DST.PORT = 80 + err := conn.sendCommand(req) + if err != nil { + return fmt.Errorf("error sending connection request: %w", err) + } + + // Read connection response + resp, err := conn.readResponse(10) + if err != nil { + return fmt.Errorf("error reading connection response: %w", err) + } + conn.results.ConnectionResponse = fmt.Sprintf("%x", resp) + conn.results.ConnectionResponseExplanation = explainResponse(resp) + + if resp[1] > 0x80 { + return fmt.Errorf("connection request failed with response: %x", resp) + } + + return nil +} + +// Scan performs the configured scan on the SOCKS5 server. +func (s *Scanner) Scan(t zgrab2.ScanTarget) (status zgrab2.ScanStatus, result interface{}, thrown error) { + var err error + var have_auth bool + conn, err := t.Open(&s.config.BaseFlags) + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("error opening connection: %w", err) + } + cn := conn + defer func() { + cn.Close() + }() + + results := ScanResults{} + socks5Conn := Connection{conn: cn, config: s.config, results: results} + + have_auth, err = socks5Conn.PerformHandshake() + if err != nil { + if have_auth { + return zgrab2.SCAN_SUCCESS, &socks5Conn.results, nil + } else { + return zgrab2.TryGetScanStatus(err), &socks5Conn.results, fmt.Errorf("error during handshake: %w", err) + } + } + + err = socks5Conn.PerformConnectionRequest() + if err != nil { + return zgrab2.TryGetScanStatus(err), &socks5Conn.results, fmt.Errorf("error during connection request: %w", err) + } + + return zgrab2.SCAN_SUCCESS, &socks5Conn.results, nil +} diff --git a/zgrab2_schemas/zgrab2/__init__.py b/zgrab2_schemas/zgrab2/__init__.py index 4a261f08..51b15e96 100644 --- a/zgrab2_schemas/zgrab2/__init__.py +++ b/zgrab2_schemas/zgrab2/__init__.py @@ -22,5 +22,6 @@ from . import ipp from . import banner from . import amqp091 +from . import socks5 from . import mqtt from . import pptp diff --git a/zgrab2_schemas/zgrab2/socks5.py b/zgrab2_schemas/zgrab2/socks5.py new file mode 100644 index 00000000..c41975c4 --- /dev/null +++ b/zgrab2_schemas/zgrab2/socks5.py @@ -0,0 +1,38 @@ +# zschema sub-schema for zgrab2's Socks5 module +# Registers zgrab2-socks5 globally, and socks5 with the main zgrab2 schema. +from zschema.leaves import * +from zschema.compounds import * +import zschema.registry + +from . import zgrab2 + +# Schema for ScanResults struct +socks5_response_explanation = SubRecord( + { + "Version": String(), + "Reply": String(), + "Reserved": String(), + "Address Type": String(), + "Bound Address": String(), + "Bound Port": String(), + } +) + +socks5_scan_response = SubRecord( + { + "version": String(), + "method_selection": String(), + "connection_response": String(), + "connection_response_explanation": socks5_response_explanation, + } +) + +socks5_scan = SubRecord( + { + "result": socks5_scan_response, + }, + extends=zgrab2.base_scan_response, +) + +zschema.registry.register_schema("zgrab2-socks5", socks5_scan) +zgrab2.register_scan_response_type("socks5", socks5_scan)