เซสชัน RDP ต้องมีที่อยู่ไคลเอนต์ และแหล่งข้อมูลที่ดูเหมือนชัดเจนก็ไม่ตรงกัน CLIENTNAME ให้ชื่อคอมพิวเตอร์ รีจิสทรีเก็บค่าจากเซสชันอื่น และคู่ปลายซ็อกเก็ตอาจเป็นที่อยู่ของเกตเวย์หรือ NAT ไม่ใช่ไคลเอนต์ Windows เปิดเผยที่อยู่ไคลเอนต์ที่รายงานต่อเซสชันจริง แต่คุณต้องสอบถามเซสชันที่ถูกต้องและถอดรหัสคำตอบให้ถูกต้อง

ยึดการค้นหากับเซสชันเดียว

เริ่มจากเซสชัน ไม่ใช่จากบุคคล โฮสต์ RDP ของ Windows อาจมีหลายเซสชันสำหรับบัญชีเดียว และที่อยู่ที่เห็นก่อนเชื่อมต่อใหม่ อาจไม่ตรงกับลิงก์ที่ใช้งานอยู่แล้ว บันทึก ID เซสชัน ผู้ใช้ และเวลาที่สังเกตไว้ด้วยกัน ID เซสชันคือกุญแจที่ API คาดหวัง และเป็นค่าที่ป้องกันไม่ให้สองเซสชันสับสนกัน

บนโฮสต์ เครื่องมือเซสชันที่ติดตั้งจะแสดงรายการที่มีอยู่ ใช้มุมมองผู้ดูแลระบบเฉพาะเมื่อคุณมีสิทธิ์เท่านั้น ยืนยัน ID ที่เลือกทันทีก่อนการสอบถาม เพราะ ID ที่ถูกนำกลับมาใช้ใหม่จะคืนข้อมูลของเซสชันอื่น

qwinsta

เรียก WTSQuerySessionInformationW สำหรับ ID นั้น

API ของ Terminal Services ตอบคำถามด้วยคลาสข้อมูล 14, WTSClientAddress ctypes wrapper ขนาดเล็กก็เพียงพอบน Windows และหลีกเลี่ยงการอ่าน hive ของผู้ใช้อื่น โค้ดด้านล่างรับ ID เซสชันอย่างชัดเจนและสอบถามโฮสต์ภายในเครื่อง

import ctypes
import ipaddress
import socket
from ctypes import wintypes

class WTS_CLIENT_ADDRESS(ctypes.Structure):
    _fields_ = [("AddressFamily", wintypes.DWORD),
                ("Address", ctypes.c_ubyte * 20)]

wts = ctypes.WinDLL("Wtsapi32", use_last_error=True)
query = wts.WTSQuerySessionInformationW
query.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.c_int,
                  ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(wintypes.DWORD)]
query.restype = wintypes.BOOL
free = wts.WTSFreeMemory
free.argtypes = [ctypes.c_void_p]

def client_address(session_id):
    buf = ctypes.c_void_p()
    size = wintypes.DWORD()
    if not query(None, session_id, 14, ctypes.byref(buf), ctypes.byref(size)):
        raise ctypes.WinError(ctypes.get_last_error())
    try:
        if size.value < ctypes.sizeof(WTS_CLIENT_ADDRESS):
            raise RuntimeError("short WTS buffer")
        value = ctypes.cast(buf, ctypes.POINTER(WTS_CLIENT_ADDRESS)).contents
        raw = bytes(value.Address)
        if value.AddressFamily == socket.AF_INET:
            addr = ipaddress.ip_address(raw[2:6])
        elif value.AddressFamily == socket.AF_INET6:
            addr = ipaddress.ip_address(raw[:16])
        else:
            return None
        return None if addr.is_unspecified else str(addr)
    finally:
        free(buf)

