Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for socks5 #471

Merged
merged 12 commits into from
Jan 27, 2025
12 changes: 12 additions & 0 deletions integration_tests/socks5/3proxy.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
internal 0.0.0.0
external 0.0.0.0

maxconn 10

auth none

socks -p1080

allow *

flush
9 changes: 9 additions & 0 deletions integration_tests/socks5/cleanup.sh
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions integration_tests/socks5/setup.sh
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions integration_tests/socks5/test.sh
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions modules/socks5.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package modules

import "github.com/zmap/zgrab2/modules/socks5"

func init() {
socks5.RegisterModule()
}
255 changes: 255 additions & 0 deletions modules/socks5/scanner.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions zgrab2_schemas/zgrab2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
from . import ipp
from . import banner
from . import amqp091
from . import socks5
from . import mqtt
from . import pptp
38 changes: 38 additions & 0 deletions zgrab2_schemas/zgrab2/socks5.py
Original file line number Diff line number Diff line change
@@ -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)
Loading