shadow1ng/fscan · error

NODE_RDP_PROTOCOL_PDU_SEC_BAD_LICENSE_HEADER

NODE_RDP_PROTOCOL_PDU_SEC_BAD_LICENSE_HEADER

Error message

NODE_RDP_PROTOCOL_PDU_SEC_BAD_LICENSE_HEADER

What it means

recvLicenceInfo in sec/sec.go reads the security header of an incoming license PDU on the channel. If the securityFlag does not include LICENSE_PKT (0x0080), the data is not a valid license packet, so the client emits the node-rdp-style constant error NODE_RDP_PROTOCOL_PDU_SEC_BAD_LICENSE_HEADER and stops processing.

Source

Thrown at libs/grdp/protocol/sec/sec.go:730

	c.sendFlagged(EXCHANGE_PKT, message.serialize())
	return true
}
func (c *Client) sendInfoPkt() {
	var secFlag uint16 = INFO_PKT
	if c.enableEncryption {
		secFlag |= ENCRYPT
	}

	glog.Debug("RdpVersion:", c.ClientCoreData().RdpVersion, ":", gcc.RDP_VERSION_5_PLUS)
	c.sendFlagged(secFlag, c.info.Serialize(c.ClientCoreData().RdpVersion == gcc.RDP_VERSION_5_PLUS))
}

func (c *Client) recvLicenceInfo(channel string, s []byte) {
	glog.Debug("sec recvLicenceInfo", hex.EncodeToString(s))
	r := bytes.NewReader(s)
	h := readSecurityHeader(r)
	if (h.securityFlag & LICENSE_PKT) == 0 {
		c.Emit("error", errors.New("NODE_RDP_PROTOCOL_PDU_SEC_BAD_LICENSE_HEADER"))
		return
	}

	p := lic.ReadLicensePacket(r)
	switch p.BMsgtype {
	case lic.NEW_LICENSE:
		glog.Info("sec NEW_LICENSE")
		c.Emit("success")
		goto connect
	case lic.ERROR_ALERT:
		message := p.LicensingMessage.(*lic.ErrorMessage)
		glog.Info("sec ERROR_ALERT and ErrorCode:", message.DwErrorCode)
		if message.DwErrorCode == lic.STATUS_VALID_CLIENT && message.DwStateTransaction == lic.ST_NO_TRANSITION {
			goto connect
		}
		goto retry
	case lic.LICENSE_REQUEST:
		glog.Info("sec LICENSE_REQUEST")

View on GitHub (pinned to 95cc12e753)

Solutions

  1. Log the raw header bytes (h.securityFlag, h.securitySequence) to confirm misalignment vs. a genuinely non-license packet.
  2. Verify earlier PDU framing: ensure the length consumed by the previous packet matches, so readSecurityHeader starts at the true header offset.
  3. If the server doesn't implement licensing properly, configure it to skip the license phase (e.g. fix xrdp/terminal server licensing settings) or patch the client to ignore non-license data on this channel.
  4. Update grdp/node-rdp-derived code so recvLicenceInfo tolerates and skips unexpected PDUs instead of erroring.

Example fix

// before
if (h.securityFlag & LICENSE_PKT) == 0 {
    c.Emit("error", errors.New("NODE_RDP_PROTOCOL_PDU_SEC_BAD_LICENSE_HEADER"))
    return
}
// after
if (h.securityFlag & LICENSE_PKT) == 0 {
    glog.Warnf("skipping non-license packet on licence channel, flags=0x%x", h.securityFlag)
    return
}
Defensive patterns

Strategy: try-catch

Type guard

func isLicensePacket(h securityHeader) bool {
    return (h.securityFlag & LICENSE_PKT) != 0
}

Try / catch

client.On("error", func(err error) {
    if strings.Contains(err.Error(), "BAD_LICENSE_HEADER") {
        glog.Warn("non-license data on licence channel; ignoring")
        return
    }
    glog.Error("rdp error: ", err)
})

Prevention

When it happens

Trigger: Server sends a licensing-phase message whose security header flags lack the LICENSE_PKT bit — e.g. a differently framed PDU delivered on the licensing path, stream misalignment making readSecurityHeader read the wrong bytes, or a server that doesn't do licensing but still sends data where a license packet is expected.

Common situations: Connecting to servers with nonstandard or disabled licensing (e.g. some Linux RDP servers, xrdp misconfigurations); connecting when license negotiation state is out of order after a reconnect; a preceding parse bug shifting bytes so flags read incorrectly.

Related errors


AI-assisted analysis of shadow1ng/fscan@95cc12e753 (2026-09-06). Data as JSON: /api/errors/3699ed2eb698ff82. Report an issue: GitHub.