diff --git a/cmd/integration-test/javascript.go b/cmd/integration-test/javascript.go index c85a8758a..6cc71e91a 100644 --- a/cmd/integration-test/javascript.go +++ b/cmd/integration-test/javascript.go @@ -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) + } } diff --git a/go.mod b/go.mod index 3b6928f21..c2b712284 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 7db9b0b94..3e2e6fe0c 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/integration_tests/protocols/javascript/telnet-auth-test.yaml b/integration_tests/protocols/javascript/telnet-auth-test.yaml new file mode 100644 index 000000000..79d1c844c --- /dev/null +++ b/integration_tests/protocols/javascript/telnet-auth-test.yaml @@ -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 diff --git a/pkg/js/generated/go/libtelnet/telnet.go b/pkg/js/generated/go/libtelnet/telnet.go index a9b50a5fb..a51a54b9c 100644 --- a/pkg/js/generated/go/libtelnet/telnet.go +++ b/pkg/js/generated/go/libtelnet/telnet.go @@ -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() } diff --git a/pkg/js/generated/ts/telnet.ts b/pkg/js/generated/ts/telnet.ts index cd49c2078..39513bd91 100755 --- a/pkg/js/generated/ts/telnet.ts +++ b/pkg/js/generated/ts/telnet.ts @@ -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, +} + diff --git a/pkg/js/libs/telnet/telnet.go b/pkg/js/libs/telnet/telnet.go index db220309f..86006bcb0 100644 --- a/pkg/js/libs/telnet/telnet.go +++ b/pkg/js/libs/telnet/telnet.go @@ -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 +} diff --git a/pkg/utils/telnetmini/doc.go b/pkg/utils/telnetmini/doc.go new file mode 100644 index 000000000..c63a495c8 --- /dev/null +++ b/pkg/utils/telnetmini/doc.go @@ -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 diff --git a/pkg/utils/telnetmini/ntlm.go b/pkg/utils/telnetmini/ntlm.go new file mode 100644 index 000000000..b4105b52b --- /dev/null +++ b/pkg/utils/telnetmini/ntlm.go @@ -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() +} diff --git a/pkg/utils/telnetmini/smb.go b/pkg/utils/telnetmini/smb.go new file mode 100644 index 000000000..12d150564 --- /dev/null +++ b/pkg/utils/telnetmini/smb.go @@ -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 +} diff --git a/pkg/utils/telnetmini/telnet.go b/pkg/utils/telnetmini/telnet.go new file mode 100644 index 000000000..6c65cdb61 --- /dev/null +++ b/pkg/utils/telnetmini/telnet.go @@ -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] + "..." +}