├── .gitignore ├── param-ninja-logo.png ├── __pycache__ ├── index.cpython-38.pyc └── pentool.cpython-38.pyc ├── user.py ├── templates ├── alert.html ├── layout.html ├── profile.html ├── menu.html ├── login.html ├── register.html └── index.html ├── requirements.txt ├── param_ninja.py ├── starter.py ├── models.py ├── auth.py ├── README.md ├── main.py ├── scanner.py └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | config 2 | *.pyc 3 | __pycache__/* 4 | .vscode/launch.json 5 | db.sqlite 6 | -------------------------------------------------------------------------------- /param-ninja-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urchinsec/param-ninja/HEAD/param-ninja-logo.png -------------------------------------------------------------------------------- /__pycache__/index.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urchinsec/param-ninja/HEAD/__pycache__/index.cpython-38.pyc -------------------------------------------------------------------------------- /__pycache__/pentool.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urchinsec/param-ninja/HEAD/__pycache__/pentool.cpython-38.pyc -------------------------------------------------------------------------------- /user.py: -------------------------------------------------------------------------------- 1 | from flask_login import UserMixin 2 | 3 | class User(UserMixin): 4 | 5 | def is_authenticated(self): 6 | return True 7 | 8 | def is_active(self): 9 | return True 10 | 11 | def is_anonymous(self): 12 | return False 13 | 14 | def get_id(self): 15 | return "root_user" -------------------------------------------------------------------------------- /templates/alert.html: -------------------------------------------------------------------------------- 1 |
| Test | 32 |Result | 33 |
|---|---|
| [1] Technologies Used | 38 |{{ check_result["technology"]}} | 39 |
| [2] Vulnerability Spotted | 42 |{{ check_result["vuln"]}} | 43 |
| [3] Server Type | 46 |{{ check_result["server_version"]}} | 47 |
| [4] Domain Information | 50 |{{ check_result["domain_information"]}} | 51 |
| [5] Ports Discovered | 54 |{{ check_result["port_discovery"]}} | 55 |
| [6] DNS Records | 58 |{{ check_result["dns_record"]}} | 59 |
| [7] Suspicious Directories | 62 |{{ check_result["scan_files"]}} | 63 |
| [8] Subdomain Enumeration | 66 |{{ check_result["subdomain_scan"]}} | 67 |
| [9] Exploit Information | 70 |{{ check_result["exploit_info"]}} | 71 |
| [10] Mitigation | 74 |{{ check_result["mitigation_info"]}} | 75 | 76 |
](param-ninja-logo.png)
4 | An automated penetration testing tool , that automates web vulnerabilities testing upon a URL given with a parameter
5 |
6 | ## INSTALLATION
7 | #### Requirements::
8 | ```
9 | pip3 install -r requirements.txt
10 | ```
11 | This should install all the requirements required to fully function.
12 |
13 | #### Configuration::
14 | You need to create a `config` file and put in your secret key and SQLAlchemy URL as an example:
15 |
16 | ```
17 | SECRET_KEY = 'somesecretgoesherepewpew333'
18 | SQLALCHEMY_DATABASE_URI = 'sqlite:///db.sqlite'
19 | VULN_KEY = 'keyhere'
20 | SHODAN_API_KEY = 'keyhere'
21 | WHOISXMLAPI_KEY = 'keyhere'
22 | ```
23 |
24 | You can generate your Shodan API from shodan's official website , and vulners from https://vulners.com , whoisxmlapi from https://whoisxmlapi.com
25 |
26 | ## USAGE
27 | Now to finally run it up :
28 |
29 | ```
30 | python3 starter.py
31 | ```
32 |
33 | And the it will start the flask web server , then you can access it from the web by visiting `http://localhost:5000/`
34 | then you will need to log in , where the default creds `admin:admin`:
35 |
36 | 
37 |
38 |
39 | Now in the main page you can put in a URL with an endpoint that you want to test , and then click on scan and it'll perform the magic.
40 |
41 | 
42 |
43 | Then the user can change the password by navigating to the profile tab , and setting the username he wants as well as the password.
44 |
45 | 
46 |
47 | This is an example of scanning `https://api.github.com/users` but it's best you put something with an endpoint and parameter , since this is a parameter tester :) , As seen below are the output:
48 |
49 | 
50 |
51 | Another Example:
52 |
53 | 
54 |
55 |
56 | **We have a new tab which is `Post Based`, and it's underdevelopment.**
57 |
58 | ## WHAT'S MORE?
59 | You can visit `/output` to check the output of possible exploits found from exploit-db.
60 | You can visit `/subdomains` to check the output of subdomains available under the domain provided at first.
61 | You can visit `/domain` to get information about the domain hosting the web application.
62 |
63 | **FUNCTIONALITIES::**
64 |
65 | Below are the vulnerabilities that can be tested currently:
66 |
67 | 1. XSS (Cross Site Scripting)
68 | 2. HTML injection
69 | 3. SSTI (Server Side Template Injection)
70 | 4. OS Command Injection
71 | 5. LFI (Local File Inclusion)
72 | 6. SQL injection
73 | 7. SSRF (Server Side Request Forgery)
74 | 8. Directory Traversal
75 | 9. Open Redirection
76 | 10. Anonymous FTP Login (if exists an FTP Service)
77 |
78 | More Vulnerability testing functions will be added soon:)
79 |
80 | The tool performs 10 core functions as of now, and these are:
81 |
82 | 1. Determine Technologies Used
83 | 2. Find Vulnerabilities
84 | 3. Check Web Server Type
85 | 4. Get Domain Information
86 | 5. Perform Ports Enumeration
87 | 6. Pull DNS Records
88 | 7. Get Suspicious Directories
89 | 8. Perform Subdomain Enumeration
90 | 9. Provide Exploit Information Upon Technologies Used
91 | 10. Provide Mitigation Information Accordingly To The Exploits
92 |
93 | ## CONTACTS::
94 | 1. info@urchinsec.com
95 | 2. [urchinsec](https://twitter.com/urchinsec_)
96 |
97 | *LOGO ArtWork By [witchdocsec](https://github.com/witchdocsec/)*
98 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask import Blueprint, render_template, request, jsonify
3 | from flask_login import login_required
4 | from scanner import Scanner, PostScanner
5 |
6 |
7 | def build_main_blueprint(app):
8 | main = Blueprint('main', __name__)
9 | scanner_config = {
10 | "v_key": app.config['VULN_KEY'],
11 | "s_key": app.config['SHODAN_API_KEY'],
12 | 'w_key': app.config['WHOISXMLAPI_KEY']
13 | }
14 | scanner = Scanner(scanner_config)
15 |
16 | @main.route('/')
17 | @login_required
18 | def index():
19 | return render_template("index.html")
20 |
21 | @main.route("/posttool", methods=["GET"])
22 | @login_required
23 | def posttool():
24 | return render_template("post.html")
25 |
26 | # to work on function!
27 | @main.route("/post", methods=["POST"])
28 | @login_required
29 | def post_url():
30 | post_result = PostScanner.full_test(request.form["url"], request.form["data"])
31 |
32 | return render_template("post.html", post_result=post_result)
33 |
34 | @main.route("/check", methods=["POST"])
35 | @login_required
36 | def check_url():
37 | url = request.form["url"] # url goes here
38 | blacklisted_inps = ["127.0.0.1", "localhost", "0.0.0.0", "[::]", "0000::1", "127.127.127.127", "127.0.1.3", "127.0.0.0", "2130706433", "3232235521", "3232235521", "2852039166", "169.254.169.254", "192.168.1.1", "192.168.0.1", "0177.0.0.1", "o177.0.0.1", "0o177.0.0.1", "q177.0.0.1", "[0:0:0:0:0:ffff:127.0.0.1]", "[::ffff:127.0.0.1]", "localhost:+11211aaa", "localhost:00011211aaaa", "0", "127.1", "127.0.1", "file:///", "smb:///", "telnet:///", "ftp:"]
39 | for word in blacklisted_inps:
40 | if word in url:
41 | return render_template("index.html", check_results="MALICIOUS ATTEMPT!")
42 | else:
43 | check_result = scanner.full_scan(url)
44 |
45 | return render_template("index.html", check_result=check_result)
46 |
47 | @main.route("/output", methods=["GET"])
48 | @login_required
49 | def output():
50 | with open("output_exploit_search.json", 'r') as exploitResult:
51 | read = exploitResult.readlines()
52 | read = jsonify(read)
53 |
54 | return read
55 |
56 | @main.route("/suspicious", methods=["GET"])
57 | @login_required
58 | def suspicious_files():
59 | with open("found_files.txt", "r") as found:
60 | read = found.readlines()
61 |
62 | return read
63 |
64 | @main.route("/domain", methods=["GET"])
65 | @login_required
66 | def domain():
67 | with open("output_domain_info.json", "r") as domainInfo:
68 | read = domainInfo.readlines()
69 | read = jsonify(read)
70 |
71 | return read
72 |
73 | @main.route("/lfi", methods=["GET"])
74 | @login_required
75 | def lfi_results():
76 | with open("lfi_proof.html", "r") as lfi_proof:
77 | read = lfi_proof.readlines()
78 |
79 | return read
80 |
81 | @main.route("/oscmdi", methods=["GET"])
82 | @login_required
83 | def oscmdi_results():
84 | with open("os_cmdi_proof.html", "r") as oscmdi_proof:
85 | read = oscmdi_proof.readlines()
86 |
87 | return read
88 |
89 |
90 | @main.route("/subdomains",methods=["GET"])
91 | @login_required
92 | def subdomains():
93 | with open("subdomains.txt","r") as subdomains:
94 | read = subdomains.readlines()
95 | read = jsonify(read)
96 |
97 | return read
98 |
99 | @main.route("/dnsrecords", methods=["GET"])
100 | @login_required
101 | def dnsrecords():
102 | with open("dnsrecord.json", "r") as dnsrecords:
103 | read = dnsrecords.readlines()
104 | read = jsonify(read)
105 |
106 | return read
107 |
108 | return (main)
109 |
--------------------------------------------------------------------------------
/scanner.py:
--------------------------------------------------------------------------------
1 | from Wappalyzer import Wappalyzer, WebPage
2 | from urllib.parse import urlparse, quote_plus
3 | from shodan import APIError
4 | # import dns.resolver
5 | import requests
6 | import vulners
7 | import shodan
8 | import base64
9 | import socket
10 | import json
11 | import os
12 |
13 |
14 | class Scanner:
15 | def __init__(self, params):
16 | self.params = params
17 |
18 | def full_scan(self, url):
19 | return {
20 | "checked_url": url,
21 | "technology": self.scan_for_technologies(url),
22 | "vuln": self.scan_for_vuln(url),
23 | "server_version": self.scan_for_server_version(url),
24 | "domain_information": self.domain_information(url),
25 | "port_discovery": self.port_discovery(url),
26 | "dns_record": self.dns_record(url),
27 | "scan_files": self.enum_files(url),
28 | "subdomain_scan": self.subdomain_scan(url),
29 | "exploit_info": self.exploit_info(url),
30 | "mitigation_info": self.mitigation_info(url)
31 | }
32 |
33 | def scan_for_technologies(self, url):
34 | webpage = WebPage.new_from_url(url)
35 | wappalyzer = Wappalyzer.latest()
36 | res = wappalyzer.analyze_with_versions_and_categories(webpage)
37 |
38 | return res
39 |
40 | def domain_information(self, url):
41 | try:
42 | domain = urlparse(url).netloc
43 | ip = socket.gethostbyname(domain)
44 |
45 | SKey = self.params['s_key']
46 | SApi = shodan.Shodan(SKey)
47 |
48 | info = json.dumps(SApi.host(ip))
49 |
50 | with open('output_domain_info.json', 'w') as domainInfo:
51 | domainInfo.writelines(info)
52 |
53 | return 'Visit /domain'
54 | except APIError as e:
55 | return 'Unfortunately, Your Shodan Key Returned An Error'
56 |
57 | def anonymous_ftp_login(url):
58 | domain = urlparse(url).netloc
59 | ip = socket.gethostbyname(domain)
60 |
61 | try:
62 | import ftplib
63 | server = ftplib.FTP()
64 | server.connect(ip, 21)
65 | login = server.login('Anonymous', '')
66 | if '230' in login:
67 | return "FTP Anonymous Login"
68 | else:
69 | return ""
70 | except:
71 | return ""
72 |
73 | def port_discovery(self, url):
74 | domain = urlparse(url).netloc
75 | ip = socket.gethostbyname(domain)
76 | """for port in range(1, 65535):
77 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
78 | socket.setdefaulttimeout(1)
79 | result = s.connect_ex((ip, port))
80 | if result == 0:
81 | with open('ports_opened.txt', 'w') as ports:
82 | write = f'Port - {port} - Opened'
83 | ports.writelines(write)
84 | # reading the file with ports result
85 | with open('ports_opened.txt', 'r') as ports:
86 | read = ports.readlines()
87 | return read
88 | s.close()"""
89 | try:
90 | SKey = self.params['s_key']
91 | SApi = shodan.Shodan(SKey)
92 |
93 | SInfo = SApi.host(ip)
94 | ports = json.dumps(SInfo['ports'])
95 |
96 | return ports
97 | except shodan.exception.APITimeout:
98 | return "Time Out! Something Went Wrong!"
99 | except shodan.exception.APIError:
100 | return "Shodan Key Not Authorized"
101 |
102 | def dns_record(self, url):
103 | domain = urlparse(url).netloc
104 |
105 | """A_RECORD = dns.resolver.query(domain,'A')
106 |
107 | for val in A_RECORD:
108 | data = json.dumps({
109 | 'A RECORD': val.to_text()
110 | })
111 |
112 | return data"""
113 | WKey = self.params['w_key']
114 | WApi = "https://reverse-dns.whoisxmlapi.com/api/v1"
115 |
116 | data_TXT = json.dumps({
117 | "apiKey": WKey,
118 | "limit": 1000,
119 | "recordType": "TXT",
120 | "terms": [{
121 | "field": "domain",
122 | "term": domain
123 | }]
124 | })
125 | data_SOA = json.dumps({
126 | "apiKey": WKey,
127 | "limit": 1000,
128 | "recordType": "SOA",
129 | "terms": [{
130 | "field": "domain",
131 | "term": domain
132 | }]
133 | })
134 | data_CNAME = json.dumps({
135 | "apiKey": WKey,
136 | "limit": 1000,
137 | "recordType": "CNAME",
138 | "terms": [{
139 | "field": "domain",
140 | "term": domain
141 | }]
142 | })
143 |
144 | headers = {
145 | 'Content-Type': 'application/json'
146 | }
147 |
148 | req_TXT = requests.post(WApi, headers=headers, data=data_TXT, verify=False)
149 | req_CNAME = requests.post(WApi, headers=headers, data=data_CNAME, verify=False)
150 | req_SOA = requests.post(WApi, headers=headers, data=data_SOA, verify=False)
151 |
152 | if req_TXT.status_code == 200 and req_CNAME.status_code == 200 and req_SOA.status_code == 200:
153 | res_TXT = json.dumps(req_TXT.json())
154 | res_SOA = json.dumps(req_SOA.json())
155 | res_CNAME = json.dumps(req_CNAME.json())
156 |
157 | full_data = json.dumps({
158 | "TXT RECORDS": [res_TXT],
159 | "SOA RECORDS": [res_SOA],
160 | "CNAME RECORDS": [res_CNAME]
161 | })
162 |
163 | with open("dnsrecords.json", "w") as dnsrecords:
164 | dnsrecords.writelines(full_data)
165 |
166 | return full_data
167 | else:
168 | return "Unable To Gather DNS Records"
169 |
170 | def scan_for_vuln(self, url):
171 | test1 = Scanner.test_xss(url)
172 | test2 = Scanner.test_ssti(url)
173 | test3 = Scanner.test_htmli(url)
174 | test4 = Scanner.test_sqli(url)
175 | test5 = Scanner.test_lfi(url)
176 | test6 = Scanner.test_cmdi(url)
177 | test7 = Scanner.test_ssrf(url)
178 | test8 = Scanner.test_dtraversal(url)
179 | test9 = Scanner.test_openredirection(url)
180 | test10 = Scanner.anonymous_ftp_login(url)
181 |
182 | return f"{test1} , {test2} , {test3}, {test4}, {test5}, {test6}, {test7}, {test8}, {test9} , {test10}"
183 |
184 | def scan_for_server_version(self, url):
185 | req = requests.get(url, verify=False)
186 | headers = req.headers
187 | server = headers['Server']
188 |
189 | return server
190 |
191 | """def history_information(self, url):
192 | domain = urlparse(url).netloc
193 | ip = socket.gethostbyname(domain)
194 |
195 | SKey = self.params['s_key']
196 | SApi = shodan.Shodan(SKey)
197 |
198 | info = json.dumps(SApi.host(ip))
199 |
200 | return info"""
201 |
202 | def exploit_info(self, url):
203 | req = requests.get(url)
204 | headers = req.headers
205 |
206 | server = headers["Server"]
207 |
208 | if server == '':
209 | return "No Server Version Found! Or Type!"
210 | else:
211 | try:
212 | VKey = self.params["v_key"] # get api key by going to https://vulners.com
213 | VApi = vulners.Vulners(api_key=VKey)
214 |
215 | search = VApi.searchExploit(server)
216 | search = json.dumps(search, indent=2)
217 |
218 | os.system(f"touch output_exploit_search.json")
219 | with open(f"output_exploit_search.json", "w") as exploitResult:
220 | exploitResult.writelines(search)
221 |
222 | return f"Visit /output"
223 | except requests.exceptions.HTTPError:
224 | return "Something Went Wrong! But it works!"
225 | except:
226 | return "Something Went Wrong But it works!"
227 |
228 | def mitigation_info(self, url):
229 | payloadxss = ""
230 | payloadssti = "{{7*7}}"
231 | payloadhtmli = "