像fofa一樣解析RDP信息,RDP提取操作系統,RDP登錄截屏 (Golang實現)
從fofa中搜索RDP,會看到它會解析出RDP的信息。

本文探索如何自己實現一個。
1. Nmap指紋
在https://raw.githubusercontent.com/nmap/nmap/master/nmap-service-probes 可以找到關于RDP發包的定義
##############################NEXT PROBE############################## # This is an RDP connection request with the MSTS cookie set. Some RDP # listeners (with NLA?) only respond to this one. # This must be sent before TLSSessionReq because Windows RDP will handshake TLS # immediately and we don't have a way of identifying RDP at that point. Probe TCP TerminalServerCookie q|\x03\0\0*%\xe0\0\0\0\0\0Cookie: mstshash=nmap\r\x01\0\x08\0\x03\0\0\0| rarity 7 ports 3388,3389 fallback TerminalServer Probe TCP TerminalServer q|\x03\0\0\x0b\x06\xe0\0\0\0\0\0| rarity 6 ports 515,1028,1068,1503,1720,1935,2040,3388,3389 # Windows 2000 Server # Windows 2000 Advanced Server # Windows XP Professional match ms-wbt-server m|^\x03\0\0\x0b\x06\xd0\0\0\x12.\0$|s p/Microsoft Terminal Service/ o/Windows/ cpe:/o:microsoft:windows/a match ms-wbt-server m|^\x03\0\0\x17\x08\x02\0\0Z~\0\x0b\x05\x05@\x06\0\x08\x91J\0\x02X$| p/Microsoft Terminal Service/ i/Used with Netmeeting, Remote Desktop, Remote Assistance/ o/Windows/ cpe:/o:microsoft:windows/a match ms-wbt-server m|^\x03\0\0\x11\x08\x02..}\x08\x03\0\0\xdf\x14\x01\x01$|s p/Microsoft NetMeeting Remote Desktop Service/ o/Windows/ cpe:/a:microsoft:netmeeting/ cpe:/o:microsoft:windows/a match ms-wbt-server m|^\x03\0\0\x0b\x06\xd0\0\0\x03.\0$|s p/Microsoft NetMeeting Remote Desktop Service/ o/Windows/ cpe:/a:microsoft:netmeeting/ cpe:/o:microsoft:windows/a # Need more samples! match ms-wbt-server m|^\x03\0\0\x0b\x06\xd0\0\0\0\0\0| p/xrdp/ cpe:/a:jay_sorg:xrdp/ match ms-wbt-server m|^\x03\0\0\x0e\t\xd0\0\0\0[\x02\xa1]\0\xc0\x01$| p/IBM Sametime Meeting Services/ o/Windows/ cpe:/a:ibm:sametime/ cpe:/o:microsoft:windows/a match ms-wbt-server m|^\x03\0\0\x0b\x06\xd0\0\x004\x12\0| p/VirtualBox VM Remote Desktop Service/ o/Windows/ cpe:/a:oracle:vm_virtualbox/ cpe:/o:microsoft:windows/a match ms-wbt-server-proxy m|^nmproxy: Procotol byte is not 8$| p/nmproxy NetMeeting proxy/
它在tcp連接上之后會發包 \x03\0\0*%\xe0\0\0\0\0\0Cookie: mstshash=nmap\r\x01\0\x08\0\x03\0\0\0,nmap關于rdp的版本指紋比較少,而且發的包還有特征。
nmap有一個rdp.lua,封裝了rdp連接的前幾層協議,后面深入學習協議時可以對照著看。
2. 深入協議
官方文檔:https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/023f1e69-cfe8-4ee6-9ee0-7e759fb4e4ee 有協議的交互流程圖

2.1. 發包
看了文檔后,發現連接順序分為十個不同的階段,但是獲得一些基礎信息,只用看第一階段Connection Initiation就行了。
Connection Initiation:客戶端通過向服務器發送 X.224 連接請求 PDU(第2.2.1.1節)來啟動連接。服務器響應 0 類 X.224 連接確認 PDU(第2.2.1.2節)。
從這一點開始,客戶端和服務器之間發送的所有后續數據都包裝在 X.224 數據協議數據單元 (PDU) (1) 中。
請求結構

