adding telnet login + crypto (#6419)

* adding telnet login + crypto

* smbauth lib porting + ntlm parsing over telnet

* gen lib

* adding telnet test

* adding breakout after max iterations

* fix(utils): broken pkt creation & impl `Create{LN,NT}Response`

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore(utils): satisfy lints

Signed-off-by: Dwi Siswanto <git@dw1.io>

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
Co-authored-by: Dwi Siswanto <git@dw1.io>
This commit is contained in:
Mzack9999
2026-01-02 03:28:46 +04:00
committed by GitHub
parent 891dffb4a1
commit dbeebdaa1d
11 changed files with 1833 additions and 4 deletions

View File

@@ -22,6 +22,7 @@ var jsTestcases = []TestCaseInfo{
{Path: "protocols/javascript/mysql-connect.yaml", TestCase: &javascriptMySQLConnect{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
{Path: "protocols/javascript/multi-ports.yaml", TestCase: &javascriptMultiPortsSSH{}},
{Path: "protocols/javascript/no-port-args.yaml", TestCase: &javascriptNoPortArgs{}},
{Path: "protocols/javascript/telnet-auth-test.yaml", TestCase: &javascriptTelnetAuthTest{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
}
var (
@@ -29,6 +30,7 @@ var (
sshResource *dockertest.Resource
oracleResource *dockertest.Resource
vncResource *dockertest.Resource
telnetResource *dockertest.Resource
postgresResource *dockertest.Resource
mysqlResource *dockertest.Resource
rsyncResource *dockertest.Resource
@@ -292,6 +294,38 @@ func (j *javascriptRsyncTest) Execute(filePath string) error {
return multierr.Combine(errs...)
}
type javascriptTelnetAuthTest struct{}
func (j *javascriptTelnetAuthTest) Execute(filePath string) error {
if telnetResource == nil || pool == nil {
// skip test as telnet is not running
return nil
}
tempPort := telnetResource.GetPort("23/tcp")
finalURL := "localhost:" + tempPort
defer purge(telnetResource)
errs := []error{}
for i := 0; i < defaultRetry; i++ {
results := []string{}
var err error
_ = pool.Retry(func() error {
//let telnet server start
time.Sleep(3 * time.Second)
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
return nil
})
if err != nil {
return err
}
if err := expectResultsCount(results, 1); err == nil {
return nil
} else {
errs = append(errs, err)
}
}
return multierr.Combine(errs...)
}
// purge any given resource if it is not nil
func purge(resource *dockertest.Resource) {
if resource != nil && pool != nil {
@@ -447,4 +481,22 @@ func init() {
if err := rsyncResource.Expire(30); err != nil {
log.Printf("Could not expire Rsync resource: %s", err)
}
// setup a temporary telnet server
// username: dev
// password: mysecret
telnetResource, err = pool.RunWithOptions(&dockertest.RunOptions{
Repository: "alpine",
Tag: "latest",
Cmd: []string{"sh", "-c", "apk add --no-cache busybox-extras shadow && useradd -m dev && echo 'dev:mysecret' | chpasswd && exec /usr/sbin/telnetd -F -p 23 -l /bin/login"},
Platform: "linux/amd64",
})
if err != nil {
log.Printf("Could not start Telnet resource: %s", err)
return
}
// by default expire after 30 sec
if err := telnetResource.Expire(30); err != nil {
log.Printf("Could not expire Telnet resource: %s", err)
}
}

2
go.mod
View File

@@ -50,6 +50,7 @@ require (
code.gitea.io/sdk/gitea v0.17.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0
github.com/Azure/go-ntlmssp v0.1.0
github.com/DataDog/gostackparse v0.7.0
github.com/Masterminds/semver/v3 v3.2.1
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057
@@ -138,7 +139,6 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect

4
go.sum
View File

@@ -64,8 +64,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 h1:nVocQV40OQne5613E
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0/go.mod h1:7QJP7dr2wznCMeqIrhMgWGf7XpAQnVrJqDm9nvV3Cu4=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=

View File

@@ -0,0 +1,28 @@
id: telnet-auth-test
info:
name: Telnet Authentication Test
author: pdteam
severity: info
metadata:
shodan-query: port:23
javascript:
- code: |
var m = require("nuclei/telnet");
var c = m.TelnetClient();
c.Connect(Host, Port, User, Password);
args:
Host: "{{Host}}"
Port: "23"
User: "dev"
Password: "mysecret"
matchers:
- type: dsl
dsl:
- "response == true"
- "success == true"
condition: and

View File

@@ -2,6 +2,7 @@ package telnet
import (
lib_telnet "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/telnet"
telnetmini "github.com/projectdiscovery/nuclei/v3/pkg/utils/telnetmini"
"github.com/Mzack9999/goja"
"github.com/projectdiscovery/nuclei/v3/pkg/js/gojs"
@@ -20,7 +21,10 @@ func init() {
// Var and consts
// Objects / Classes
"IsTelnetResponse": gojs.GetClassConstructor[lib_telnet.IsTelnetResponse](&lib_telnet.IsTelnetResponse{}),
"TelnetClient": gojs.GetClassConstructor[lib_telnet.TelnetClient](&lib_telnet.TelnetClient{}),
"IsTelnetResponse": gojs.GetClassConstructor[lib_telnet.IsTelnetResponse](&lib_telnet.IsTelnetResponse{}),
"TelnetInfoResponse": gojs.GetClassConstructor[lib_telnet.TelnetInfoResponse](&lib_telnet.TelnetInfoResponse{}),
"NTLMInfoResponse": gojs.GetClassConstructor[telnetmini.NTLMInfoResponse](&telnetmini.NTLMInfoResponse{}),
},
).Register()
}

View File

@@ -13,7 +13,65 @@ export function IsTelnet(host: string, port: number): IsTelnetResponse | null {
return null;
}
/**
* TelnetClient is a client for Telnet servers.
* @example
* ```javascript
* const telnet = require('nuclei/telnet');
* const client = new telnet.TelnetClient();
* ```
*/
export class TelnetClient {
/**
* Connect tries to connect to provided host and port with telnet.
* Optionally provides username and password for authentication.
* Returns state of connection. If the connection is successful,
* the function will return true, otherwise false.
* @example
* ```javascript
* const telnet = require('nuclei/telnet');
* const client = new telnet.TelnetClient();
* const connected = client.Connect('acme.com', 23, 'username', 'password');
* ```
*/
public Connect(host: string, port: number, username: string, password: string): boolean {
return false;
}
/**
* Info gathers information about the telnet server including encryption support.
* Uses the telnetmini library's DetectEncryption helper function.
* WARNING: The connection used for detection becomes unusable after this call.
* @example
* ```javascript
* const telnet = require('nuclei/telnet');
* const client = new telnet.TelnetClient();
* const info = client.Info('acme.com', 23);
* log(toJSON(info));
* ```
*/
public Info(host: string, port: number): TelnetInfoResponse | null {
return null;
}
/**
* GetTelnetNTLMInfo implements the Nmap telnet-ntlm-info.nse script functionality.
* This function uses the telnetmini library and SMB packet crafting functions to send
* MS-TNAP NTLM authentication requests with null credentials. It might work only on
* Microsoft Telnet servers.
* @example
* ```javascript
* const telnet = require('nuclei/telnet');
* const client = new telnet.TelnetClient();
* const ntlmInfo = client.GetTelnetNTLMInfo('acme.com', 23);
* log(toJSON(ntlmInfo));
* ```
*/
public GetTelnetNTLMInfo(host: string, port: number): NTLMInfoResponse | null {
return null;
}
}
/**
* IsTelnetResponse is the response from the IsTelnet function.
@@ -32,3 +90,76 @@ export interface IsTelnetResponse {
Banner?: string,
}
/**
* TelnetInfoResponse is the response from the Info function.
* @example
* ```javascript
* const telnet = require('nuclei/telnet');
* const client = new telnet.TelnetClient();
* const info = client.Info('acme.com', 23);
* log(toJSON(info));
* ```
*/
export interface TelnetInfoResponse {
SupportsEncryption?: boolean,
Banner?: string,
Options?: { [key: number]: number[] },
}
/**
* NTLMInfoResponse represents the response from NTLM information gathering.
* This matches exactly the output structure from the Nmap telnet-ntlm-info.nse script.
* @example
* ```javascript
* const telnet = require('nuclei/telnet');
* const client = new telnet.TelnetClient();
* const ntlmInfo = client.GetTelnetNTLMInfo('acme.com', 23);
* log(toJSON(ntlmInfo));
* ```
*/
export interface NTLMInfoResponse {
/**
* Target_Name from script (target_realm in script)
*/
TargetName?: string,
/**
* NetBIOS_Domain_Name from script
*/
NetBIOSDomainName?: string,
/**
* NetBIOS_Computer_Name from script
*/
NetBIOSComputerName?: string,
/**
* DNS_Domain_Name from script
*/
DNSDomainName?: string,
/**
* DNS_Computer_Name from script (fqdn in script)
*/
DNSComputerName?: string,
/**
* DNS_Tree_Name from script (dns_forest_name in script)
*/
DNSTreeName?: string,
/**
* Product_Version from script
*/
ProductVersion?: string,
/**
* Raw timestamp for skew calculation
*/
Timestamp?: number,
}

View File

@@ -10,6 +10,23 @@ import (
"github.com/praetorian-inc/fingerprintx/pkg/plugins"
"github.com/praetorian-inc/fingerprintx/pkg/plugins/services/telnet"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/telnetmini"
)
// Telnet protocol constants
const (
IAC = 255 // Interpret As Command
WILL = 251 // Will
WONT = 252 // Won't
DO = 253 // Do
DONT = 254 // Don't
SB = 250 // Subnegotiation Begin
SE = 240 // Subnegotiation End
ECHO = 1 // Echo
SUPPRESS_GO_AHEAD = 3 // Suppress Go Ahead
TERMINAL_TYPE = 24 // Terminal Type
NAWS = 31 // Negotiate About Window Size
ENCRYPT = 38 // Encryption option (0x26)
)
type (
@@ -25,6 +42,28 @@ type (
IsTelnet bool
Banner string
}
// TelnetInfoResponse is the response from the Info function.
// @example
// ```javascript
// const telnet = require('nuclei/telnet');
// const client = new telnet.TelnetClient();
// const info = client.Info('acme.com', 23);
// log(toJSON(info));
// ```
TelnetInfoResponse struct {
SupportsEncryption bool
Banner string
Options map[int][]int
}
// TelnetClient is a client for Telnet servers.
// @example
// ```javascript
// const telnet = require('nuclei/telnet');
// const client = new telnet.TelnetClient();
// ```
TelnetClient struct{}
)
// IsTelnet checks if a host is running a Telnet server.
@@ -69,3 +108,171 @@ func isTelnet(executionId string, host string, port int) (IsTelnetResponse, erro
resp.IsTelnet = true
return resp, nil
}
// Connect tries to connect to provided host and port with telnet.
// Optionally provides username and password for authentication.
// Returns state of connection. If the connection is successful,
// the function will return true, otherwise false.
// @example
// ```javascript
// const telnet = require('nuclei/telnet');
// const client = new telnet.TelnetClient();
// const connected = client.Connect('acme.com', 23, 'username', 'password');
// ```
func (c *TelnetClient) Connect(ctx context.Context, host string, port int, username string, password string) (bool, error) {
executionId := ctx.Value("executionId").(string)
dialer := protocolstate.GetDialersWithId(executionId)
if dialer == nil {
return false, fmt.Errorf("dialers not initialized for %s", executionId)
}
if !protocolstate.IsHostAllowed(executionId, host) {
return false, protocolstate.ErrHostDenied.Msgf(host)
}
// Create TCP connection
conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
if err != nil {
return false, err
}
// Create telnet client using the telnetmini library
client := telnetmini.New(conn)
defer func() {
_ = client.Close()
}()
// Handle authentication if credentials provided
if username != "" && password != "" {
// Set a timeout context for authentication
authCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := client.Auth(authCtx, username, password); err != nil {
return false, err
}
}
return true, nil
}
// Info gathers information about the telnet server including encryption support.
// Uses the telnetmini library's DetectEncryption helper function.
// WARNING: The connection used for detection becomes unusable after this call.
// @example
// ```javascript
// const telnet = require('nuclei/telnet');
// const client = new telnet.TelnetClient();
// const info = client.Info('acme.com', 23);
// log(toJSON(info));
// ```
func (c *TelnetClient) Info(ctx context.Context, host string, port int) (TelnetInfoResponse, error) {
executionId := ctx.Value("executionId").(string)
if !protocolstate.IsHostAllowed(executionId, host) {
return TelnetInfoResponse{}, protocolstate.ErrHostDenied.Msgf(host)
}
// Create TCP connection for encryption detection
dialer := protocolstate.GetDialersWithId(executionId)
if dialer == nil {
return TelnetInfoResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
}
conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
if err != nil {
return TelnetInfoResponse{}, err
}
defer func() {
_ = conn.Close()
}()
// Use the telnetmini library's DetectEncryption helper function
// Note: The connection becomes unusable after this call
encryptionInfo, err := telnetmini.DetectEncryption(conn, 7*time.Second)
if err != nil {
return TelnetInfoResponse{}, err
}
return TelnetInfoResponse{
SupportsEncryption: encryptionInfo.SupportsEncryption,
Banner: encryptionInfo.Banner,
Options: encryptionInfo.Options,
}, nil
}
// GetTelnetNTLMInfo implements the Nmap telnet-ntlm-info.nse script functionality.
// This function uses the telnetmini library and SMB packet crafting functions to send
// MS-TNAP NTLM authentication requests with null credentials. It might work only on
// Microsoft Telnet servers.
// @example
// ```javascript
// const telnet = require('nuclei/telnet');
// const client = new telnet.TelnetClient();
// const ntlmInfo = client.GetTelnetNTLMInfo('acme.com', 23);
// log(toJSON(ntlmInfo));
// ```
func (c *TelnetClient) GetTelnetNTLMInfo(ctx context.Context, host string, port int) (*telnetmini.NTLMInfoResponse, error) {
executionId := ctx.Value("executionId").(string)
if !protocolstate.IsHostAllowed(executionId, host) {
return nil, protocolstate.ErrHostDenied.Msgf(host)
}
dialer := protocolstate.GetDialersWithId(executionId)
if dialer == nil {
return nil, fmt.Errorf("dialers not initialized for %s", executionId)
}
// Create TCP connection
conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
if err != nil {
return nil, err
}
defer func() {
_ = conn.Close()
}()
// Create telnet client using the telnetmini library
client := telnetmini.New(conn)
defer func() {
_ = client.Close()
}()
// Set timeout
_ = conn.SetDeadline(time.Now().Add(10 * time.Second))
// Use the MS-TNAP packet crafting functions from our telnetmini library
// Create MS-TNAP Login Packet (Option Command IS) as per Nmap script
tnapLoginPacket := telnetmini.CreateTNAPLoginPacket()
// Send the MS-TNAP login packet
_, err = conn.Write(tnapLoginPacket)
if err != nil {
return nil, fmt.Errorf("failed to send MS-TNAP login packet: %w", err)
}
// Read response data
buffer := make([]byte, 4096)
n, err := conn.Read(buffer)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if n == 0 {
return nil, fmt.Errorf("no response received")
}
// Parse NTLM response using our telnetmini library functions
response := buffer[:n]
// Use the parsing functions from our library instead of reimplementing
// This should use the NTLM parsing functions we added to telnetmini
ntlmInfo, err := telnetmini.ParseNTLMResponse(response)
if err != nil {
return nil, fmt.Errorf("failed to parse NTLM response: %w", err)
}
return ntlmInfo, nil
}

View File

@@ -0,0 +1,7 @@
// Package telnetmini is a library for interacting with Telnet servers.
// it supports
// - Basic Authentication phase (username/password)
// - Encryption detection via encryption negotiation packet
// - Minimal porting of https://github.com/nmap/nmap/blob/master/nselib/smbauth.lua SMB via NTLM negotiations
// (TNAP Login Packet + Raw NTLM response parsing)
package telnetmini

View File

@@ -0,0 +1,247 @@
package telnetmini
import (
"bytes"
"encoding/binary"
"fmt"
)
// NTLMInfoResponse represents the response from NTLM information gathering
// This matches exactly the output structure from the Nmap telnet-ntlm-info.nse script
type NTLMInfoResponse struct {
TargetName string // Target_Name from script
NetBIOSDomainName string // NetBIOS_Domain_Name from script
NetBIOSComputerName string // NetBIOS_Computer_Name from script
DNSDomainName string // DNS_Domain_Name from script
DNSComputerName string // DNS_Computer_Name from script
DNSTreeName string // DNS_Tree_Name from script
ProductVersion string // Product_Version from script
Timestamp uint64 // Raw timestamp for skew calculation
}
// ParseNTLMResponse parses the NTLM response to extract system information
// This implements the exact parsing logic from the Nmap telnet-ntlm-info.nse script
func ParseNTLMResponse(data []byte) (*NTLMInfoResponse, error) {
// Continue only if NTLMSSP response is returned.
// Verify that the response is terminated with Sub-option End values as various
// non Microsoft telnet implementations support NTLM but do not return valid data.
// This matches the script's: local data = string.match(response, "(NTLMSSP.*)\xff\xf0")
ntlmStart := bytes.Index(data, []byte("NTLMSSP"))
if ntlmStart == -1 {
return nil, fmt.Errorf("NTLMSSP signature not found in response")
}
// Find the end of NTLM data (Sub-option End: 0xFF 0xF0)
ntlmEnd := bytes.Index(data[ntlmStart:], []byte{0xFF, 0xF0})
if ntlmEnd == -1 {
return nil, fmt.Errorf("NTLM response not properly terminated with Sub-option End")
}
// Extract NTLM data (NTLMSSP.*\xff\xf0)
ntlmData := data[ntlmStart : ntlmStart+ntlmEnd]
// Check message type (should be 2 for Challenge)
if len(ntlmData) < 12 {
return nil, fmt.Errorf("NTLM response too short")
}
messageType := binary.LittleEndian.Uint32(ntlmData[8:12])
if messageType != 2 {
return nil, fmt.Errorf("expected NTLM challenge message, got type %d", messageType)
}
// Parse target name fields
targetNameLen := binary.LittleEndian.Uint16(ntlmData[12:14])
targetNameOffset := binary.LittleEndian.Uint32(ntlmData[16:20])
// Parse target info fields
targetInfoLen := binary.LittleEndian.Uint16(ntlmData[40:42])
targetInfoOffset := binary.LittleEndian.Uint32(ntlmData[44:48])
// Extract target name (Target Name will always be returned under any implementation)
var targetName string
if targetNameLen > 0 && int(targetNameOffset) < len(ntlmData) {
end := int(targetNameOffset) + int(targetNameLen)
if end <= len(ntlmData) {
targetName = string(ntlmData[targetNameOffset:end])
}
}
// Extract target info (contains detailed system information)
var ntlmInfo NTLMInfoResponse
ntlmInfo.TargetName = targetName
// Parse target info structure if available
if targetInfoLen > 0 && int(targetInfoOffset) < len(ntlmData) {
end := int(targetInfoOffset) + int(targetInfoLen)
if end <= len(ntlmData) {
parseTargetInfo(ntlmData[targetInfoOffset:end], &ntlmInfo)
}
}
return &ntlmInfo, nil
}
// CalculateTimestampSkew calculates the time skew from NTLM timestamp
// This implements the timestamp calculation from the Nmap script:
// local unixstamp = ntlm_decoded.timestamp // 10000000 - 11644473600
func CalculateTimestampSkew(ntlmTimestamp uint64) int64 {
if ntlmTimestamp == 0 {
return 0
}
// Convert 100ns clicks since 1/1/1601 to Unix timestamp
// Formula: (ntlmTimestamp / 10000000) - 11644473600
unixTimestamp := int64(ntlmTimestamp/10000000) - 11644473600
return unixTimestamp
}
// parseTargetInfo parses the NTLM target info structure to extract system details
func parseTargetInfo(data []byte, info *NTLMInfoResponse) {
// Target info is a series of type-length-value pairs
// Each entry starts with a 2-byte type and 2-byte length
for i := 0; i < len(data)-4; {
if i+4 > len(data) {
break
}
infoType := binary.LittleEndian.Uint16(data[i : i+2])
infoLen := binary.LittleEndian.Uint16(data[i+2 : i+4])
if i+4+int(infoLen) > len(data) {
break
}
infoData := data[i+4 : i+4+int(infoLen)]
switch infoType {
case 1: // NetBIOS Computer Name
// Display information returned & ignore responses with null values
if len(infoData) > 0 {
info.NetBIOSComputerName = string(infoData)
}
case 2: // NetBIOS Domain Name
if len(infoData) > 0 {
info.NetBIOSDomainName = string(infoData)
}
case 3: // DNS Computer Name (fqdn in script)
if len(infoData) > 0 {
info.DNSComputerName = string(infoData)
}
case 4: // DNS Domain Name
if len(infoData) > 0 {
info.DNSDomainName = string(infoData)
}
case 5: // DNS Tree Name (dns_forest_name in script)
if len(infoData) > 0 {
info.DNSTreeName = string(infoData)
}
case 6: // Timestamp - 64-bit number of 100ns clicks since 1/1/1601
if len(infoData) >= 8 {
info.Timestamp = binary.LittleEndian.Uint64(infoData)
}
case 7: // Single Host
// Skip single host
case 8: // Target Name (target_realm in script)
if len(infoData) > 0 {
info.TargetName = string(infoData)
}
case 9: // Channel Bindings
// Skip channel bindings
case 10: // Target Information
// Skip target information
case 11: // OS Version
if len(infoData) >= 8 {
major := uint8(infoData[0])
minor := uint8(infoData[1])
build := binary.LittleEndian.Uint16(infoData[2:4])
info.ProductVersion = fmt.Sprintf("%d.%d.%d", major, minor, build)
}
}
i += 4 + int(infoLen)
}
}
// CreateNTLMNegotiateBlob creates the NTLM negotiate blob with specific flags
// This matches the flags used in the Nmap script
func CreateNTLMNegotiateBlob() []byte {
var buf bytes.Buffer
// NTLMSSP signature
buf.WriteString("NTLMSSP")
buf.WriteByte(0x00)
// Message type (1 = Negotiate)
messageTypeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(messageTypeBytes, 1)
buf.Write(messageTypeBytes)
// Negotiate flags (matching Nmap script exactly)
flags := uint32(0x00000001 + // Negotiate Unicode
0x00000002 + // Negotiate OEM strings
0x00000004 + // Request Target
0x00000200 + // Negotiate NTLM
0x00008000 + // Negotiate Always Sign
0x00080000 + // Negotiate NTLM2 Key
0x20000000 + // Negotiate 128
0x80000000) // Negotiate 56
flagsBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(flagsBytes, flags)
buf.Write(flagsBytes)
// Domain name fields (empty for negotiate)
domainNameLenBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(domainNameLenBytes, 0)
buf.Write(domainNameLenBytes)
domainNameMaxLenBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(domainNameMaxLenBytes, 0)
buf.Write(domainNameMaxLenBytes)
domainNameOffsetBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(domainNameOffsetBytes, 0)
buf.Write(domainNameOffsetBytes)
// Workstation name fields (empty for negotiate)
workstationNameLenBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(workstationNameLenBytes, 0)
buf.Write(workstationNameLenBytes)
workstationNameMaxLenBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(workstationNameMaxLenBytes, 0)
buf.Write(workstationNameMaxLenBytes)
workstationNameOffsetBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(workstationNameOffsetBytes, 0)
buf.Write(workstationNameOffsetBytes)
// Version (empty for negotiate)
buf.Write(make([]byte, 8))
return buf.Bytes()
}
// CreateTNAPLoginPacket creates the MS-TNAP Login Packet (Option Command IS)
// This implements the exact packet structure from the Nmap script
func CreateTNAPLoginPacket() []byte {
var buf bytes.Buffer
// TNAP Option Command IS (0x01)
buf.WriteByte(0x01)
// Length (will be updated later)
buf.WriteByte(0x00)
// NTLM authentication blob
ntlmBlob := CreateNTLMNegotiateBlob()
// Update length
data := buf.Bytes()
data[1] = byte(len(ntlmBlob)) // Length of the NTLM blob
// Append NTLM blob
buf.Write(ntlmBlob)
return buf.Bytes()
}

781
pkg/utils/telnetmini/smb.go Normal file
View File

@@ -0,0 +1,781 @@
package telnetmini
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/Azure/go-ntlmssp"
)
// SMB constants for packet crafting
const (
// SMB Commands
SMB_COM_NEGOTIATE_PROTOCOL = 0x72
SMB_COM_SESSION_SETUP_ANDX = 0x73
SMB_COM_TREE_CONNECT_ANDX = 0x75
SMB_COM_NT_CREATE_ANDX = 0xA2
SMB_COM_READ_ANDX = 0x2E
SMB_COM_WRITE_ANDX = 0x2F
SMB_COM_CLOSE = 0x04
SMB_COM_TREE_DISCONNECT = 0x71
SMB_COM_LOGOFF_ANDX = 0x74
// SMB Flags
SMB_FLAGS_CANONICAL_PATHNAMES = 0x10
SMB_FLAGS_CASELESS_PATHNAMES = 0x08
SMB_FLAGS2_UNICODE_STRINGS = 0x8000
SMB_FLAGS2_ERRSTATUS = 0x4000
SMB_FLAGS2_READ_IF_EXECUTE = 0x2000
SMB_FLAGS2_32_BIT_ERRORS = 0x1000
SMB_FLAGS2_DFS = 0x0800
SMB_FLAGS2_EXTENDED_SECURITY = 0x0400
SMB_FLAGS2_REPARSE_PATH = 0x0200
SMB_FLAGS2_SMB_SECURITY_SIGNATURE = 0x0100
SMB_FLAGS2_SMB_SECURITY_SIGNATURE_REQUIRED = 0x0080
// SMB Security modes
SMB_SECURITY_SHARE = 0x00
SMB_SECURITY_USER = 0x01
SMB_SECURITY_DOMAIN = 0x02
// SMB Capabilities
SMB_CAP_EXTENDED_SECURITY = 0x80000000
SMB_CAP_COMPRESSED_DATA = 0x40000000
SMB_CAP_BULK_TRANSFER = 0x20000000
SMB_CAP_UNIX = 0x00800000
SMB_CAP_LARGE_READX = 0x00400000
SMB_CAP_LARGE_WRITEX = 0x00200000
SMB_CAP_INFOLEVEL_PASSTHRU = 0x00100000
SMB_CAP_DFS = 0x00080000
SMB_CAP_NT_FIND = 0x00040000
SMB_CAP_LOCK_AND_READ = 0x00020000
SMB_CAP_LEVEL_II_OPLOCKS = 0x00010000
SMB_CAP_STATUS32 = 0x00008000
SMB_CAP_RPC_REMOTE_APIS = 0x00004000
SMB_CAP_NT_SMBS = 0x00002000
// NTLM constants
NTLMSSP_NEGOTIATE_56 = 0x80000000
NTLMSSP_NEGOTIATE_KEY_EXCH = 0x40000000
NTLMSSP_NEGOTIATE_128 = 0x20000000
NTLMSSP_NEGOTIATE_VERSION = 0x02000000
NTLMSSP_NEGOTIATE_TARGET_INFO = 0x00800000
NTLMSSP_REQUEST_NON_NT_SESSION_KEY = 0x00400000
NTLMSSP_NEGOTIATE_IDENTIFY = 0x00100000
NTLMSSP_NEGOTIATE_EXTENDED_SESSION_SECURITY = 0x00080000
NTLMSSP_TARGET_TYPE_SERVER = 0x00020000
NTLMSSP_NEGOTIATE_ALWAYS_SIGN = 0x00008000
NTLMSSP_NEGOTIATE_NTLM = 0x00000200
NTLMSSP_NEGOTIATE_LM_KEY = 0x00000080
NTLMSSP_NEGOTIATE_DATAGRAM = 0x00000040
NTLMSSP_NEGOTIATE_SEAL = 0x00000020
NTLMSSP_NEGOTIATE_SIGN = 0x00000010
NTLMSSP_REQUEST_TARGET = 0x00000004
NTLMSSP_NEGOTIATE_UNICODE = 0x00000001
)
// SMBPacket represents a complete SMB packet
type SMBPacket struct {
NetBIOSHeader []byte
SMBHeader []byte
SMBData []byte
}
// SMBHeader represents the SMB header structure
type SMBHeader struct {
ProtocolID [4]byte // 0xFF, 'S', 'M', 'B'
Command byte
Status uint32
Flags byte
Flags2 uint16
PIDHigh uint16
Signature [8]byte
Reserved uint16
TreeID uint16
ProcessID uint16
UserID uint16
MultiplexID uint16
}
// CreateSMBPacket creates a complete SMB packet with NetBIOS header
func CreateSMBPacket(smbData []byte) *SMBPacket {
// Create NetBIOS header
netbiosHeader := make([]byte, 4)
netbiosHeader[0] = 0x00 // Message type (Session message)
netbiosHeader[1] = 0x00 // Padding
netbiosHeader[2] = 0x00 // Padding
// Calculate NetBIOS length (big-endian)
length := len(smbData)
netbiosHeader[3] = byte(length & 0xFF)
return &SMBPacket{
NetBIOSHeader: netbiosHeader,
SMBHeader: createSMBHeader(),
SMBData: smbData,
}
}
// createSMBHeader creates a standard SMB header
func createSMBHeader() []byte {
header := make([]byte, 32)
// Protocol ID: 0xFF, 'S', 'M', 'B'
header[0] = 0xFF
header[1] = 'S'
header[2] = 'M'
header[3] = 'B'
// Command (will be set by caller)
header[4] = 0x00
// Status (0 for requests)
binary.LittleEndian.PutUint32(header[5:9], 0)
// Flags
header[9] = SMB_FLAGS_CANONICAL_PATHNAMES
// Flags2
binary.LittleEndian.PutUint16(header[10:12], SMB_FLAGS2_UNICODE_STRINGS|SMB_FLAGS2_EXTENDED_SECURITY)
// PIDHigh, Signature, Reserved, TreeID, ProcessID, UserID, MultiplexID
// All set to 0 for new connections
binary.LittleEndian.PutUint16(header[12:14], 0) // PIDHigh
// Signature is 8 bytes of zeros
binary.LittleEndian.PutUint16(header[20:22], 0) // Reserved
binary.LittleEndian.PutUint16(header[22:24], 0) // TreeID
binary.LittleEndian.PutUint16(header[24:26], 0) // ProcessID
binary.LittleEndian.PutUint16(header[26:28], 0) // UserID
binary.LittleEndian.PutUint16(header[28:30], 0) // MultiplexID
return header
}
// CreateNegotiateProtocolPacket creates an SMB negotiate protocol packet
func CreateNegotiateProtocolPacket() []byte {
// Create SMB header
header := createSMBHeader()
header[4] = SMB_COM_NEGOTIATE_PROTOCOL
// Create negotiate protocol data
data := createNegotiateProtocolData()
// Combine header and data
packet := append(header, data...)
// Create complete packet with NetBIOS header
smbPacket := CreateSMBPacket(packet)
return smbPacket.Bytes()
}
// createNegotiateProtocolData creates the data portion of negotiate protocol packet
func createNegotiateProtocolData() []byte {
var buf bytes.Buffer
// Word count
_ = buf.WriteByte(0x00)
// Byte count
_ = buf.WriteByte(0x00)
// Dialect strings
dialects := []string{
"NT LM 0.12",
"SMB 2.002",
"SMB 2.???",
}
for _, dialect := range dialects {
_ = buf.WriteByte(byte(len(dialect)))
_, _ = buf.WriteString(dialect)
_ = buf.WriteByte(0x00)
}
// Update byte count
data := buf.Bytes()
data[1] = byte(len(data) - 2)
return data
}
// CreateSessionSetupPacket creates an SMB session setup packet
func CreateSessionSetupPacket(username, password, domain string, sessionKey uint64) []byte {
// Create SMB header
header := createSMBHeader()
header[4] = SMB_COM_SESSION_SETUP_ANDX
// Create session setup data
data := createSessionSetupData(username, password, domain, sessionKey)
// Combine header and data
packet := append(header, data...)
// Create complete packet with NetBIOS header
smbPacket := CreateSMBPacket(packet)
return smbPacket.Bytes()
}
// createSessionSetupData creates the data portion of session setup packet
func createSessionSetupData(username, password, domain string, sessionKey uint64) []byte {
var buf bytes.Buffer
// Word count
_ = buf.WriteByte(0x0D)
// AndXCommand (no chained command)
_ = buf.WriteByte(0xFF)
// AndXReserved
_ = buf.WriteByte(0x00)
// AndXOffset
_ = binary.Write(&buf, binary.LittleEndian, uint16(0))
// MaxBufferSize
_ = binary.Write(&buf, binary.LittleEndian, uint16(0xFFFF))
// MaxMpxCount
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x01))
// VcNumber
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x00))
// SessionKey
_ = binary.Write(&buf, binary.LittleEndian, uint32(sessionKey))
// CaseInsensitivePasswordLength
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(password)))
// CaseSensitivePasswordLength
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(password)))
// Reserved
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00))
// Capabilities
_ = binary.Write(&buf, binary.LittleEndian, uint32(SMB_CAP_EXTENDED_SECURITY|SMB_CAP_NT_SMBS))
// Byte count
_ = buf.WriteByte(0x00)
// CaseInsensitivePassword
_, _ = buf.WriteString(password)
// CaseSensitivePassword
_, _ = buf.WriteString(password)
// Account name
_, _ = buf.WriteString(username)
_ = buf.WriteByte(0x00)
// Primary domain
_, _ = buf.WriteString(domain)
_ = buf.WriteByte(0x00)
// Native OS
_, _ = buf.WriteString("Windows 2000 2195")
_ = buf.WriteByte(0x00)
// Native LAN Manager
_, _ = buf.WriteString("Windows 2000 5.0")
_ = buf.WriteByte(0x00)
// Update byte count
data := buf.Bytes()
data[len(data)-1] = byte(len(data) - 0x21) // 0x21 is the offset to the start of variable data
return data
}
// CreateTreeConnectPacket creates an SMB tree connect packet
func CreateTreeConnectPacket(shareName string, password string) []byte {
// Create SMB header
header := createSMBHeader()
header[4] = SMB_COM_TREE_CONNECT_ANDX
// Create tree connect data
data := createTreeConnectData(shareName, password)
// Combine header and data
packet := append(header, data...)
// Create complete packet with NetBIOS header
smbPacket := CreateSMBPacket(packet)
return smbPacket.Bytes()
}
// createTreeConnectData creates the data portion of tree connect packet
func createTreeConnectData(shareName, password string) []byte {
var buf bytes.Buffer
// Word count
_ = buf.WriteByte(0x04)
// AndXCommand (no chained command)
_ = buf.WriteByte(0xFF)
// AndXReserved
_ = buf.WriteByte(0x00)
// AndXOffset
_ = binary.Write(&buf, binary.LittleEndian, uint16(0))
// Flags
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x00))
// Password length
_ = buf.WriteByte(byte(len(password)))
// Byte count
_ = buf.WriteByte(0x00)
// Password
_, _ = buf.WriteString(password)
_ = buf.WriteByte(0x00)
// Tree
_, _ = buf.WriteString(shareName)
_ = buf.WriteByte(0x00)
// Service
_, _ = buf.WriteString("?????")
_ = buf.WriteByte(0x00)
// Update byte count
data := buf.Bytes()
data[7] = byte(len(data) - 0x0B) // 0x0B is the offset to the start of variable data
return data
}
// CreateNTCreatePacket creates an SMB NT create packet
func CreateNTCreatePacket(fileName string) []byte {
// Create SMB header
header := createSMBHeader()
header[4] = SMB_COM_NT_CREATE_ANDX
// Create NT create data
data := createNTCreateData(fileName)
// Combine header and data
packet := append(header, data...)
// Create complete packet with NetBIOS header
smbPacket := CreateSMBPacket(packet)
return smbPacket.Bytes()
}
// createNTCreateData creates the data portion of NT create packet
func createNTCreateData(fileName string) []byte {
var buf bytes.Buffer
// Word count
_ = buf.WriteByte(0x18)
// AndXCommand (no chained command)
_ = buf.WriteByte(0xFF)
// AndXReserved
_ = buf.WriteByte(0x00)
// AndXOffset
_ = binary.Write(&buf, binary.LittleEndian, uint16(0))
// Reserved
_ = buf.WriteByte(0x00)
// NameLength
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(fileName)))
// Flags
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
// RootDirectoryFID
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
// DesiredAccess
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
// AllocationSize
_ = binary.Write(&buf, binary.LittleEndian, uint64(0x0000000000000000))
// FileAttributes
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
// ShareAccess
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
// CreateDisposition
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000001)) // FILE_OPEN
// CreateOptions
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
// ImpersonationLevel
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000002)) // SecurityImpersonation
// SecurityFlags
_ = buf.WriteByte(0x00)
// Byte count
_ = buf.WriteByte(0x00)
// SecurityDescriptor
_ = buf.WriteByte(0x00)
// FileName
_, _ = buf.WriteString(fileName)
_ = buf.WriteByte(0x00)
// Update byte count
data := buf.Bytes()
data[0x3B] = byte(len(data) - 0x3C) // 0x3C is the offset to the start of variable data
return data
}
// Bytes returns the complete SMB packet as bytes
func (p *SMBPacket) Bytes() []byte {
var result bytes.Buffer
result.Write(p.NetBIOSHeader)
result.Write(p.SMBHeader)
result.Write(p.SMBData)
return result.Bytes()
}
// CreateNTLMNegotiatePacket creates an NTLM negotiate packet for SMB authentication
func CreateNTLMNegotiatePacket() []byte {
var buf bytes.Buffer
// NTLMSSP signature
_, _ = buf.WriteString("NTLMSSP")
_ = buf.WriteByte(0x00)
// Message type (1 = Negotiate)
_ = binary.Write(&buf, binary.LittleEndian, uint32(1))
// Negotiate flags
flags := uint32(NTLMSSP_NEGOTIATE_56 |
NTLMSSP_NEGOTIATE_128 |
NTLMSSP_NEGOTIATE_VERSION |
NTLMSSP_NEGOTIATE_TARGET_INFO |
NTLMSSP_NEGOTIATE_EXTENDED_SESSION_SECURITY |
NTLMSSP_TARGET_TYPE_SERVER |
NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
NTLMSSP_NEGOTIATE_NTLM |
NTLMSSP_NEGOTIATE_UNICODE)
_ = binary.Write(&buf, binary.LittleEndian, flags)
// Domain name fields (empty for negotiate)
_ = binary.Write(&buf, binary.LittleEndian, uint16(0)) // DomainNameLen
_ = binary.Write(&buf, binary.LittleEndian, uint16(0)) // DomainNameMaxLen
_ = binary.Write(&buf, binary.LittleEndian, uint32(0)) // DomainNameBufferOffset
// Workstation name fields (empty for negotiate)
_ = binary.Write(&buf, binary.LittleEndian, uint16(0)) // WorkstationNameLen
_ = binary.Write(&buf, binary.LittleEndian, uint16(0)) // WorkstationNameMaxLen
_ = binary.Write(&buf, binary.LittleEndian, uint32(0)) // WorkstationNameBufferOffset
// Version
_ = buf.WriteByte(0x05) // Major version
_ = buf.WriteByte(0x02) // Minor version
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x0A28)) // Build number
_, _ = buf.Write(make([]byte, 3)) // Reserved
_ = buf.WriteByte(0x0F) // NTLM revision
return buf.Bytes()
}
// CreateNTLMChallengePacket creates an NTLM challenge packet (for testing)
func CreateNTLMChallengePacket(challenge []byte, targetInfo []byte) []byte {
var buf bytes.Buffer
// NTLMSSP signature
_, _ = buf.WriteString("NTLMSSP")
_ = buf.WriteByte(0x00)
// Message type (2 = Challenge)
_ = binary.Write(&buf, binary.LittleEndian, uint32(2))
// Target name fields
_ = binary.Write(&buf, binary.LittleEndian, uint16(0))
_ = binary.Write(&buf, binary.LittleEndian, uint16(0))
_ = binary.Write(&buf, binary.LittleEndian, uint32(56)) // TargetNameBufferOffset
// Negotiate flags
flags := uint32(NTLMSSP_NEGOTIATE_56 |
NTLMSSP_NEGOTIATE_128 |
NTLMSSP_NEGOTIATE_VERSION |
NTLMSSP_NEGOTIATE_TARGET_INFO |
NTLMSSP_NEGOTIATE_EXTENDED_SESSION_SECURITY |
NTLMSSP_TARGET_TYPE_SERVER |
NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
NTLMSSP_NEGOTIATE_NTLM |
NTLMSSP_NEGOTIATE_UNICODE)
_ = binary.Write(&buf, binary.LittleEndian, flags)
// Challenge
_, _ = buf.Write(challenge)
// Reserved
_, _ = buf.Write(make([]byte, 8))
// Target info fields
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(targetInfo)))
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(targetInfo)))
_ = binary.Write(&buf, binary.LittleEndian, uint32(56)) // TargetInfoBufferOffset
// Version
_ = buf.WriteByte(0x05) // Major version
_ = buf.WriteByte(0x02) // Minor version
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x0A28)) // Build number
_, _ = buf.Write(make([]byte, 3)) // Reserved
_ = buf.WriteByte(0x0F) // NTLM revision
// Target info
_, _ = buf.Write(targetInfo)
return buf.Bytes()
}
// CreateNTLMAuthPacket creates an NTLM authenticate packet
func CreateNTLMAuthPacket(username, password, domain, workstation string, challenge []byte, lmResponse, ntResponse []byte) []byte {
var buf bytes.Buffer
// NTLMSSP signature
_, _ = buf.WriteString("NTLMSSP")
_ = buf.WriteByte(0x00)
// Message type (3 = Authenticate)
_ = binary.Write(&buf, binary.LittleEndian, uint32(3))
// Calculate offsets
baseOffset := uint32(72) // Header size (assuming Version is present)
lmOffset := baseOffset
ntOffset := lmOffset + uint32(len(lmResponse))
domainOffset := ntOffset + uint32(len(ntResponse))
userOffset := domainOffset + uint32(len(domain))
workOffset := userOffset + uint32(len(username))
sessionKeyOffset := workOffset + uint32(len(workstation))
// LM response fields
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(lmResponse)))
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(lmResponse)))
_ = binary.Write(&buf, binary.LittleEndian, uint32(lmOffset))
// NT response fields
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(ntResponse)))
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(ntResponse)))
_ = binary.Write(&buf, binary.LittleEndian, uint32(ntOffset))
// Domain name fields
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(domain)))
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(domain)))
_ = binary.Write(&buf, binary.LittleEndian, uint32(domainOffset))
// Username fields
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(username)))
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(username)))
_ = binary.Write(&buf, binary.LittleEndian, uint32(userOffset))
// Workstation name fields
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(workstation)))
_ = binary.Write(&buf, binary.LittleEndian, uint16(len(workstation)))
_ = binary.Write(&buf, binary.LittleEndian, uint32(workOffset))
// Encrypted random session key fields
_ = binary.Write(&buf, binary.LittleEndian, uint16(0))
_ = binary.Write(&buf, binary.LittleEndian, uint16(0))
_ = binary.Write(&buf, binary.LittleEndian, uint32(sessionKeyOffset))
// Negotiate flags
flags := uint32(NTLMSSP_NEGOTIATE_56 |
NTLMSSP_NEGOTIATE_128 |
NTLMSSP_NEGOTIATE_VERSION |
NTLMSSP_NEGOTIATE_TARGET_INFO |
NTLMSSP_NEGOTIATE_EXTENDED_SESSION_SECURITY |
NTLMSSP_TARGET_TYPE_SERVER |
NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
NTLMSSP_NEGOTIATE_NTLM |
NTLMSSP_NEGOTIATE_UNICODE)
_ = binary.Write(&buf, binary.LittleEndian, flags)
// Version
_ = buf.WriteByte(0x05) // Major version
_ = buf.WriteByte(0x02) // Minor version
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x0A28)) // Build number
_, _ = buf.Write(make([]byte, 3)) // Reserved
_ = buf.WriteByte(0x0F) // NTLM revision
// LM response
_, _ = buf.Write(lmResponse)
// NT response
_, _ = buf.Write(ntResponse)
// Domain name
_, _ = buf.WriteString(domain)
// Username
_, _ = buf.WriteString(username)
// Workstation name
_, _ = buf.WriteString(workstation)
return buf.Bytes()
}
// Helper function to create LM hash response
func CreateLMResponse(challenge []byte, password string) []byte {
// Create a minimal Type 2 challenge packet to satisfy ntlmssp
type2 := CreateNTLMChallengePacket(challenge, []byte{})
// Generate Type 3 authenticate packet
// We use empty username/domain as we only need the hash response
authMsg, err := ntlmssp.NewAuthenticateMessage(type2, "", password, nil)
if err != nil {
return nil
}
// Parse the response to extract LM response
// LM Response Len is at offset 12 (2 bytes)
// LM Response Offset is at offset 16 (4 bytes)
if len(authMsg) < 20 {
return nil
}
lmLen := binary.LittleEndian.Uint16(authMsg[12:14])
lmOffset := binary.LittleEndian.Uint32(authMsg[16:20])
if int(lmOffset)+int(lmLen) > len(authMsg) {
return nil
}
return authMsg[lmOffset : lmOffset+uint32(lmLen)]
}
// Helper function to create NT hash response
func CreateNTResponse(challenge []byte, password string) []byte {
// Create a minimal Type 2 challenge packet to satisfy ntlmssp
type2 := CreateNTLMChallengePacket(challenge, []byte{})
// Generate Type 3 authenticate packet
authMsg, err := ntlmssp.NewAuthenticateMessage(type2, "", password, nil)
if err != nil {
return nil
}
// Parse the response to extract NT response
// NT Response Len is at offset 20 (2 bytes)
// NT Response Offset is at offset 24 (4 bytes)
if len(authMsg) < 28 {
return nil
}
ntLen := binary.LittleEndian.Uint16(authMsg[20:22])
ntOffset := binary.LittleEndian.Uint32(authMsg[24:28])
if int(ntOffset)+int(ntLen) > len(authMsg) {
return nil
}
return authMsg[ntOffset : ntOffset+uint32(ntLen)]
}
// CreateSMBv2NegotiatePacket creates an SMBv2 negotiate protocol packet
func CreateSMBv2NegotiatePacket() []byte {
var buf bytes.Buffer
// SMB2 header
_ = buf.WriteByte(0xFE) // Protocol ID
_, _ = buf.WriteString("SMB")
_ = buf.WriteByte(0x00) // Protocol ID
// Command (Negotiate Protocol)
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x0000))
// Status
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
// Flags
_ = buf.WriteByte(0x00)
// Next command
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x0000))
// Message ID
_ = binary.Write(&buf, binary.LittleEndian, uint64(0x0000000000000001))
// Reserved
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
// Tree ID
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
// Session ID
_ = binary.Write(&buf, binary.LittleEndian, uint64(0x0000000000000000))
// Signature
_, _ = buf.Write(make([]byte, 16))
// Structure size
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x24))
// Dialect count
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x0001))
// Security mode
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x0001))
// Reserved
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x0000))
// Capabilities
_ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000001))
// Client GUID
_, _ = buf.Write(make([]byte, 16))
// Client start time
_ = binary.Write(&buf, binary.LittleEndian, uint64(0x0000000000000000))
// Dialects
_ = binary.Write(&buf, binary.LittleEndian, uint16(0x0311)) // SMB 3.1.1
return buf.Bytes()
}
// ParseSMBResponse parses an SMB response packet
func ParseSMBResponse(data []byte) (*SMBHeader, error) {
if len(data) < 32 {
return nil, fmt.Errorf("SMB response too short: %d bytes", len(data))
}
header := &SMBHeader{}
// Check protocol ID
if data[0] != 0xFF || data[1] != 'S' || data[2] != 'M' || data[3] != 'B' {
return nil, fmt.Errorf("invalid SMB protocol ID")
}
// Parse header fields
header.Command = data[4]
header.Status = binary.LittleEndian.Uint32(data[5:9])
header.Flags = data[9]
header.Flags2 = binary.LittleEndian.Uint16(data[10:12])
header.PIDHigh = binary.LittleEndian.Uint16(data[12:14])
copy(header.Signature[:], data[14:22])
header.Reserved = binary.LittleEndian.Uint16(data[22:24])
header.TreeID = binary.LittleEndian.Uint16(data[24:26])
header.ProcessID = binary.LittleEndian.Uint16(data[26:28])
header.UserID = binary.LittleEndian.Uint16(data[28:30])
header.MultiplexID = binary.LittleEndian.Uint16(data[30:32])
return header, nil
}

