mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-01-31 15:53:10 +08:00
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:
@@ -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
2
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
28
integration_tests/protocols/javascript/telnet-auth-test.yaml
Normal file
28
integration_tests/protocols/javascript/telnet-auth-test.yaml
Normal 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
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
7
pkg/utils/telnetmini/doc.go
Normal file
7
pkg/utils/telnetmini/doc.go
Normal 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
|
||||
247
pkg/utils/telnetmini/ntlm.go
Normal file
247
pkg/utils/telnetmini/ntlm.go
Normal 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
781
pkg/utils/telnetmini/smb.go
Normal 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
|
||||
}
|
||||
372
pkg/utils/telnetmini/telnet.go
Normal file
372
pkg/utils/telnetmini/telnet.go
Normal 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] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user