ส่ง ID เซสชันแบบจำนวนเต็มที่คุณเพิ่งตรวจสอบ และเก็บแฮนเดิลเซิร์ฟเวอร์ที่เป็น null ไว้สำหรับเครื่องภายใน ให้ถือว่าการเรียกที่ล้มเหลวเป็นข้อผิดพลาด ไม่ใช่ที่อยู่ที่ไม่มี เพราะทั้งสองนำไปสู่ข้อสรุปที่ต่างกัน การสอบถามเซสชันของผู้ใช้อื่นต้องใช้สิทธิ์ Query Information ที่เหมาะสม ดังนั้นให้รันเครื่องมือภายใต้บัญชีที่ได้รับสิทธิ์นั้นแทนการยกระดับสิทธิ์แบบเฉพาะกิจ บัฟเฟอร์ที่คืนมาเป็นของ API และต้องถูกปล่อยแม้การถอดรหัสจะล้มเหลว

ถอดรหัสตระกูล แล้วปล่อยบัฟเฟอร์

โครงสร้างนี้ไม่ใช่สตริง IP ฟิลด์ตระกูลเป็นตัวกำหนดวิธีอ่านไบต์ที่อยู่ยี่สิบไบต์: IPv4 เริ่มที่ออฟเซ็ตสอง ส่วน IPv6 เริ่มที่ออฟเซ็ตศูนย์ ที่อยู่ที่ไม่ระบุหมายความว่าไคลเอนต์ไม่ได้รายงานมา ซึ่งไม่เหมือนกับสตริงว่าง การเชื่อมต่อผ่าน Remote Desktop Gateway ได้รับการบันทึกว่าเป็นกรณีที่ที่อยู่ไม่พร้อมใช้งาน เซสชันที่ผ่านเกตเวย์จึงไม่รายงานสิ่งที่เป็นประโยชน์เกี่ยวกับที่อยู่จริงของไคลเอนต์ จากนั้นการเชื่อมโยงต้องย้ายไปที่บันทึกของเกตเวย์เอง

พิมพ์ตระกูลควบคู่กับที่อยู่ขณะดีบัก เพราะความไม่ตรงกันระหว่างสองสิ่งนี้เป็นสาเหตุทั่วไปของค่าที่ดูสมเหตุสมผลแต่ผิด ปล่อยบัฟเฟอร์ด้วย WTSFreeMemory ในบล็อก finally เพื่อไม่ให้ข้อยกเว้นทำให้รั่ว

แยกที่อยู่ที่รายงานออกจากหลักฐาน

ที่อยู่ไคลเอนต์ที่รายงานเป็นการกล่าวอ้างที่ไคลเอนต์จัดหาให้ ไม่ใช่การสังเกตที่เชื่อถือได้ ไม่ใช่คู่ปลาย TCP ไม่ใช่ตารางเช่าของเกตเวย์ VPN และไม่ใช่ที่อยู่สาธารณะของไคลเอนต์ เมื่อคำถามจริงคือสัญญาเช่า VPN ใดเป็นของบุคคลหนึ่ง ให้เชื่อมโยงบันทึกเซสชันที่ผ่านการรับรองของเซิร์ฟเวอร์ VPN ในเวลาเดียวกัน เมื่อคำถามคือแหล่งใดไปถึงผู้ฟัง ให้อ่านตารางการเชื่อมต่อหรือบันทึกฝั่งเซิร์ฟเวอร์ที่ได้รับอนุญาต

เก็บค่าเหล่านี้โดยติดป้ายแยกไว้ในเครื่องมือสนับสนุนใดก็ตาม ผลลัพธ์ WTS ที่หายไปควรคงสภาพหายไป แทนที่จะถอยไปใช้ค่ารีจิสทรีใด ๆ จากเซสชันอื่น บันทึก ID เซสชันไว้ข้างที่อยู่ เพราะผู้ใช้คนเดียวกันอาจเชื่อมต่อใหม่และได้รับค่าใหม่ ทั้งชื่อไคลเอนต์และที่อยู่ที่รายงานไม่ใช่หลักฐานที่เพียงพอสำหรับการให้สิทธิ์เข้าถึง

บทความที่เกี่ยวข้อง