import socket
import struct
import sys
def check_rdp_nla(target_ip, port=3389, timeout=5):
"""
检测目标 RDP 服务是否启用了网络级身份验证 (NLA)。
返回:
dict: {
'status': 'open' | 'closed' | 'error',
'nla_enabled': True | False | None,
'supported_protocols': list, # ['RDP', 'SSL', 'NLA']
'message': str
}
"""
result = {
'status': 'error',
'nla_enabled': None,
'supported_protocols': [],
'message': ''
}
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((target_ip, port))
result['status'] = 'open'
# 1. 构建 RDP Negotiation Request (Client -> Server)
# TPKT Header (4 bytes)
# X.224 Connection Request (variable)
# RDP Negotiation Request (optional but recommended for detection)
# RDP Negotiation Request Structure:
# Type: 0x01 (TYPE_RDP_NEG_REQ)
# Flags: 0x00
# Length: 0x08 (LSB), 0x00 (MSB)
# Protocols: 0x03 (PROTOCOL_RDP | PROTOCOL_SSL) - 我们声明支持这两种,看服务器回什么
# 如果要测试服务器是否强制 NLA,我们可以声明支持 NLA (0x02) 或者不声明。
# 最佳实践:声明支持所有常见协议 (RDP | SSL | HYBRID),看服务器选择哪个。
# PROTOCOL_RDP=0x00, PROTOCOL_SSL=0x01, PROTOCOL_HYBRID(NLA)=0x02
supported_protocols_client = 0x03 # RDP | SSL
neg_req = struct.pack('<BBHI',
0x01, # Type: RDP_NEG_REQ
0x00, # Flags
0x0008, # Length: 8 bytes
supported_protocols_client # Protocols
)
# X.224 Connection Request PDU
# Length: variable (will be calculated)
# Type: 0xE0 (Connection Request)
# dst_ref: 0x0000
# src_ref: 0x0000
# class: 0x00
x224_cr = struct.pack('<BBHHB',
0x00, # Length MSB (placeholder)
0x00, # Length LSB (placeholder)
0x0000, # dst_ref
0x0000, # src_ref
0x00 # class
)
# Prepend X.224 Type (0xE0) which is part of the PDU but often handled separately in simple structs
# Actually, standard X.224 CR:
# Byte 0: Length (including this byte)
# Byte 1: Type (0xE0)
# Byte 2-3: dst_ref
# Byte 4-5: src_ref
# Byte 6: class
x224_header = bytearray([
0x00, # Length MSB (placeholder)
0x00, # Length LSB (placeholder)
0xe0, # Type: Connection Request
0x00, 0x00, # dst_ref
0x00, 0x00, # src_ref
0x00 # class
])
# Combine X.224 Header + RDP Neg Req
payload = bytes(x224_header) + neg_req
# Calculate total length for X.224 and TPKT
x224_len = len(payload)
tpkt_len = x224_len + 4 # TPKT header is 4 bytes
# Update X.224 Length (Bytes 0-1 of x224_header are at index 0-1 of payload)
# X.224 length includes the X.224 header itself but NOT the TPKT header?
# Wait, TPKT wraps X.224.
# TPKT Length field covers the entire TPKT packet (including TPKT header).
# X.224 Length field covers the X.224 PDU (including X.224 header).
# Update X.224 Length in payload
payload[0] = (x224_len >> 8) & 0xFF
payload[1] = x224_len & 0xFF
# Build TPKT Header
tpkt_header = struct.pack('>BBH',
0x03, # Version
0x00, # Reserved
tpkt_len # Length
)
final_packet = tpkt_header + payload
# 2. 发送请求
sock.send(final_packet)
# 3. 接收响应
response = sock.recv(1024)
if not response:
result['message'] = "No response received"
return result
# 4. 解析响应
# 期望收到 TPKT -> X.224 Connect Confirm -> RDP Negotiation Response
if len(response) < 11:
result['message'] = "Response too short to parse"
return result
# Parse TPKT
tpkt_ver = response[0]
tpkt_len = struct.unpack('>H', response[2:4])[0]
# Parse X.224
x224_type = response[5] # Index 5 because TPKT is 4 bytes, X.224 type is the 2nd byte of X.224 PDU?
# Let's look at structure:
# TPKT: [Ver(1)][Res(1)][Len(2)]
# X.224: [Len(2)][Type(1)][DstRef(2)][SrcRef(2)][Class(1)]
# So X.224 Type is at index 4+2 = 6?
# TPKT: 0,1,2,3
# X.224 Len: 4,5
# X.224 Type: 6
x224_type = response[6]
if x224_type != 0xd0: # 0xD0 is Connect Confirm
# Might be a Disconnect or Error
result['message'] = f"Unexpected X.224 type: {hex(x224_type)}"
return result
# Check for RDP Negotiation Response
# It should follow immediately after X.224 header
# X.224 header is 7 bytes (Len 2 + Type 1 + Dst 2 + Src 2 + Class 1) -- Wait, standard X.224 CR/CC is 7 bytes?
# Len(2) + Type(1) + Dst(2) + Src(2) + Class(1) = 8 bytes?
# Let's re-verify:
# My payload construction:
# x224_header: [LenMSB, LenLSB, 0xE0, 0,0, 0,0, 0] -> 8 bytes.
# So X.224 PDU starts at index 4 of response.
# X.224 Type is at index 4+2 = 6. Correct.
# X.224 PDU length is 8 bytes.
# So RDP Neg Resp should start at index 4 + 8 = 12.
if len(response) >= 15: # 4(TPKT) + 8(X.224) + 3(min RDP neg)
rdp_neg_type = response[12]
if rdp_neg_type == 0x02: # TYPE_RDP_NEG_RSP
flags = response[13]
length = struct.unpack('<H', response[14:16])[0]
protocol = struct.unpack('<I', response[16:20])[0]
protocols_supported = []
if protocol & 0x00: # PROTOCOL_RDP
protocols_supported.append('RDP')
if protocol & 0x01: # PROTOCOL_SSL
protocols_supported.append('SSL')
if protocol & 0x02: # PROTOCOL_HYBRID (NLA)
protocols_supported.append('NLA')
result['supported_protocols'] = protocols_supported
# 判断 NLA 是否启用/被选择
# 如果服务器选择了 PROTOCOL_HYBRID (0x02),则必须使用 NLA
if protocol == 0x02 or protocol == 0x08: # 0x08 is HYBRID_EX
result['nla_enabled'] = True
result['message'] = "NLA is required/enabled"
elif protocol == 0x01:
result['nla_enabled'] = False
result['message'] = "SSL selected, NLA not strictly enforced in negotiation"
elif protocol == 0x00:
result['nla_enabled'] = False
result['message'] = "Standard RDP security selected"
else:
result['nla_enabled'] = False
result['message'] = f"Unknown protocol selected: {hex(protocol)}"
else:
result['message'] = f"No RDP Negotiation Response found (Type: {hex(rdp_neg_type)})"
else:
result['message'] = "Response too short for RDP Negotiation"
sock.close()
except socket.timeout:
result['status'] = 'closed'
result['message'] = "Connection timed out"
except ConnectionRefusedError:
result['status'] = 'closed'
result['message'] = "Connection refused"
except Exception as e:
result['status'] = 'error'
result['message'] = str(e)
return result
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python check_rdp_nla.py <target_ip> [port]")
sys.exit(1)
ip = sys.argv[1]
port = int(sys.argv[2]) if len(sys.argv) > 2 else 3389
print(f"[*] Checking RDP NLA status for {ip}:{port} ...")
res = check_rdp_nla(ip, port)
print(f"Status: {res['status']}")
print(f"NLA Enabled: {res['nla_enabled']}")
print(f"Supported Protocols: {res['supported_protocols']}")
print(f"Details: {res['message']}")