mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
490 lines
14 KiB
Go
490 lines
14 KiB
Go
package tpkt
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/shadow1ng/fscan/libs/grdp/core"
|
|
"github.com/shadow1ng/fscan/libs/grdp/emission"
|
|
"github.com/shadow1ng/fscan/libs/grdp/glog"
|
|
"github.com/shadow1ng/fscan/libs/grdp/protocol/nla"
|
|
)
|
|
|
|
// take idea from https://github.com/Madnikulin50/gordp
|
|
|
|
/**
|
|
* Type of tpkt packet
|
|
* Fastpath is use to shortcut RDP stack
|
|
* @see http://msdn.microsoft.com/en-us/library/cc240621.aspx
|
|
* @see http://msdn.microsoft.com/en-us/library/cc240589.aspx
|
|
*/
|
|
const (
|
|
FASTPATH_ACTION_FASTPATH = 0x0
|
|
FASTPATH_ACTION_X224 = 0x3
|
|
)
|
|
|
|
/**
|
|
* TPKT layer of rdp stack
|
|
*/
|
|
type TPKT struct {
|
|
emission.Emitter
|
|
Conn *core.SocketLayer
|
|
ntlm *nla.NTLMv2
|
|
secFlag byte
|
|
lastShortLength int
|
|
fastPathListener core.FastPathListener
|
|
ntlmSec *nla.NTLMv2Security
|
|
nlaAuthOnly bool // NLA仅验证模式:验证成功后立即断开,不建立会话
|
|
}
|
|
|
|
var OsVersion = map[string]string{
|
|
"3.10.511": "Windows NT 3.1",
|
|
"3.50.807": "Windows NT 3.5",
|
|
"3.10.528": "Windows NT 3.1, Service Pack 3",
|
|
"3.51.1057": "Windows NT 3.51",
|
|
"4.00.950": "Windows 95",
|
|
"4.0.1381": "Windows NT 4.0",
|
|
"4.10.1998": "Windows 98",
|
|
"4.10.2222": "Windows 98 Second Edition (SE)",
|
|
"5.0.2195": "Windows 2000",
|
|
"4.90.3000": "Windows Me",
|
|
"5.1.2600": "Windows XP/Windows XP, Service Pack 3",
|
|
"5.1.2600.1105": "Windows XP, Service Pack 1",
|
|
"5.2.3790": "Windows Server 2003/Windows Server 2003 R2/Windows Server 2003, Service Pack 2",
|
|
"5.1.2600.2180": "Windows XP, Service Pack 2",
|
|
"5.2.3790.1180": "Windows Server 2003, Service Pack 1",
|
|
"6.0.6000": "Windows Vista",
|
|
"5.2.4500": "Windows Home Server",
|
|
"6.0.6001": "Windows Vista, Service Pack 1/Windows Server 2008",
|
|
"6.0.6002": "Windows Vista, Service Pack 2/Windows Server 2008, Service Pack 2",
|
|
"6.1.7600": "Windows 7/Windows Server 2008 R2",
|
|
"6.1.7601": "Windows 7, Service Pack 1/Windows Server 2008 R2, Service Pack 1",
|
|
"6.1.8400": "Windows Home Server 2011",
|
|
"6.2.9200": "Windows Server 2012/Windows 8",
|
|
"6.3.9600": "Windows 8.1/Windows Server 2012 R2",
|
|
"10.0.10240": "Windows 10, Version 1507",
|
|
"10.0.10586": "Windows 10, Version 1511",
|
|
"10.0.14393": "Windows 10, Version 1607/Windows Server 2016, Version 1607",
|
|
"10.0.15063": "Windows 10, Version 1703",
|
|
"10.0.16299": "Windows 10, Version 1709",
|
|
"10.0.17134": "Windows 10, Version 1803",
|
|
"10.0.17763": "Windows Server 2019, Version 1809/Windows 10, Version 1809",
|
|
"6.0.6003": "Windows Server 2008, Service Pack 2, Rollup KB4489887",
|
|
"10.0.18362": "Windows 10, Version 1903",
|
|
"10.0.18363": "Windows 10, Version 1909/Windows Server, Version 1909",
|
|
"10.0.19041": "Windows 10, Version 2004/Windows Server, Version 2004",
|
|
"10.0.19042": "Windows 10, Version 20H2/Windows Server, Version 20H2",
|
|
"10.0.19043": "Windows 10, Version 21H1",
|
|
"10.0.20348": "Windows Server 2022",
|
|
"10.0.22000": "Windows 11, Version 21H2",
|
|
"10.0.19044": "Windows 10, Version 21H2",
|
|
}
|
|
|
|
func New(s *core.SocketLayer, ntlm *nla.NTLMv2) *TPKT {
|
|
t := &TPKT{
|
|
Emitter: *emission.NewEmitter(),
|
|
Conn: s,
|
|
secFlag: 0,
|
|
ntlm: ntlm}
|
|
core.StartReadBytes(2, s, t.recvHeader)
|
|
return t
|
|
}
|
|
|
|
func (t *TPKT) StartTLS() error {
|
|
return t.Conn.StartTLS()
|
|
}
|
|
|
|
// SetNLAAuthOnly 设置NLA仅验证模式
|
|
// 启用后,NLA认证成功即返回,不发送credentials建立会话,不会挤掉已登录用户
|
|
func (t *TPKT) SetNLAAuthOnly(authOnly bool) {
|
|
t.nlaAuthOnly = authOnly
|
|
}
|
|
|
|
func (t *TPKT) StartNLA() error {
|
|
err := t.StartTLS()
|
|
if err != nil {
|
|
glog.Info("start tls failed", err)
|
|
return err
|
|
}
|
|
req := nla.EncodeDERTRequest([]nla.Message{t.ntlm.GetNegotiateMessage()}, nil, nil)
|
|
_, err = t.Conn.Write(req)
|
|
if err != nil {
|
|
glog.Info("send NegotiateMessage", err)
|
|
return err
|
|
}
|
|
|
|
resp := make([]byte, 1024)
|
|
n, err := t.Conn.Read(resp)
|
|
if err != nil {
|
|
return fmt.Errorf("read %s", err)
|
|
} else {
|
|
glog.Debug("StartNLA Read success")
|
|
}
|
|
return t.recvChallenge(resp[:n])
|
|
}
|
|
|
|
func (t *TPKT) recvChallenge(data []byte) error {
|
|
//own add
|
|
glog.Debug("start recv challenge......")
|
|
info := make(map[string]any)
|
|
type NTLMChallenge struct {
|
|
Signature [8]byte
|
|
MessageType uint32
|
|
TargetNameLen uint16
|
|
TargetNameMaxLen uint16
|
|
TargetNameBufferOffset uint32
|
|
NegotiateFlags uint32
|
|
ServerChallenge uint64
|
|
Reserved uint64
|
|
TargetInfoLen uint16
|
|
TargetInfoMaxLen uint16
|
|
TargetInfoBufferOffset uint32
|
|
Version [8]byte
|
|
// Payload (variable)
|
|
}
|
|
var challengeLen = 56
|
|
|
|
challengeStartOffset := bytes.Index(data, []byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0})
|
|
if challengeStartOffset == -1 {
|
|
}
|
|
if len(data) < challengeStartOffset+challengeLen {
|
|
return nil
|
|
}
|
|
var responseData NTLMChallenge
|
|
response := data[challengeStartOffset:]
|
|
responseBuf := bytes.NewBuffer(response)
|
|
err := binary.Read(responseBuf, binary.LittleEndian, &responseData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Check if valid NTLM challenge response message structure
|
|
if responseData.MessageType != 0x00000002 ||
|
|
responseData.Reserved != 0 ||
|
|
!reflect.DeepEqual(responseData.Version[4:], []byte{0, 0, 0, 0xF}) {
|
|
return nil
|
|
}
|
|
|
|
// Parse: Version
|
|
type version struct {
|
|
MajorVersion byte
|
|
MinorVersion byte
|
|
BuildNumber uint16
|
|
}
|
|
var versionData version
|
|
versionBuf := bytes.NewBuffer(responseData.Version[:4])
|
|
err = binary.Read(versionBuf, binary.LittleEndian, &versionData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ProductVersion := fmt.Sprintf("%d.%d.%d", versionData.MajorVersion,
|
|
versionData.MinorVersion,
|
|
versionData.BuildNumber)
|
|
glog.Debug("get product version: Windows", ProductVersion)
|
|
info["ProductVersion"] = ProductVersion
|
|
|
|
v, ok := OsVersion[ProductVersion]
|
|
if ok {
|
|
info["OsVerion"] = v
|
|
glog.Debug("get os version:", v)
|
|
} else {
|
|
if versionData.BuildNumber >= 22000 {
|
|
info["OsVerion"] = fmt.Sprintf("Windows 11, version:%s", ProductVersion)
|
|
} else {
|
|
info["OsVerion"] = fmt.Sprintf("Windows %s", ProductVersion)
|
|
}
|
|
}
|
|
|
|
// Parse: TargetName
|
|
targetNameLen := int(responseData.TargetNameLen)
|
|
if targetNameLen > 0 {
|
|
startIdx := int(responseData.TargetNameBufferOffset)
|
|
endIdx := startIdx + targetNameLen
|
|
targetName := strings.ReplaceAll(string(response[startIdx:endIdx]), "\x00", "")
|
|
info["TargetName"] = targetName
|
|
glog.Debug("target Name = ", targetName)
|
|
}
|
|
|
|
// Parse: TargetInfo
|
|
AvIDMap := map[uint16]string{
|
|
1: "NetBIOSComputerName",
|
|
2: "NetBIOSDomainName",
|
|
3: "FQDN", // DNS Computer Name
|
|
4: "DNSDomainName",
|
|
5: "DNSTreeName",
|
|
7: "Timestamp",
|
|
9: "MsvAvTargetName",
|
|
}
|
|
|
|
type AVPair struct {
|
|
AvID uint16
|
|
AvLen uint16
|
|
// Value (variable)
|
|
}
|
|
var avPairLen = 4
|
|
targetInfoLen := int(responseData.TargetInfoLen)
|
|
if targetInfoLen > 0 {
|
|
startIdx := int(responseData.TargetInfoBufferOffset)
|
|
if startIdx+targetInfoLen > len(response) {
|
|
return fmt.Errorf("Invalid TargetInfoLen value")
|
|
}
|
|
var avPair AVPair
|
|
avPairBuf := bytes.NewBuffer(response[startIdx : startIdx+avPairLen])
|
|
err = binary.Read(avPairBuf, binary.LittleEndian, &avPair)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
currIdx := startIdx
|
|
for avPair.AvID != 0 {
|
|
if field, exists := AvIDMap[avPair.AvID]; exists {
|
|
var value string
|
|
r := response[currIdx+avPairLen : currIdx+avPairLen+int(avPair.AvLen)]
|
|
if avPair.AvID == 7 {
|
|
unixStamp := binary.LittleEndian.Uint64(r)/10000000 - 11644473600
|
|
tm := time.Unix(int64(unixStamp), 0)
|
|
value = tm.Format("2006-01-02 15:04:05")
|
|
} else {
|
|
value = strings.ReplaceAll(string(r), "\x00", "")
|
|
}
|
|
info[field] = value
|
|
}
|
|
currIdx += avPairLen + int(avPair.AvLen)
|
|
if currIdx+avPairLen > startIdx+targetInfoLen {
|
|
return fmt.Errorf("Invalid AV_PAIR list")
|
|
}
|
|
avPairBuf = bytes.NewBuffer(response[currIdx : currIdx+avPairLen])
|
|
err = binary.Read(avPairBuf, binary.LittleEndian, &avPair)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
glog.Info("get os info by NLA done !")
|
|
glog.Info("=======================================")
|
|
for key, value := range info {
|
|
glog.Info(key, ":", value)
|
|
}
|
|
glog.Info("=======================================")
|
|
|
|
//判断是否存在windows域
|
|
if netBiosDomainName, exists := info["NetBIOSDomainName"]; exists {
|
|
if netBiosComputerName, exists := info["NetBIOSComputerName"]; exists {
|
|
if netBiosDomainName == netBiosComputerName {
|
|
info["DNSDomainName"], info["NetBIOSDomainName"] = "WORKGROUP", "WORKGROUP"
|
|
//delete(info, "FQDN")
|
|
} else {
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Emit("os_info", info)
|
|
|
|
// end
|
|
glog.Trace("recvChallenge", hex.EncodeToString(data))
|
|
tsreq, err := nla.DecodeDERTRequest(data)
|
|
if err != nil {
|
|
glog.Info("DecodeDERTRequest", err)
|
|
return err
|
|
}
|
|
glog.Debugf("tsreq:%+v", tsreq)
|
|
// get pubkey
|
|
pubkey, err := t.Conn.TlsPubKey()
|
|
glog.Debugf("pubkey=%+v", pubkey)
|
|
|
|
authMsg, ntlmSec := t.ntlm.GetAuthenticateMessage(tsreq.NegoTokens[0].Data)
|
|
t.ntlmSec = ntlmSec
|
|
|
|
encryptPubkey := ntlmSec.GssEncrypt(pubkey)
|
|
req := nla.EncodeDERTRequest([]nla.Message{authMsg}, nil, encryptPubkey)
|
|
_, err = t.Conn.Write(req)
|
|
if err != nil {
|
|
glog.Info("send AuthenticateMessage", err)
|
|
return err
|
|
}
|
|
resp := make([]byte, 1024)
|
|
n, err := t.Conn.Read(resp)
|
|
if err != nil {
|
|
glog.Error("Read:", err)
|
|
return fmt.Errorf("read %s", err)
|
|
} else {
|
|
glog.Debug("recvChallenge Read success")
|
|
}
|
|
return t.recvPubKeyInc(resp[:n])
|
|
}
|
|
|
|
// ErrNLAAuthSuccess 表示NLA仅验证模式下认证成功(非真正错误)
|
|
var ErrNLAAuthSuccess = fmt.Errorf("NLA_AUTH_SUCCESS")
|
|
|
|
func (t *TPKT) recvPubKeyInc(data []byte) error {
|
|
glog.Trace("recvPubKeyInc", hex.EncodeToString(data))
|
|
|
|
tsreq, err := nla.DecodeDERTRequest(data)
|
|
if err != nil {
|
|
glog.Info("DecodeDERTRequest", err)
|
|
return err
|
|
}
|
|
|
|
// 检查服务器是否返回错误码(认证失败)
|
|
// 常见错误码: 0xC000006D = STATUS_LOGON_FAILURE (密码错误)
|
|
if tsreq.ErrorCode != 0 {
|
|
glog.Error("NLA authentication failed with error code:", tsreq.ErrorCode)
|
|
return fmt.Errorf("NLA auth failed: error code %d (0x%X)", tsreq.ErrorCode, uint32(tsreq.ErrorCode))
|
|
}
|
|
|
|
// 验证 PubKeyAuth 不为空(认证成功的标志)
|
|
if len(tsreq.PubKeyAuth) == 0 {
|
|
glog.Error("NLA authentication failed: empty PubKeyAuth")
|
|
return fmt.Errorf("NLA auth failed: empty PubKeyAuth")
|
|
}
|
|
|
|
glog.Trace("PubKeyAuth:", tsreq.PubKeyAuth)
|
|
|
|
// 尝试解密验证公钥,但不作为强制失败条件
|
|
// 因为某些Windows版本的响应格式可能略有不同
|
|
pubkey := t.ntlmSec.GssDecrypt(tsreq.PubKeyAuth)
|
|
if pubkey == nil {
|
|
glog.Debug("GssDecrypt returned nil, but continuing since no ErrorCode was returned")
|
|
}
|
|
|
|
// NLA仅验证模式:凭据已验证成功,不发送credentials,直接返回
|
|
// 这样不会建立RDP会话,不会挤掉已登录用户
|
|
if t.nlaAuthOnly {
|
|
glog.Info("NLA auth-only mode: credentials verified, skipping session establishment")
|
|
return ErrNLAAuthSuccess
|
|
}
|
|
|
|
domain, username, password := t.ntlm.GetEncodedCredentials()
|
|
credentials := nla.EncodeDERTCredentials(domain, username, password)
|
|
authInfo := t.ntlmSec.GssEncrypt(credentials)
|
|
req := nla.EncodeDERTRequest(nil, authInfo, nil)
|
|
_, err = t.Conn.Write(req)
|
|
if err != nil {
|
|
glog.Info("send AuthenticateMessage", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *TPKT) Read(b []byte) (n int, err error) {
|
|
return t.Conn.Read(b)
|
|
}
|
|
|
|
func (t *TPKT) Write(data []byte) (n int, err error) {
|
|
buff := &bytes.Buffer{}
|
|
core.WriteUInt8(FASTPATH_ACTION_X224, buff)
|
|
core.WriteUInt8(0, buff)
|
|
core.WriteUInt16BE(uint16(len(data)+4), buff)
|
|
buff.Write(data)
|
|
glog.Trace("tpkt Write", hex.EncodeToString(buff.Bytes()))
|
|
return t.Conn.Write(buff.Bytes())
|
|
}
|
|
|
|
func (t *TPKT) Close() error {
|
|
return t.Conn.Close()
|
|
}
|
|
|
|
func (t *TPKT) SetFastPathListener(f core.FastPathListener) {
|
|
t.fastPathListener = f
|
|
}
|
|
|
|
func (t *TPKT) SendFastPath(secFlag byte, data []byte) (n int, err error) {
|
|
buff := &bytes.Buffer{}
|
|
core.WriteUInt8(FASTPATH_ACTION_FASTPATH|((secFlag&0x3)<<6), buff)
|
|
core.WriteUInt16BE(uint16(len(data)+3)|0x8000, buff)
|
|
buff.Write(data)
|
|
glog.Trace("TPTK SendFastPath", hex.EncodeToString(buff.Bytes()))
|
|
return t.Conn.Write(buff.Bytes())
|
|
}
|
|
|
|
func (t *TPKT) recvHeader(s []byte, err error) {
|
|
glog.Trace("tpkt recvHeader", hex.EncodeToString(s), err)
|
|
if err != nil {
|
|
t.Emit("error", err)
|
|
return
|
|
}
|
|
r := bytes.NewReader(s)
|
|
version, _ := core.ReadUInt8(r)
|
|
if version == FASTPATH_ACTION_X224 {
|
|
glog.Debug("tptk recvHeader FASTPATH_ACTION_X224, wait for recvExtendedHeader")
|
|
core.StartReadBytes(2, t.Conn, t.recvExtendedHeader)
|
|
} else {
|
|
glog.Debug("[-] !!!! version is not FASTPATH_ACTION_X224, version=", version)
|
|
t.secFlag = (version >> 6) & 0x3
|
|
length, _ := core.ReadUInt8(r)
|
|
t.lastShortLength = int(length)
|
|
glog.Debug("last read len:", length)
|
|
if t.lastShortLength&0x80 != 0 {
|
|
core.StartReadBytes(1, t.Conn, t.recvExtendedFastPathHeader)
|
|
} else {
|
|
//core.StartReadBytes(1, t.Conn, t.recvExtendedFastPathHeader)
|
|
if t.lastShortLength >= 2 {
|
|
core.StartReadBytes(t.lastShortLength-2, t.Conn, t.recvFastPath)
|
|
} else {
|
|
glog.Debug("lastShortLength = 0")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *TPKT) recvExtendedHeader(s []byte, err error) {
|
|
glog.Trace("tpkt recvExtendedHeader", hex.EncodeToString(s), err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
r := bytes.NewReader(s)
|
|
size, _ := core.ReadUint16BE(r)
|
|
glog.Debug("tpkt wait recvData:", size)
|
|
core.StartReadBytes(int(size-4), t.Conn, t.recvData)
|
|
}
|
|
|
|
func (t *TPKT) recvData(s []byte, err error) {
|
|
glog.Trace("tpkt recvData", hex.EncodeToString(s), err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
t.Emit("data", s)
|
|
core.StartReadBytes(2, t.Conn, t.recvHeader)
|
|
}
|
|
|
|
func (t *TPKT) recvExtendedFastPathHeader(s []byte, err error) {
|
|
glog.Trace("tpkt recvExtendedFastPathHeader", hex.EncodeToString(s))
|
|
r := bytes.NewReader(s)
|
|
rightPart, err := core.ReadUInt8(r)
|
|
if err != nil {
|
|
glog.Error("TPTK recvExtendedFastPathHeader", err)
|
|
return
|
|
}
|
|
|
|
leftPart := t.lastShortLength & ^0x80
|
|
packetSize := (leftPart << 8) + int(rightPart)
|
|
if packetSize == 0 {
|
|
fmt.Println("get packetSize,rightPart=", packetSize, rightPart)
|
|
t.Emit("close")
|
|
} else {
|
|
core.StartReadBytes(packetSize-3, t.Conn, t.recvFastPath)
|
|
}
|
|
}
|
|
|
|
func (t *TPKT) recvFastPath(s []byte, err error) {
|
|
glog.Trace("tpkt recvFastPath")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// NLA-only authentication can receive a Fast-Path packet before the PDU
|
|
// layer installs a listener. Treat it as an ignorable early packet instead
|
|
// of dereferencing a nil interface and crashing the whole scan.
|
|
if t.fastPathListener == nil {
|
|
return
|
|
}
|
|
|
|
t.fastPathListener.RecvFastPath(t.secFlag, s)
|
|
core.StartReadBytes(2, t.Conn, t.recvHeader)
|
|
}
|