├── README.md └── paramhunter.py /README.md: -------------------------------------------------------------------------------- 1 | # ParamHunter 2 | 3 | ParamHunter is a powerful and flexible tool for discovering hidden parameters in web applications. Built upon the foundation of the Arjun project, ParamHunter offers enhanced functionality and improved performance for parameter discovery. 4 | 5 | ## Features 6 | 7 | - Efficient parameter discovery using various techniques 8 | - Rate limiting to prevent overwhelming target servers 9 | - Verbose output option for detailed process information 10 | - Customizable wordlists for parameter guessing 11 | - Handles GET requests with discovered parameters 12 | 13 | ## Installation 14 | 15 | 1. Clone the repository: 16 | ``` 17 | git clone https://github.com/yourusername/paramhunter.git 18 | cd paramhunter 19 | ``` 20 | 21 | 2. Install the required dependencies: 22 | ``` 23 | pip install requests ratelimit urllib3 24 | ``` 25 | 26 | 3. Ensure you have Python 3.6 or higher installed. 27 | 28 | ## Usage 29 | 30 | Basic usage: 31 | 32 | ``` 33 | cat urls.txt | python3 paramhunter.py -w wordlist.txt 34 | ``` 35 | 36 | This will process the URLs from `urls.txt`, using `wordlist.txt` as the source of potential parameter names. 37 | 38 | ### Options 39 | 40 | - `-w FILE`: Specify the wordlist file (required) 41 | - `-c INT`: Set the chunk size for processing (default: 250) 42 | - `-r FLOAT`: Set the rate limit in requests per second (default: 10) 43 | - `-t FLOAT`: Set the timeout for requests in seconds (default: 5) 44 | - `--disable-redirects`: Disable following redirects 45 | - `-v, --verbose`: Enable verbose output 46 | 47 | ### Example with Verbose Output 48 | 49 | ``` 50 | cat urls.txt | python3 paramhunter.py -w wordlist.txt -v -r 5 -t 10 51 | ``` 52 | 53 | This will run ParamHunter with verbose output, a rate limit of 5 requests per second, and a timeout of 10 seconds per request. 54 | 55 | Example of findings 56 | ![image](https://github.com/user-attachments/assets/fb5f416e-7e2f-4b98-a910-5dfd8a229d62) 57 | 58 | 59 | ## Output 60 | 61 | ParamHunter outputs the full HTTP GET requests for each URL, including any discovered parameters. In verbose mode, it also provides detailed information about the discovery process. 62 | 63 | ## Contributing 64 | 65 | Contributions to ParamHunter are welcome! Please feel free to submit pull requests, create issues or spread the word. 66 | 67 | ## License 68 | 69 | [MIT License](LICENSE) 70 | 71 | ## Acknowledgements 72 | 73 | ParamHunter is based on the [Arjun](https://github.com/s0md3v/Arjun) project by s0md3v. We express our gratitude for their excellent work in the field of web parameter discovery. 74 | 75 | ## Disclaimer 76 | 77 | ParamHunter is intended for use in authorized security testing only. Users are responsible for complying with applicable laws and regulations. The developers assume no liability for misuse or damage caused by this program. 78 | -------------------------------------------------------------------------------- /paramhunter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import argparse 6 | import requests 7 | import time 8 | from urllib.parse import urlparse, parse_qs, urlencode 9 | from bs4 import BeautifulSoup 10 | import random 11 | import string 12 | 13 | def verbose_print(*args, **kwargs): 14 | if verbose_print.is_verbose: 15 | print(*args, file=sys.stderr, **kwargs) 16 | 17 | verbose_print.is_verbose = False 18 | 19 | def rate_limited_requester(url, params=None): 20 | if hasattr(rate_limited_requester, 'last_call'): 21 | elapsed = time.time() - rate_limited_requester.last_call 22 | if elapsed < 1 / args.rate_limit: 23 | time.sleep(1 / args.rate_limit - elapsed) 24 | 25 | verbose_print(f"Sending request to {url} with params {params}") 26 | try: 27 | response = requests.get(url, params=params, timeout=args.timeout, allow_redirects=True) 28 | rate_limited_requester.last_call = time.time() 29 | verbose_print(f"Received response with status code {response.status_code}") 30 | return response 31 | except requests.exceptions.RequestException as e: 32 | verbose_print(f"Error occurred while requesting {url}: {str(e)}") 33 | return None 34 | 35 | def generate_random_string(length=10): 36 | return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) 37 | 38 | def compare_responses(response1, response2, param, test_value): 39 | if response1 is None or response2 is None: 40 | return False 41 | 42 | if response1.status_code != response2.status_code: 43 | return True 44 | 45 | if abs(len(response1.content) - len(response2.content)) > 100: 46 | return True 47 | 48 | soup1 = BeautifulSoup(response1.text, 'html.parser') 49 | soup2 = BeautifulSoup(response2.text, 'html.parser') 50 | 51 | # Check if the parameter value appears in the response 52 | if test_value in soup2.get_text() and test_value not in soup1.get_text(): 53 | return True 54 | 55 | # Check for significant changes in the page structure 56 | if len(soup1.find_all()) != len(soup2.find_all()): 57 | return True 58 | 59 | return False 60 | 61 | def test_parameter(url, param): 62 | base_response = rate_limited_requester(url) 63 | if base_response is None: 64 | return False 65 | 66 | test_value = generate_random_string() 67 | param_response = rate_limited_requester(url, {param: test_value}) 68 | 69 | if param_response is None: 70 | return False 71 | 72 | return compare_responses(base_response, param_response, param, test_value) 73 | 74 | def discover_parameters(url, wordlist): 75 | existing_params = parse_qs(urlparse(url).query) 76 | discovered_params = [] 77 | 78 | for param in wordlist: 79 | if param not in existing_params and test_parameter(url, param): 80 | discovered_params.append(param) 81 | verbose_print(f"Discovered parameter: {param}") 82 | 83 | return discovered_params 84 | 85 | def construct_url_with_params(url, params): 86 | parsed_url = urlparse(url) 87 | existing_params = parse_qs(parsed_url.query) 88 | 89 | for param in params: 90 | if param not in existing_params: 91 | existing_params[param] = ['value'] 92 | 93 | new_query = urlencode(existing_params, doseq=True) 94 | return parsed_url._replace(query=new_query).geturl() 95 | 96 | def main(): 97 | parser = argparse.ArgumentParser(description="Parameter Hunter - Discover hidden parameters in URLs") 98 | parser.add_argument('-w', '--wordlist', help='Wordlist file path (required)', required=True) 99 | parser.add_argument('-r', '--rate-limit', help='Rate limit (requests per second)', type=float, default=10) 100 | parser.add_argument('-t', '--timeout', help='Timeout for requests (seconds)', type=float, default=10) 101 | parser.add_argument('-v', '--verbose', help='Enable verbose output', action='store_true') 102 | global args 103 | args = parser.parse_args() 104 | 105 | verbose_print.is_verbose = args.verbose 106 | 107 | try: 108 | with open(args.wordlist, 'r') as f: 109 | wordlist = [line.strip() for line in f if line.strip()] 110 | verbose_print(f"Loaded wordlist with {len(wordlist)} entries") 111 | except FileNotFoundError: 112 | print(f"Error: Wordlist file '{args.wordlist}' not found.", file=sys.stderr) 113 | sys.exit(1) 114 | except IOError: 115 | print(f"Error: Unable to read wordlist file '{args.wordlist}'.", file=sys.stderr) 116 | sys.exit(1) 117 | 118 | if not wordlist: 119 | print("Error: Wordlist is empty.", file=sys.stderr) 120 | sys.exit(1) 121 | 122 | for url in sys.stdin: 123 | url = url.strip() 124 | verbose_print(f"\nProcessing URL: {url}") 125 | try: 126 | discovered_params = discover_parameters(url, wordlist) 127 | if discovered_params: 128 | full_url = construct_url_with_params(url, discovered_params) 129 | print(full_url) 130 | else: 131 | verbose_print(f"No new parameters found for: {url}") 132 | except Exception as e: 133 | verbose_print(f"Error processing URL {url}: {str(e)}") 134 | 135 | if __name__ == "__main__": 136 | main() 137 | --------------------------------------------------------------------------------