Skip to content

Commit

Permalink
Merge pull request #471 from AlexAQ972/socks5
Browse files Browse the repository at this point in the history
Add support for socks5
  • Loading branch information
phillip-stephens authored Jan 27, 2025
2 parents 00852fd + eb7e37b commit 06df365
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 0 deletions.
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)

0 comments on commit 06df365

Please sign in to comment.