View File

@@ -0,0 +1,372 @@
// telnetmini.go
// Minimal Telnet helper for authentication + simple I/O over an existing or new connection.
package telnetmini
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"net"
"strings"
"time"
)
// Telnet protocol constants
const (
IAC = 255 // Interpret As Command
WILL = 251 // Will
WONT = 252 // Won't
DO = 253 // Do
DONT = 254 // Don't
SB = 250 // Subnegotiation Begin
SE = 240 // Subnegotiation End
ENCRYPT = 38 // Encryption option (0x26)
)
// EncryptionInfo contains information about telnet encryption support
type EncryptionInfo struct {
SupportsEncryption bool
Banner string
Options map[int][]int
}
// Client wraps a Telnet connection with tiny helpers.
type Client struct {
Conn net.Conn
rd *bufio.Reader
wr *bufio.Writer
LoginPrompts []string // matched case-insensitively
UserPrompts []string // alternative to LoginPrompts; if empty, LoginPrompts used for username step
PasswordPrompts []string
FailBanners []string // e.g., "login incorrect", "authentication failed"
ShellPrompts []string // e.g., "$ ", "# ", "> "
ReadCapBytes int // safety cap while scanning (default 64 KiB)
}
// Defaults sets reasonable prompt patterns if none provided.
func (c *Client) Defaults() {
if c.ReadCapBytes == 0 {
c.ReadCapBytes = 64 * 1024
}
if len(c.LoginPrompts) == 0 {
c.LoginPrompts = []string{"login:", "username:"}
}
if len(c.PasswordPrompts) == 0 {
c.PasswordPrompts = []string{"password:"}
}
if len(c.FailBanners) == 0 {
c.FailBanners = []string{"login incorrect", "authentication failed", "login failed"}
}
if len(c.ShellPrompts) == 0 {
c.ShellPrompts = []string{"$ ", "# ", "> "}
}
if len(c.UserPrompts) == 0 {
c.UserPrompts = c.LoginPrompts
}
}
// New wraps an existing net.Conn.
func New(conn net.Conn) *Client {
c := &Client{
Conn: conn,
rd: bufio.NewReader(conn),
wr: bufio.NewWriter(conn),
}
c.Defaults()
return c
}
// Close closes the underlying connection.
func (c *Client) Close() error {
return c.Conn.Close()
}
// DetectEncryption detects if a telnet server supports encryption.
// Based on Nmap's telnet-encryption.nse script functionality.
// WARNING: The connection becomes unusable after calling this function
// due to the encryption negotiation packets sent.
func DetectEncryption(conn net.Conn, timeout time.Duration) (*EncryptionInfo, error) {
if timeout == 0 {
timeout = 7 * time.Second
}
// Set connection timeout
_ = conn.SetDeadline(time.Now().Add(timeout))
// Send encryption negotiation packet (based on Nmap script)
// FF FD 26 FF FB 26 = IAC DO ENCRYPT IAC WILL ENCRYPT
encryptionPacket := []byte{IAC, DO, ENCRYPT, IAC, WILL, ENCRYPT}
_, err := conn.Write(encryptionPacket)
if err != nil {
return nil, fmt.Errorf("failed to send encryption packet: %w", err)
}
// Process server responses
options := make(map[int][]int)
supportsEncryption := false
banner := ""
// Read responses until we get encryption info or timeout
for {
_ = conn.SetReadDeadline(time.Now().Add(1 * time.Second))
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
// Timeout or connection closed, break
break
}
if n > 0 {
data := buffer[:n]
// Check if this contains banner text (non-IAC bytes)
for _, b := range data {
if b != IAC {
banner += string(b)
}
}
// Process telnet options
encrypted, opts := processTelnetOptions(data)
if encrypted {
supportsEncryption = true
}
// Merge options
for opt, cmds := range opts {
if options[opt] == nil {
options[opt] = make([]int, 0)
}
options[opt] = append(options[opt], cmds...)
}
// Check if we have encryption info
if cmds, exists := options[ENCRYPT]; exists {
for _, cmd := range cmds {
if cmd == WILL || cmd == DO {
supportsEncryption = true
break
}
}
}
}
}
return &EncryptionInfo{
SupportsEncryption: supportsEncryption,
Banner: banner,
Options: options,
}, nil
}
// processTelnetOptions processes telnet protocol options and returns encryption support status
func processTelnetOptions(data []byte) (bool, map[int][]int) {
options := make(map[int][]int)
supportsEncryption := false
for i := 0; i < len(data); i++ {
if data[i] == IAC && i+2 < len(data) {
cmd := data[i+1]
option := data[i+2]
// Initialize option slice if not exists
optInt := int(option)
if options[optInt] == nil {
options[optInt] = make([]int, 0)
}
options[optInt] = append(options[optInt], int(cmd))
// Check for encryption support
if option == ENCRYPT && (cmd == WILL || cmd == DO) {
supportsEncryption = true
}
// Handle subnegotiation
if cmd == SB {
// Skip until SE
for j := i + 3; j < len(data); j++ {
if data[j] == IAC && j+1 < len(data) && data[j+1] == SE {
i = j + 1
break
}
}
} else {
i += 2 // Skip command and option
}
}
}
return supportsEncryption, options
}
// Auth performs a minimal Telnet username/password interaction.
// It waits for a username/login prompt, sends username, waits for a password prompt,
// sends password, and then looks for fail banners or shell prompts.
// A timeout should be enforced via ctx.
func (c *Client) Auth(ctx context.Context, username, password string) error {
// Wait for username/login prompt
if _, _, err := c.readUntil(ctx, c.UserPrompts...); err != nil {
return fmt.Errorf("waiting for login/username prompt: %w", err)
}
if err := c.writeLine(ctx, username); err != nil {
return fmt.Errorf("sending username: %w", err)
}
// Wait for password prompt
if _, _, err := c.readUntil(ctx, c.PasswordPrompts...); err != nil {
return fmt.Errorf("waiting for password prompt: %w", err)
}
if err := c.writeLine(ctx, password); err != nil {
return fmt.Errorf("sending password: %w", err)
}
// Post-auth: look quickly for explicit failure, else accept shell prompt / silence.
match, got, err := c.readUntil(ctx,
append(append([]string{}, c.FailBanners...), c.ShellPrompts...)...,
)
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("post-auth read: %s (got: %s)", preview(got, 200), err)
}
low := strings.ToLower(match)
for _, fb := range c.FailBanners {
if low == strings.ToLower(fb) {
return errors.New("authentication failed")
}
}
// success (matched a shell prompt or timed out without explicit failure)
return nil
}
// Exec sends a command followed by CRLF and returns text captured until one of
// the provided prompts appears (typically your shell prompt). Provide a deadline via ctx.
func (c *Client) Exec(ctx context.Context, command string, until ...string) (string, error) {
if err := c.writeLine(ctx, command); err != nil {
return "", err
}
_, out, err := c.readUntil(ctx, until...)
return out, err
}
// --- internals ---
// writeLine writes s + CRLF and flushes.
func (c *Client) writeLine(ctx context.Context, s string) error {
c.setDeadlineFromCtx(ctx, true)
if _, err := io.WriteString(c.wr, s+"\r\n"); err != nil {
return err
}
return c.wr.Flush()
}
// readUntil scans bytes, handles minimal Telnet IAC negotiation, and returns when any needle appears.
func (c *Client) readUntil(ctx context.Context, needles ...string) (matched string, bufStr string, err error) {
if len(needles) == 0 {
return "", "", errors.New("readUntil: no needles provided")
}
c.setDeadlineFromCtx(ctx, false)
lowNeedles := make([]string, len(needles))
for i, n := range needles {
lowNeedles[i] = strings.ToLower(n)
}
var b strings.Builder
tmp := make([]byte, 1)
// Maximum iteration counter to prevent infinite loops
maxIterations := 20
iterationCount := 0
for {
iterationCount++
// if we have iterated more than maxIterations, return
if iterationCount > maxIterations {
return "", b.String(), nil
}
// honor context deadline on every read
c.setDeadlineFromCtx(ctx, false)
_, err := c.rd.Read(tmp)
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
return "", b.String(), context.DeadlineExceeded
}
return "", b.String(), err
}
// Telnet IAC (Interpret As Command)
if tmp[0] == 255 { // IAC
cmd, err := c.rd.ReadByte()
if err != nil {
return "", b.String(), err
}
switch cmd {
case 251, 252, 253, 254: // WILL, WONT, DO, DONT
opt, err := c.rd.ReadByte()
if err != nil {
return "", b.String(), err
}
// Politely refuse everything: DONT to WILL; WONT to DO.
var reply []byte
if cmd == 251 { // WILL
reply = []byte{255, 254, opt} // DONT
}
if cmd == 253 { // DO
reply = []byte{255, 252, opt} // WONT
}
if len(reply) > 0 {
c.setDeadlineFromCtx(ctx, true)
_, _ = c.wr.Write(reply)
_ = c.wr.Flush()
}
case 250: // SB (subnegotiation): skip until SE
for {
bb, err := c.rd.ReadByte()
if err != nil {
return "", b.String(), err
}
if bb == 255 {
if se, err := c.rd.ReadByte(); err == nil && se == 240 { // SE
break
}
}
}
default:
// NOP for other commands (IAC NOP, GA, etc.)
}
continue
}
// regular data byte
b.WriteByte(tmp[0])
lower := strings.ToLower(b.String())
for i, n := range lowNeedles {
if strings.Contains(lower, n) {
return needles[i], b.String(), nil
}
}
if b.Len() > c.ReadCapBytes {
return "", b.String(), errors.New("prompt not found (read cap reached)")
}
}
}
func (c *Client) setDeadlineFromCtx(ctx context.Context, write bool) {
if ctx == nil {
return
}
if dl, ok := ctx.Deadline(); ok {
_ = c.Conn.SetReadDeadline(dl)
if write {
_ = c.Conn.SetWriteDeadline(dl)
}
}
}
func preview(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}