結構體如下:
「tpktHeader(4 字節):」 TPKT 標頭,如[T123]第 8 節中所指定。

nmap中的定義
__tostring = function(self)
return string.pack(">BBI2",
self.version, // 一般是3
self.reserved or 0, // 一般是0
(self.data and #self.data + 4 or 4)) // 整個結構體的大小,包括后面的數據
..self.data // 后面的數據
「x224Crq(7 字節):」 一個 X.224 類 0 連接請求傳輸協議數據單元 (TPDU),如[X224] 第 13.3 節中所指定。


- 第一個是后面結構體長度,第二個是hex(int('11100000',2)),即0xe0 ,后面5個字節都是0,這個數據結構即length+0xe0,0x0,0x0,0x0,0x0,0x0
「routingToken(可變):一個可選的可變長度路由令牌(用于負載平衡),由 0x0D0A 兩字節序列終止。有關路由令牌格式的詳細信息,請參閱」「[MSFT-SDLBTS]」** “路由令牌格式”。路由令牌和 CR+LF 序列的長度包含在「X.224 連接請求長度指示符 字段中。如果此字段存在,則」cookie**字段不得存在。
「cookie(變量):可選且長度可變的」「ANSI」** 字符串,以 0x0D0A 兩字節序列結尾。此文本字符串必須是“Cookie:mstshash=IDENTIFIER”,其中 IDENTIFIER 是一個 ANSI 字符串(示例 cookie 字符串顯示在第**「4.1.1」「節中)。整個 cookie 字符串和 CR+LF 序列的長度包含在」X.224 連接請求長度指示符字段中。如果「routingToken」字段存在,則該字段不得存在。
「rdpNegReq(8 字節):一個可選的」RDP 協商請求(第2.2.1.1.1節)結構。該字段的長度包含在「X.224 連接請求長度指示符」字段中。

- 文檔描述很詳細了,這個結構體很重要,用于設置請求協議

「rdpCorrelationInfo(36 字節):一個可選的」關聯信息(第2.2.1.1.2節)結構。該字段的長度包含在「X.224 連接請求長度指示符」字段中。如果在RDP 協商請求結構的「標志」字段中設置了 CORRELATION_INFO_PRESENT (0x08) 標志,則該字段必須存在,封裝在可選的「rdpNegReq」 字段中。如果未設置 CORRELATION_INFO_PRESENT (0x08) 標志,則該字段不得存在。
- 這個結構體沒啥用,不用寫
用golang實現這個結構體
type RdpReq struct {
requestedProtocols uint32
cookie []byte
}
func NewReq(protocol uint32, cookie []byte) *RdpReq {
return &RdpReq{requestedProtocols: protocol, cookie: cookie}
}
func (r *RdpReq) Serialize() []byte {
buff := &bytes.Buffer{}
// cookie
if r.cookie != nil {
cookie := []byte(fmt.Sprintf("Cookie: mstshash=%s\r", r.cookie))
buff.Write(cookie)
}
// rdpNegReq
buff.Write([]byte{0x1, 0x0, 0x8, 0x0})
requestedProtocolData := make([]byte, 4)
binary.LittleEndian.PutUint32(requestedProtocolData, r.requestedProtocols)
buff.Write(requestedProtocolData)
buff2 := &bytes.Buffer{}
// x224Crq (7 字節)
buff2.Write([]byte{
uint8(buff.Len() + 6),
0xe0,
0x00, 0x00,
0x00, 0x00, 0x00,
})
buff2.Write(buff.Bytes())
// tpktHeader(4 字節)
buff3 := &bytes.Buffer{}
buff3.Write([]byte{3, 0})
lengthData := make([]byte, 2)
binary.BigEndian.PutUint16(lengthData, uint16(buff2.Len()+4))
buff3.Write(lengthData)
buff3.Write(buff2.Bytes())
return buff3.Bytes()
}
測試
func main() {
rdp := NewReq(PROTOCOL_RDP|PROTOCOL_SSL|PROTOCOL_HYBRID, []byte("w8ay"))
buff := rdp.Serialize()
fmt.Println(hex.Dump(buff))
}
輸出

和nmap的probe\x03\0\0*%\xe0\0\0\0\0\0Cookie: mstshash=nmap\r\x01\0\x08\0\x03\0\0\0也能對應上
2.2. 收包
在發包完畢后,會收到如下結構體

前面的結構可以跳過,直接看rdpNegData結構
文檔:https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/b2975bdc-6d56-49ee-9c57-f2ff3a0b6817
成功的話,它會返回一個服務器指定的通信協議。

根據結構用golang寫個解析程序
type RdpResp struct {
data []byte
Type int
Flags int
Result uint32
}
func ParseRdpResp(data []byte) (*RdpResp, error) {
GenericRDPSignature := []byte{
0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00,
}
if !checkSignature(data[:11], GenericRDPSignature) {
return nil, errors.New("not rdp response")
}
reader := bytes.NewReader(data)
reader.Seek(11, io.SeekStart)
r := new(RdpResp)
r.data = data
_type, err := reader.ReadByte()
if err != nil {
return r, err
}
r.Type = int(_type)
_flag, err := reader.ReadByte()
if err != nil {
return r, err
}
r.Flags = int(_flag)
reader.Seek(2, io.SeekCurrent)
result := make([]byte, 4)
_, err = reader.Read(result)
if err != nil {
return r, err
}
r.Result = binary.LittleEndian.Uint32(result)
return r, nil
}
3. 簡單OS識別
第一層連接協議中,我們可以控制請求的協議,并從返回包中解析出服務器選擇的協議以及flags參數。
協議的支持在windows不同版本是不一樣的,根據這個,將協議設定為PROTOCOL_RDP|PROTOCOL_SSL|PROTOCOL_HYBRID_EX,即可根據返回包結果來識別不同OS。
func (r *RdpResp) FingerPrintOs() string {
Windows2000 := []byte{
0x03, 0x00, 0x00, 0x0b, 0x06, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00,
}
WindowsServer2012R2 := []byte{
0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00,
0x03, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00,
}
WindowsServer2008 := []byte{
0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02,
0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00,
}
Windows7OrServer2008R2 := []byte{
0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02,
0x09, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00,
}
WindowsServer2008R2DC := []byte{
0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02,
0x01, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00,
}
Windows10 := []byte{
0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02,
0x1f, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00,
}
WindowsServer2012Or8 := []byte{
0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02,
0x0f, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00,
}
WindowsServer2016or2019 := []byte{
0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02,
0x0f, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00,
}
signatures := map[string][]byte{
"Windows 2000": Windows2000,
"WindowsServer2012R2": WindowsServer2012R2,
"Windows Server 2008": WindowsServer2008,
"Windows 7 or Server 2008 R2": Windows7OrServer2008R2,
"Windows Server 2008 R2 DC": WindowsServer2008R2DC,
"Windows 10": Windows10,
"Windows 8 or Server 2012": WindowsServer2012Or8,
"Windows Server 2016 or 2019": WindowsServer2016or2019,
}
for fingerprint, signature := range signatures {
signatureLength := len(signature)
if len(r.data) < signatureLength {
continue
}
responseSlice := r.data[:signatureLength]
tof := checkSignature(responseSlice, signature)
if tof {
return fingerprint
}
}
return ""
}
簡單識別通過包對比進行識別,有時能識別fofa所識別不了的地方。如


4. 協議枚舉
可以看nmap rdp加密協議枚舉的腳本 https://github.com/nmap/nmap/blob/master/scripts/rdp-enum-encryption.nse
-- @output -- PORT STATE SERVICE -- 3389/tcp open ms-wbt-server -- | Security layer -- | CredSSP (NLA): SUCCESS -- | CredSSP with Early User Auth: SUCCESS -- | Native RDP: SUCCESS -- | RDSTLS: SUCCESS -- | SSL: SUCCESS -- | RDP Encryption level: High -- | 40-bit RC4: SUCCESS -- | 56-bit RC4: SUCCESS -- | 128-bit RC4: SUCCESS -- | FIPS 140-1: SUCCESS -- |_ RDP Protocol Version: RDP 5.x, 6.x, 7.x, or 8.x server
它實現獲取Security layer,是遍歷發送協議,如果返回包支持則支持。這個可以很容易實現。
在之前封裝的返回包結構中加上獲取支持協議的文本
func (r *RdpResp) SupportProtocol() string {
if r.Type == TYPE_RDP_NEG_FAILURE {
return ""
}
switch r.Result {
case PROTOCOL_HYBRID_EX:
return "PROTOCOL_HYBRID_EX"
case PROTOCOL_RDSAAD:
return "PROTOCOL_RDSAAD"
case PROTOCOL_HYBRID:
return "PROTOCOL_HYBRID"
case PROTOCOL_SSL:
return "PROTOCOL_SSL"
case PROTOCOL_RDP:
return "PROTOCOL_RDP"
case PROTOCOL_RDSTLS:
return "PROTOCOL_RDSTLS"
}
return ""
}
封裝協議枚舉函數
// 獲取RDP支持的協議
func GetSupportProtocol(address string, port uint16, timeout time.Duration) []string {
ret := make([]string, 0)
for _, v := range []uint32{PROTOCOL_RDP, PROTOCOL_SSL, PROTOCOL_HYBRID, PROTOCOL_HYBRID_EX, PROTOCOL_RDSTLS, PROTOCOL_RDSAAD} {
conn, err := DialTCP(address, port, timeout)
if err != nil {
panic(err)
}
rdp := NewReq(v, []byte("w8ay"))
buff := rdp.Serialize()
err = Send(conn, buff, timeout)
if err != nil {
continue
}
response, err := Recv(conn, timeout)
if err != nil {
continue
}
resp, _ := ParseRdpResp(response)
if resp != nil {
if resp.Type == TYPE_RDP_NEG_RSP {
ret = append(ret, resp.SupportProtocol())
}
}
time.Sleep(time.Millisecond * 100)
}
return ret
}

不清楚fofa的flag是怎么實現的,fofa的似乎不太準確。相同的IP使用這種方式能識別出的協議更多。如圖。

另外還有RDP Protocol Version的獲取,但是要實現MCS結構,太麻煩不做了,資料在
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/379a020e-9925-4b4f-98f3-7d634e10b411
5. NTLM 信息獲取
大佬說:「tls連接后會進行ntlmssp的挑戰響應,能夠非常準確的提取出來主機名和操作系統的版本」
RDP文檔中寫道,為了安全考慮,可以直接走TLS協議并使用CredSSP進行驗證。CredSSP可以使用ntlm驗證進行信息獲取。
nmap有一個腳本用于提取ntlm信息 https://github.com/nmap/nmap/blob/master/scripts/rdp-ntlm-info.nse
-- @output -- 3389/tcp open ms-wbt-server syn-ack ttl 128 Microsoft Terminal Services -- | rdp-ntlm-info: -- | Target_Name: W2016 -- | NetBIOS_Domain_Name: W2016 -- | NetBIOS_Computer_Name: W16GA-SRV01 -- | DNS_Domain_Name: W2016.lab -- | DNS_Computer_Name: W16GA-SRV01.W2016.lab -- | DNS_Tree_Name: W2016.lab -- | Product_Version: 10.0.14393 -- |_ System_Time: 2019-06-13T10:38:35+00:00 -
文檔:https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/6aac4dea-08ef-47a6-8747-22ea7f6d8685?source=recommendations
請求體,negoToken字段是ntlm的結構,整個結構體要進行ASN.1編碼。

嫌麻煩可以直接用nmap組好的數據包

它的返回信息見文檔
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786

里面值得關注的數據有 OSVersion,TargetName 以及以下的結構
AvIDMap := map[uint16]string{
1: "NetBIOSComputerName",
2: "NetBIOSDomainName",
3: "FQDN", // DNS Computer Name
4: "DNSDomainName",
5: "DNSTreeName",
7: "Timestamp",
9: "MsvAvTargetName",
}
編寫程序解析后就能獲得想要的信息了。

獲得的操作系統版本信息是基于`Major-Minor-Build`的版本號,找到一個比較全的列表
https://www.gaijin.at/en/infos/windows-version-numbers

寫個爬蟲就給爬下來了

再次運行下,就能得到操作系統的詳細信息了。輸出
請求包 00000000 03 00 00 2a 25 e0 00 00 00 00 00 43 6f 6f 6b 69 |...*%......Cooki| 00000010 65 3a 20 6d 73 74 73 68 61 73 68 3d 77 38 61 79 |e: mstshash=w8ay| 00000020 0d 0a 01 00 08 00 0b 00 00 00 |..........| 返回包 00000000 03 00 00 13 0e d0 00 00 12 34 00 02 1f 08 00 08 |.........4......| 00000010 00 00 00 |...| 簡單OS識別: Windows 10 支持協議 [PROTOCOL_RDP PROTOCOL_SSL PROTOCOL_HYBRID PROTOCOL_HYBRID_EX PROTOCOL_RDSTLS] NTLMSSP 返回包 00000000 30 81 a8 a0 03 02 01 06 a1 81 a0 30 81 9d 30 81 |0..........0..0.| 00000010 9a a0 81 97 04 81 94 4e 54 4c 4d 53 53 50 00 02 |.......NTLMSSP..| 00000020 00 00 00 0c 00 0c 00 38 00 00 00 75 82 9a e2 5e |.......8...u...^| 00000030 53 34 ae 68 91 2c 56 00 00 00 00 00 00 00 00 50 |S4.h.,V........P| 00000040 00 50 00 44 00 00 00 0a 00 61 4a 00 00 00 0f 53 |.P.D.....aJ....S| 00000050 00 45 00 52 00 56 00 45 00 52 00 02 00 0c 00 53 |.E.R.V.E.R.....S| 00000060 00 45 00 52 00 56 00 45 00 52 00 01 00 0c 00 53 |.E.R.V.E.R.....S| 00000070 00 45 00 52 00 56 00 45 00 52 00 04 00 0c 00 53 |.E.R.V.E.R.....S| 00000080 00 45 00 52 00 56 00 45 00 52 00 03 00 0c 00 53 |.E.R.V.E.R.....S| 00000090 00 45 00 52 00 56 00 45 00 52 00 07 00 08 00 74 |.E.R.V.E.R.....t| 000000a0 df 2b ea 65 14 d9 01 00 00 00 00 |.+.e.......| NetBIOSComputerName:SERVER DNSDomainName:SERVER FQDN:SERVER Timestamp:2022-12-20 19:26:33 Product_Version:10.0.19041 Os_Verion:Windows 10, Version 2004/Windows Server, Version 2004 TargetName:SERVER NetBIOSDomainName:SERVER
對比下fofa中的信息

基本上將rdp信息都解析出來了。
NTLM解析代碼如下
func RdpWithNTLM(conn net.Conn, timeout time.Duration) (map[string]any, error) {
info := make(map[string]any)
// CredSSP protocol - NTLM authentication
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp
// http://davenport.sourceforge.net/ntlm.html
NegotiatePacket := []byte{
0x30, 0x37, 0xA0, 0x03, 0x02, 0x01, 0xff, 0xA1, 0x30, 0x30, 0x2E, 0x30, 0x2C, 0xA0, 0x2A, 0x04, 0x28,
// Signature
'N', 'T', 'L', 'M', 'S', 'S', 'P', 0x00,
// Message Type
0x01, 0x00, 0x00, 0x00,
// Negotiate Flags
0xF7, 0xBA, 0xDB, 0xE2,
// Domain Name Fields
0x00, 0x00, // DomainNameLen
0x00, 0x00, // DomainNameMaxLen
0x00, 0x00, 0x00, 0x00, // DomainNameBufferOffset
// Workstation Fields
0x00, 0x00, // WorkstationLen
0x00, 0x00, // WorkstationMaxLen
0x00, 0x00, 0x00, 0x00, // WorkstationBufferOffset
// Version
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
err := Send(conn, NegotiatePacket, timeout)
if err != nil {
return nil, err
}
response, err := Recv(conn, timeout)
if err != nil {
return nil, err
}
fmt.Println("NTLMSSP 返回包")
fmt.Println(hex.Dump(response))
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(response, []byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0})
if challengeStartOffset == -1 {
return info, nil
}
if len(response) < challengeStartOffset+challengeLen {
return info, nil
}
var responseData NTLMChallenge
response = response[challengeStartOffset:]
responseBuf := bytes.NewBuffer(response)
err = binary.Read(responseBuf, binary.LittleEndian, &responseData)
if err != nil {
return info, 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 info, 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 info, err
}
ProductVersion := fmt.Sprintf("%d.%d.%d", versionData.MajorVersion,
versionData.MinorVersion,
versionData.BuildNumber)
info["Product_Version"] = ProductVersion
v, ok := OsVersion[ProductVersion]
if ok {
info["Os_Verion"] = v
}
// 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
}
// 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 info, 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 info, 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 info, fmt.Errorf("Invalid AV_PAIR list")
}
avPairBuf = bytes.NewBuffer(response[currIdx : currIdx+avPairLen])
err = binary.Read(avPairBuf, binary.LittleEndian, &avPair)
if err != nil {
return info, err
}
}
}
return info, nil
}
6. RDP登錄截圖
不是所有協議都支持RDP截圖

