├── tests ├── __init__.py ├── test_failure_to_restrict_url_access.py ├── test_security_misconfiguration.py ├── test_insecure_object_references.py ├── test_unvalidated_redirects.py ├── test_authentication.py ├── test_csrf.py ├── test_sqli.py ├── test_xss.py └── test_insecure_transport_layer_protection.py ├── requirements.txt ├── config.json ├── README.md └── funcs.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.5.1 2 | py==1.4.31 3 | pytest==3.0.3 4 | requests==2.11.1 -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "url" : "http://192.168.10.105/mutillidae", 3 | "validuser": "a", 4 | "validpass": "a", 5 | "invaliduser": "invaliduser", 6 | "domain" : "192.168.10.105", 7 | "path" : "/mutillidae", 8 | "seclevel": 5 9 | } -------------------------------------------------------------------------------- /tests/test_failure_to_restrict_url_access.py: -------------------------------------------------------------------------------- 1 | from funcs import setsecuritylevel, loadconfig 2 | import re 3 | 4 | config = loadconfig() 5 | 6 | 7 | # Check to see if the text "PHP Credits" appears on the page 8 | # Can't check for "PHP Version" because the text appears in the footer 9 | def test_check_for_phpinfo_page(): 10 | s = setsecuritylevel(seclevel=config['seclevel']) 11 | url = "%s/index.php?page=phpinfo.php" % config['url'] 12 | r = s.get(url) 13 | php = re.compile(r"PHP Credits") 14 | assert php.search(r.text) is None 15 | -------------------------------------------------------------------------------- /tests/test_security_misconfiguration.py: -------------------------------------------------------------------------------- 1 | from funcs import setsecuritylevel, loadconfig 2 | from bs4 import BeautifulSoup 3 | 4 | config = loadconfig() 5 | 6 | 7 | def test_post_vs_get_form(): 8 | s = setsecuritylevel(seclevel=config['seclevel']) 9 | url = "%s/index.php?page=user-info.php" % config['url'] 10 | r = s.get(url) 11 | soup = BeautifulSoup(r.text, "html.parser") 12 | inputs = soup.find_all("form") 13 | # check and make sure there is only one form 14 | assert len(inputs) == 1 15 | # check to make sure the method is sending values over POST and not a GET request 16 | assert inputs[0]["method"] == "POST" 17 | -------------------------------------------------------------------------------- /tests/test_insecure_object_references.py: -------------------------------------------------------------------------------- 1 | from funcs import setsecuritylevel, loadconfig 2 | import re 3 | 4 | config = loadconfig() 5 | 6 | 7 | def test_lfi_source_code_viewer(): 8 | s = setsecuritylevel(seclevel=config['seclevel']) 9 | badinput = "./../../../../etc/passwd" 10 | url = "%s/index.php?page=source-viewer.php" % config['url'] 11 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 12 | data = "page=%s&phpfile=show-log.php&source-file-viewer-php-submit-button=View+File" % badinput 13 | r = s.post(url, data, headers=headers) 14 | passwd = re.compile(r"/bin/sh") 15 | assert passwd.search(r.text) is None 16 | -------------------------------------------------------------------------------- /tests/test_unvalidated_redirects.py: -------------------------------------------------------------------------------- 1 | from funcs import setsecuritylevel, loadconfig 2 | import re 3 | 4 | config = loadconfig() 5 | 6 | 7 | # this test checks for a redirect. this redirect is not a 301 or 302, 8 | # but rather a http-equiv="refresh" located at the end of the body 9 | def test_unvalidated_redirect(): 10 | s = setsecuritylevel(seclevel=config['seclevel']) 11 | redirect_url = "http://www.th3r3p0.com" 12 | url = "%s/index.php?page=redirectandlog.php&forwardurl=%s" % (config['url'], redirect_url) 13 | r = s.get(url, allow_redirects=False) 14 | redirect = re.compile(r"%s" % redirect_url) 15 | assert redirect.search(r.text) is None 16 | 17 | -------------------------------------------------------------------------------- /tests/test_authentication.py: -------------------------------------------------------------------------------- 1 | from funcs import setsecuritylevel, loadconfig, auth 2 | 3 | config = loadconfig() 4 | 5 | 6 | # Response header "Logged-In-User" displays what user is logged in. 7 | # This test ensures that you cannot manipulate the uid cookie on the client side 8 | def test_uid_cookie_manipulation(): 9 | s = setsecuritylevel(seclevel=config['seclevel']) 10 | 11 | s = auth(config['validuser'], config['validpass'], s) 12 | url = "%s/index.php" % config['url'] 13 | r = s.get(url) 14 | actual_user = (r.headers["Logged-In-User"]) 15 | s.cookies.set('uid', '1', domain=config["domain"].encode('utf-8'), path=config["path"].encode('utf-8')) 16 | r = s.get(url) 17 | 18 | assert actual_user == r.headers["Logged-In-User"] 19 | -------------------------------------------------------------------------------- /tests/test_csrf.py: -------------------------------------------------------------------------------- 1 | from funcs import setsecuritylevel, loadconfig 2 | from bs4 import BeautifulSoup 3 | 4 | config = loadconfig() 5 | 6 | 7 | # use beautiful soup to extract the csrf token 8 | def get_csrf_token(r): 9 | soup = BeautifulSoup(r.text, "html.parser") 10 | inputs = soup.find_all("input") 11 | for i in inputs: 12 | if i["name"] == "csrf-token": 13 | csrftoken = i["value"] 14 | return csrftoken 15 | 16 | 17 | # ensures that csrf tokens are different 18 | def test_csrf_blog_post(): 19 | s = setsecuritylevel(seclevel=config['seclevel']) 20 | url = "%s/index.php?page=add-to-your-blog.php" % config['url'] 21 | r = s.get(url) 22 | csrf_token_one = get_csrf_token(r) 23 | r = s.get(url) 24 | csrf_token_two = get_csrf_token(r) 25 | assert csrf_token_one != csrf_token_two 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/test_sqli.py: -------------------------------------------------------------------------------- 1 | from funcs import loadconfig 2 | from funcs import setsecuritylevel 3 | import re 4 | 5 | config = loadconfig() 6 | 7 | 8 | # Test for SQLi on the "View your details" page 9 | def test_view_your_details(): 10 | s = setsecuritylevel(seclevel=config['seclevel']) 11 | badinputs = ['%27', '%22'] 12 | # You could configure the sqli test to check for a 500 error instead of specific text on a page 13 | sqli_error = re.compile(r"Error: Failure is always an option and this situation proves it") 14 | for badinput in badinputs: 15 | url = "%s/index.php?page=user-info.php&username=%s&password=&user-info-php-submit-button=View+Account+Details"\ 16 | % (config['url'], badinput) 17 | r = s.get(url) 18 | # This will error out if a badinput triggers the sqli error text to be displayed 19 | assert sqli_error.search(r.text) is None 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoSec4WebApps 2 | 3 | This repository contains some sample pytest scripts to run against a Mutillidae 1 web server. You can use these scripts to become familiar with the pytest framework and help you visualize some different ways to test for vulnerabilities which you may have found in your application in the past. 4 | 5 | To use: 6 | 7 | 1. git clone https://github.com/Th3R3p0/AutoSec4WebApps.git 8 | 2. download and run Metasploitable 2 which contains Mutillidae and other vulnerable web apps - https://sourceforge.net/projects/metasploitable/files/Metasploitable2/ 9 | 3. modify config.json file to include the correct URL, domain and path of your Mutillidae web server 10 | 4. create a virtualenv and activate it 11 | 5. pip install -r requirements.txt 12 | 6. from AutoSec4WebApps directory run: pytest tests/test_*.py 13 | 7. You can modify the config.json seclevel to level 0, 1 or 5. Level 5 is the most secure level of mutillidae and Level 0 is the most insecure. All tests will fail on level 0 and all but 1 test will pass on level 5. 14 | -------------------------------------------------------------------------------- /tests/test_xss.py: -------------------------------------------------------------------------------- 1 | from funcs import loadconfig 2 | from funcs import setsecuritylevel 3 | import re 4 | 5 | config = loadconfig() 6 | 7 | 8 | # Test for Reflected (type one) XSS on the "DNS Lookup" page 9 | def test_xss_dns_lookup(): 10 | s = setsecuritylevel(seclevel=config['seclevel']) 11 | badinputs = ['%3Cscript%3Ealert%281%29%3C%2Fscript%3E'] 12 | # todo: fix the following: 13 | # the regex will only search for the one badinput listed above. I tried to parse the badinputs, but the parenthesis 14 | # needed to be escaped. 15 | for badinput in badinputs: 16 | url = "%s/index.php?page=dns-lookup.php" % config['url'] 17 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 18 | data = "target_host=%s&dns-lookup-php-submit-button=Lookup+DNS" % badinput 19 | r = s.post(url, data, headers=headers) 20 | xss = re.compile("") 21 | # This will error out if a badinput triggers the sqli error text to be displayed 22 | assert xss.search(r.text) is None 23 | -------------------------------------------------------------------------------- /tests/test_insecure_transport_layer_protection.py: -------------------------------------------------------------------------------- 1 | from funcs import setsecuritylevel, loadconfig 2 | 3 | config = loadconfig() 4 | 5 | # this is a best practice check. this can be used out of the box with any URL. 6 | # this check is assuming that the http requests should ALWAYS redirect to https 7 | def test_http_to_https(): 8 | s = setsecuritylevel(seclevel=config['seclevel']) 9 | 10 | # use the url from config, but turn it into an insecure request if using https 11 | url = "%s/index.php" % config['url'] 12 | if url.startswith("https://"): 13 | insecure_url = url.replace("https://", "http://") 14 | elif url.startswith("http://"): 15 | insecure_url = url 16 | else: 17 | raise NameError("URL is not valid - must start with http:// or http://") 18 | 19 | r = s.get(insecure_url, allow_redirects=False) 20 | # make sure a redirection occurs 21 | print(r.status_code) 22 | assert r.status_code == 301 or r.status_code == 302 23 | if r.status_code == 301 or r.status_code == 302: 24 | # make sure the redirection is to an https protocol 25 | if "Location" in r.headers: 26 | https_redirect = r.headers["Location"].startswith("https://") 27 | assert https_redirect is True 28 | else: 29 | raise NameError("Redirect location did not appear in the headers") 30 | -------------------------------------------------------------------------------- /funcs.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | 5 | # Todo: look into the open config file location 6 | def loadconfig(): 7 | with open('config.json') as json_data_file: 8 | config = json.load(json_data_file) 9 | return config 10 | 11 | config = loadconfig() 12 | 13 | 14 | # This function only exists for Mutillidae. This would not be needed in a real world application 15 | # This function returns a session with a security level - security levels are mapped to a jsessionid 16 | def setsecuritylevel(seclevel=0): 17 | if seclevel == 0: 18 | s = requests.Session() 19 | return s 20 | if seclevel == 1: 21 | s = requests.Session() 22 | s.get("%s/index.php" % config['url']) 23 | s.get("%s/index.php?do=toggle-security" % config['url'], allow_redirects=False) 24 | return s 25 | if seclevel == 5: 26 | s = requests.Session() 27 | s.get("%s/index.php" % config['url'], allow_redirects=False) 28 | s.get("%s/index.php?do=toggle-security" % config['url'], allow_redirects=False) 29 | s.get("%s/index.php?do=toggle-security" % config['url'], allow_redirects=False) 30 | return s 31 | 32 | 33 | # This function returns the python session so that the cookies can be used for future requests 34 | def auth(user, pwd, s=None): 35 | if s is None: 36 | s = requests.Session() 37 | data = "username=%s&password=%s&login-php-submit-button=Login" % (user, pwd) 38 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 39 | s.post("%s/index.php?page=login.php" % config['url'], data, headers=headers) 40 | return s 41 | --------------------------------------------------------------------------------