要實現RDP登錄交互,這個涉及更后面的交互了,所以找了個go rdp的庫 https://github.com/tomatome/grdp (這個庫有點小bug,而且只能編譯windows版本,不過我已經一通魔改,修了一些bug,并且支持全平臺編譯)
它里面提供了一個接口,可以獲得位圖數據
g.pdu.On("update", func(rectangles []pdu.BitmapData) {}
bitmap的數據結構可以看文檔:https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/f4ed1422-2eed-4474-bafb-42ab35ad3707
「獲得截圖要做的事情也很多很雜」
- 獲得的位圖只是一小塊圖片,后需要將這些圖片拼接起來
- 位圖有的是壓縮的,需要解壓
- 涉及到將位圖數據轉換為圖片等等
偽代碼如下
ScreenImage := image.NewRGBA(image.Rect(0, 0, 1024, 768))
host := g.Host
g.pdu.On("update", func(rectangles []pdu.BitmapData) {
glog.Info("on update bitmap:", len(rectangles))
bs := make([]Bitmap, 0)
for _, v := range rectangles {
IsCompress := v.IsCompress()
data := v.BitmapDataStream
if IsCompress {
data = BitmapDecompress(&v)
IsCompress = false
}
b := Bitmap{int(v.DestLeft), int(v.DestTop), int(v.DestRight), int(v.DestBottom),
int(v.Width), int(v.Height), Bpp(v.BitsPerPixel), IsCompress, data}
bs = append(bs, b)
}
var (
pixel int
i int
r, g, b, a uint8
)
for _, bm := range bs {
i = 0
pixel = bm.BitsPerPixel
m := image.NewRGBA(image.Rect(0, 0, bm.Width, bm.Height))
for y := 0; y < bm.Height; y++ {
for x := 0; x < bm.Width; x++ {
r, g, b, a = ToRGBA(pixel, i, bm.Data)
c := color.RGBA{r, g, b, a}
i += pixel
m.Set(x, y, c)
}
}
draw.Draw(ScreenImage, ScreenImage.Bounds().Add(image.Pt(bm.DestLeft, bm.DestTop)), m, m.Bounds().Min, draw.Src)
}
// Encode to jpeg.
var imageBuf bytes.Buffer
err = jpeg.Encode(&imageBuf, ScreenImage, nil)
if err != nil {
log.Panic(err)
}
// Write to file.
fo, err := os.Create(fmt.Sprintf("img/%s-%d.jpg", host, index))
index += 1
if err != nil {
panic(err)
}
fw := bufio.NewWriter(fo)
fw.Write(imageBuf.Bytes())
})
這個代碼會在前一幀的基礎行保存圖片,每一幀都會保存為一個完整的圖片。



連起來就像是一個登錄的gif

7. 源碼
完整源碼放在知識星球了,微信公眾號關注“Hacking就是好玩”,回復“知識星球”即可。
8. 參考
- https://github.com/praetorian-inc/fingerprintx
- https://github.com/nmap/nmap
- https://cloud.tencent.com/developer/article/1888905
- https://mp.weixin.qq.com/s?__biz=MjM5NDQ5NjM5NQ==&mid=2651626127&idx=1&sn=0bdcd4969306c5f79707fa946da76b83
- 網絡空間測繪技術之:協議識別(RDP篇)