├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── __init__.py ├── common ├── __init__.py ├── common.py ├── corscheck.py └── logger.py ├── cors_scan.py ├── images ├── screenshot.png └── walmart.png ├── origins.json ├── requirements.txt ├── setup.py └── top_100_domains.txt /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | with: 19 | path: CORScanner 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: '3.x' 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install setuptools wheel twine 30 | 31 | - name: Build and publish 32 | env: 33 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 34 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 35 | run: | 36 | mv CORScanner/setup.py . 37 | mv CORScanner/README.md . 38 | mv CORScanner/MANIFEST.in . 39 | python setup.py sdist bdist_wheel 40 | twine upload dist/* 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | .vscode/ 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jianjun Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include * 2 | recursive-include CORScanner * 3 | 4 | exclude .gitignore 5 | global-exclude .DS_Store 6 | recursive-exclude CORScanner/.git/ * 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.pyc 9 | recursive-exclude * *.pyo 10 | recursive-exclude * *.orig 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About CORScanner 2 | 3 | CORScanner is a python tool designed to discover CORS misconfigurations vulnerabilities of websites. It helps website administrators and penetration testers to check whether the domains/urls they are targeting have insecure CORS policies. 4 | 5 | ### Features 6 | * **Fast**. It uses [gevent](https://github.com/gevent/gevent) instead of Python threads for concurrency, which is much faster for network scanning. 7 | * **Comprehensive**. It covers all [the common types of CORS misconfigurations](#misconfiguration-types) we know. 8 | * **Flexible**. It supports various self-define features (e.g. file output), which is helpful for large-scale scanning. 9 | * 🆕 CORScanner supports installation via pip (`pip install corscanner` or `pip install cors`) 10 | * 🆕 CORScanner can be used as a library in your project. 11 | 12 | Two useful references for understanding CORS systematically: 13 | * USENIX security 18 paper: [We Still Don’t Have Secure Cross-Domain Requests: an Empirical Study of CORS](https://www.jianjunchen.com/p/CORS-USESEC18.pdf) 14 | * 中文详解:[绕过浏览器SOP,跨站窃取信息:CORS配置安全漏洞报告及最佳部署实践](https://www.jianjunchen.com/post/cors%E5%AE%89%E5%85%A8%E9%83%A8%E7%BD%B2%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/) 15 | 16 | 17 |
Please consider citing our paper if you do scentific research (Click me). 18 |

19 | 20 | *Latex version:* 21 | 22 | ```tex 23 | @inproceedings{chen-cors, 24 | author = {Jianjun Chen and Jian Jiang and Haixin Duan and Tao Wan and Shuo Chen and Vern Paxson and Min Yang}, 25 | title = {We Still Don{\textquoteright}t Have Secure Cross-Domain Requests: an Empirical Study of {CORS}}, 26 | booktitle = {27th {USENIX} Security Symposium ({USENIX} Security 18)}, 27 | year = {2018}, 28 | isbn = {978-1-939133-04-5}, 29 | address = {Baltimore, MD}, 30 | pages = {1079--1093}, 31 | url = {https://www.usenix.org/conference/usenixsecurity18/presentation/chen-jianjun}, 32 | publisher = {{USENIX} Association}, 33 | month = aug, 34 | } 35 | ``` 36 | 37 | *Word version:* 38 | 39 | Jianjun Chen, Jian Jiang, Haixin Duan, Tao Wan, Shuo Chen, Vern Paxson, and Min Yang. "We Still Don’t Have Secure Cross-Domain Requests: an Empirical Study of CORS." In 27th USENIX Security Symposium (USENIX Security 18), pp. 1079-1093. 2018. 40 | 41 |

42 |
43 | 44 | ## Screenshots 45 | 46 | ![CORScanner](https://github.com/chenjj/CORScanner/raw/master/images/screenshot.png "CORScanner in action") 47 | 48 | ## Installation 49 | 50 | - Download this tool 51 | ``` 52 | git clone https://github.com/chenjj/CORScanner.git 53 | ``` 54 | 55 | - Install dependencies 56 | ``` 57 | sudo pip install -r requirements.txt 58 | ``` 59 | CORScanner depends on the `requests`, `gevent`, `tldextract`, `colorama` and `argparse` python modules. 60 | 61 | ## Python Version: 62 | 63 | * Both Python 2 (**2.7.x**) and Python 3 (**3.7.x**) are supported. 64 | 65 | ## CORScanner as a library 66 | 67 | - Install CORScanner via pip 68 | 69 | ``` 70 | sudo pip install corscanner 71 | ``` 72 | 73 | or use the short name: 74 | 75 | ``` 76 | sudo pip install cors 77 | ``` 78 | 79 | - Example code: 80 | ```python 81 | >>> from CORScanner.cors_scan import cors_check 82 | >>> ret = cors_check("https://www.instagram.com", None) 83 | >>> ret 84 | {'url': 'https://www.instagram.com', 'type': 'reflect_origin', 'credentials': 'false', 'origin': 'https://evil.com', 'status_code': 200} 85 | ``` 86 | 87 | You can also use CORScanner via the `corscanner` or `cors` command: `cors -vu https://www.instagram.com` 88 | 89 | ## Usage 90 | 91 | Short Form | Long Form | Description 92 | ------------- | ------------- |------------- 93 | -u | --url | URL/domain to check it's CORS policy 94 | -d | --headers | Add headers to the request 95 | -i | --input | URL/domain list file to check their CORS policy 96 | -t | --threads | Number of threads to use for CORS scan 97 | -o | --output | Save the results to json file 98 | -v | --verbose | Enable the verbose mode and display results in realtime 99 | -T | --timeout | Set requests timeout (default 10 sec) 100 | -p | --proxy | Enable proxy (http or socks5) 101 | -h | --help | show the help message and exit 102 | 103 | ### Examples 104 | 105 | * To check CORS misconfigurations of specific domain: 106 | 107 | ``python cors_scan.py -u example.com`` 108 | 109 | * To enable more debug info, use -v: 110 | 111 | ``python cors_scan.py -u example.com -v`` 112 | 113 | * To save scan results to a JSON file, use -o: 114 | 115 | ``python cors_scan.py -u example.com -o output_filename`` 116 | 117 | * To check CORS misconfigurations of specific URL: 118 | 119 | ``python cors_scan.py -u http://example.com/restapi`` 120 | 121 | * To check CORS misconfiguration with specific headers: 122 | 123 | ``python cors_scan.py -u example.com -d "Cookie: test"`` 124 | 125 | * To check CORS misconfigurations of multiple domains/URLs: 126 | 127 | ``python cors_scan.py -i top_100_domains.txt -t 100`` 128 | 129 | * To enable proxy for CORScanner, use -p 130 | 131 | ```python cors_scan.py -u example.com -p http://127.0.0.1:8080``` 132 | 133 | To use socks5 proxy, install PySocks with `pip install PySocks` 134 | 135 | ```python cors_scan.py -u example.com -p socks5://127.0.0.1:8080``` 136 | 137 | * To list all the basic options and switches use -h switch: 138 | 139 | ```python cors_scan.py -h``` 140 | 141 | ## Misconfiguration types 142 | This tool covers the following misconfiguration types: 143 | 144 | Misconfiguration type | Description 145 | ------------------------ | -------------------------- 146 | Reflect_any_origin | Blindly reflect the Origin header value in `Access-Control-Allow-Origin headers` in responses, which means any website can read its secrets by sending cross-orign requests. 147 | Prefix_match | `wwww.example.com` trusts `example.com.evil.com`, which is an attacker's domain. 148 | Suffix_match | `wwww.example.com` trusts `evilexample.com`, which could be registered by an attacker. 149 | Not_escape_dot | `wwww.example.com` trusts `wwwaexample.com`, which could be registered by an attacker. 150 | Substring match | `wwww.example.com` trusts `example.co`, which could be registered by an attacker. 151 | Trust_null | `wwww.example.com` trusts `null`, which can be forged by iframe sandbox scripts 152 | HTTPS_trust_HTTP | Risky trust dependency, a MITM attacker may steal HTTPS site secrets 153 | Trust_any_subdomain | Risky trust dependency, a subdomain XSS may steal its secrets 154 | Custom_third_parties | Custom unsafe third parties origins like `github.io`, see more in [origins.json](./origins.json) file. Thanks [@phackt](https://github.com/phackt)! 155 | Special_characters_bypass| Exploiting browsers’ handling of special characters. Most can only work in Safari except `_`, which can also work in Chrome and Firefox. See more in [Advanced CORS Exploitation Techniques](https://www.corben.io/advanced-cors-techniques/). Thanks [@Malayke](https://github.com/Malayke). 156 | 157 | Welcome to contribute more. 158 | 159 | ## Exploitation examples 160 | Here is an example about how to exploit "Reflect_any_origin" misconfiguration on Walmart.com(fixed). Localhost is the malicious website in the video. 161 | 162 | Walmart.com video on Youtube: 163 | 164 | [![Walmart_CORS_misconfiguration_exploitation](https://github.com/chenjj/CORScanner/raw/master/images/walmart.png)](http://www.youtube.com/watch?v=3abaevsSHXY) 165 | 166 | Here is the exploitation code: 167 | ```javascript 168 | 188 | ``` 189 | 190 | If you have understood how the demo works, you can read Section 5 and Section 6 of the [CORS paper](https://www.jianjunchen.com/publication/an-empirical-study-of-cors/) and know how to exploit other misconfigurations. 191 | 192 | ## License 193 | 194 | CORScanner is licensed under the MIT license. take a look at the [LICENSE](./LICENSE) for more information. 195 | 196 | 197 | ## Credits 198 | This work is inspired by the following excellent researches: 199 | 200 | * James Kettle, “Exploiting CORS misconfigurations for Bitcoins and bounties”, AppSecUSA 2016* 201 | * Evan Johnson, “Misconfigured CORS and why web appsec is not getting easier”, AppSecUSA 2016* 202 | * Von Jens Müller, "CORS misconfigurations on a large scale", [CORStest](https://github.com/RUB-NDS/CORStest)* 203 | 204 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | sys.dont_write_bytecode = True 7 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 8 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenjj/CORScanner/593043f836a158246fc6a13a89a5a1401cbff0b5/common/__init__.py -------------------------------------------------------------------------------- /common/common.py: -------------------------------------------------------------------------------- 1 | import linecache 2 | 3 | 4 | def normalize_url(i): 5 | if '://' in i: 6 | return [i] 7 | else: 8 | return ["http://" + i, "https://" + i] 9 | 10 | def parse_headers(headers): 11 | if headers == None: 12 | return None 13 | else: 14 | parsedheaders = {} 15 | 16 | for header in headers: 17 | index = header.find(":") 18 | if index == -1: 19 | return None 20 | parsedheaders[header[0:index].strip()] = header[index+1:].strip() 21 | return parsedheaders 22 | 23 | def read_file(input_file): 24 | lines = linecache.getlines(input_file) 25 | return lines 26 | 27 | 28 | def read_urls(test_url, input_file, queue): 29 | if test_url: 30 | for u in normalize_url(test_url): 31 | queue.put(u) 32 | if input_file: 33 | lines = read_file(input_file) 34 | for i in lines: 35 | for u in normalize_url(i.strip()): 36 | queue.put(u) 37 | -------------------------------------------------------------------------------- /common/corscheck.py: -------------------------------------------------------------------------------- 1 | import gevent.monkey 2 | gevent.monkey.patch_all() 3 | 4 | import requests, json, os, inspect, tldextract 5 | 6 | from future.utils import iteritems 7 | try: 8 | from urllib.parse import urlparse 9 | except Exception as e: 10 | from urlparse import urlparse 11 | 12 | import urllib3 13 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 14 | 15 | from threading import Thread 16 | 17 | class CORSCheck: 18 | """docstring for CORSCheck""" 19 | url = None 20 | cfg = None 21 | headers = None 22 | timeout = None 23 | result = {} 24 | 25 | def __init__(self, url, cfg): 26 | self.url = url 27 | self.cfg = cfg 28 | self.timeout = cfg["timeout"] 29 | self.all_results = [] 30 | if cfg["headers"] != None: 31 | self.headers = cfg["headers"] 32 | self.proxies = {} 33 | if cfg.get("proxy") != None: 34 | self.proxies = { 35 | "http": cfg["proxy"], 36 | "https": cfg["proxy"], 37 | } 38 | 39 | def send_req(self, url, origin): 40 | try: 41 | 42 | headers = { 43 | 'Origin': 44 | origin, 45 | 'Cache-Control': 46 | 'no-cache', 47 | 'User-Agent': 48 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' 49 | } 50 | if self.headers != None: 51 | headers.update(self.headers) 52 | 53 | # self-signed cert OK, follow redirections 54 | resp = requests.get(self.url, timeout=self.timeout, headers=headers, 55 | verify=False, allow_redirects=True, proxies=self.proxies) 56 | 57 | # remove cross-domain redirections, which may cause false results 58 | first_domain =tldextract.extract(url).registered_domain 59 | last_domain = tldextract.extract(resp.url).registered_domain 60 | 61 | if(first_domain.lower() != last_domain.lower()): 62 | resp = None 63 | 64 | except Exception as e: 65 | resp = None 66 | return resp 67 | 68 | def get_resp_headers(self, resp): 69 | if resp == None: 70 | return None 71 | resp_headers = dict( 72 | (k.lower(), v) for k, v in iteritems(resp.headers)) 73 | return resp_headers 74 | 75 | def check_cors_policy(self, test_module_name,test_origin,test_url): 76 | resp = self.send_req(self.url, test_origin) 77 | resp_headers = self.get_resp_headers(resp) 78 | status_code = resp.status_code if resp is not None else None 79 | 80 | if resp_headers == None: 81 | return None 82 | 83 | parsed = urlparse(str(resp_headers.get("access-control-allow-origin"))) 84 | if test_origin != "null": 85 | resp_origin = parsed.scheme + "://" + parsed.netloc.split(':')[0] 86 | else: 87 | resp_origin = str(resp_headers.get("access-control-allow-origin")) 88 | 89 | msg = None 90 | 91 | # test_origin does not have to be case sensitive 92 | if test_origin.lower() == resp_origin.lower(): 93 | credentials = "false" 94 | 95 | if resp_headers.get("access-control-allow-credentials") == "true": 96 | credentials = "true" 97 | 98 | # Set the msg 99 | msg = { 100 | "url": test_url, 101 | "type": test_module_name, 102 | "credentials": credentials, 103 | "origin": test_origin, 104 | "status_code" : status_code 105 | } 106 | return msg 107 | 108 | def is_cors_permissive(self,test_module_name,test_origin,test_url): 109 | msg = self.check_cors_policy(test_module_name,test_origin,test_url) 110 | 111 | if msg != None: 112 | self.cfg["logger"].warning(msg) 113 | self.result = msg 114 | self.all_results.append(msg) 115 | return True 116 | 117 | self.cfg["logger"].info("nothing found for {url: %s, origin: %s, type: %s}" % (test_url, test_origin, test_module_name)) 118 | return False 119 | 120 | def test_reflect_origin(self): 121 | module_name = inspect.stack()[0][3].replace('test_',''); 122 | test_url = self.url 123 | parsed = urlparse(test_url) 124 | test_origin = parsed.scheme + "://" + "evil.com" 125 | 126 | self.cfg["logger"].info( 127 | "Start checking %s for %s" % (module_name,test_url)) 128 | 129 | return self.is_cors_permissive(module_name,test_origin,test_url) 130 | 131 | def test_prefix_match(self): 132 | module_name = inspect.stack()[0][3].replace('test_',''); 133 | test_url = self.url 134 | parsed = urlparse(test_url) 135 | test_origin = parsed.scheme + "://" + parsed.netloc.split(':')[0] + ".evil.com" 136 | 137 | self.cfg["logger"].info( 138 | "Start checking %s for %s" % (module_name,test_url)) 139 | 140 | return self.is_cors_permissive(module_name,test_origin,test_url) 141 | 142 | 143 | def test_suffix_match(self): 144 | module_name = inspect.stack()[0][3].replace('test_',''); 145 | test_url = self.url 146 | parsed = urlparse(test_url) 147 | sld = tldextract.extract(test_url.strip()).registered_domain 148 | test_origin = parsed.scheme + "://" + "evil" + sld 149 | 150 | self.cfg["logger"].info( 151 | "Start checking %s for %s" % (module_name,test_url)) 152 | 153 | return self.is_cors_permissive(module_name,test_origin,test_url) 154 | 155 | 156 | def test_trust_null(self): 157 | module_name = inspect.stack()[0][3].replace('test_',''); 158 | test_url = self.url 159 | test_origin = "null" 160 | 161 | self.cfg["logger"].info( 162 | "Start checking %s for %s" % (module_name,test_url)) 163 | 164 | return self.is_cors_permissive(module_name,test_origin,test_url) 165 | 166 | 167 | def test_include_match(self): 168 | module_name = inspect.stack()[0][3].replace('test_',''); 169 | test_url = self.url 170 | parsed = urlparse(test_url) 171 | sld = tldextract.extract(test_url.strip()).registered_domain 172 | test_origin = parsed.scheme + "://" + sld[1:] 173 | 174 | self.cfg["logger"].info( 175 | "Start checking %s for %s" % (module_name,test_url)) 176 | 177 | return self.is_cors_permissive(module_name,test_origin,test_url) 178 | 179 | 180 | def test_not_escape_dot(self): 181 | module_name = inspect.stack()[0][3].replace('test_',''); 182 | test_url = self.url 183 | parsed = urlparse(test_url) 184 | sld = tldextract.extract(test_url.strip()).registered_domain 185 | domain = parsed.netloc.split(':')[0] 186 | test_origin = parsed.scheme + "://" + domain[::-1].replace( 187 | '.', 'a', 1)[::-1] 188 | 189 | self.cfg["logger"].info( 190 | "Start checking %s for %s" % (module_name,test_url)) 191 | 192 | return self.is_cors_permissive(module_name,test_origin,test_url) 193 | 194 | 195 | def test_trust_any_subdomain(self): 196 | module_name = inspect.stack()[0][3].replace('test_',''); 197 | test_url = self.url 198 | parsed = urlparse(test_url) 199 | test_origin = parsed.scheme + "://" + "evil." + parsed.netloc.split(':')[0] 200 | 201 | self.cfg["logger"].info( 202 | "Start checking %s for %s" % (module_name,test_url)) 203 | 204 | return self.is_cors_permissive(module_name,test_origin,test_url) 205 | 206 | 207 | def test_https_trust_http(self): 208 | module_name = inspect.stack()[0][3].replace('test_',''); 209 | test_url = self.url 210 | parsed = urlparse(test_url) 211 | if parsed.scheme != "https": 212 | return 213 | test_origin = "http://" + parsed.netloc.split(':')[0] 214 | 215 | self.cfg["logger"].info( 216 | "Start checking %s for %s" % (module_name,test_url)) 217 | 218 | return self.is_cors_permissive(module_name,test_origin,test_url) 219 | 220 | 221 | def test_custom_third_parties(self): 222 | module_name = inspect.stack()[0][3].replace('test_',''); 223 | test_url = self.url 224 | parsed = urlparse(test_url) 225 | sld = tldextract.extract(test_url.strip()).registered_domain 226 | domain = parsed.netloc.split(':')[0] 227 | 228 | self.cfg["logger"].info( 229 | "Start checking %s for %s" % (module_name,test_url)) 230 | 231 | is_cors_perm = False 232 | 233 | # Opening origins file 234 | with open(os.path.join(os.path.dirname(os.path.realpath(__file__)),'..%sorigins.json' % os.sep)) as origins_file: 235 | origins = json.load(origins_file)['origins'] 236 | 237 | for test_origin in origins: 238 | 239 | is_cors_perm = self.is_cors_permissive(module_name,test_origin,test_url) 240 | if is_cors_perm: break 241 | 242 | return is_cors_perm 243 | 244 | def test_special_characters_bypass(self): 245 | module_name = inspect.stack()[0][3].replace('test_',''); 246 | test_url = self.url 247 | parsed = urlparse(test_url) 248 | special_characters = ['_','-','"','{','}','+','^','%60','!','~','`',';','|','&',"'",'(',')','*',',','$','=','+',"%0b"] 249 | 250 | origins = [] 251 | 252 | for char in special_characters: 253 | attempt = parsed.scheme + "://" + parsed.netloc.split(':')[0] + char + ".evil.com" 254 | origins.append(attempt) 255 | 256 | is_cors_perm = False 257 | 258 | self.cfg["logger"].info( 259 | "Start checking %s for %s" % (module_name,test_url)) 260 | 261 | for test_origin in origins: 262 | is_cors_perm = self.is_cors_permissive(module_name,test_origin,test_url) 263 | if is_cors_perm: break 264 | 265 | return is_cors_perm 266 | 267 | def check_one_by_one(self): 268 | functions = [ 269 | 'test_reflect_origin', 270 | 'test_prefix_match', 271 | 'test_suffix_match', 272 | 'test_trust_null', 273 | 'test_include_match', 274 | 'test_not_escape_dot', 275 | 'test_custom_third_parties', 276 | 'test_special_characters_bypass', 277 | 'test_trust_any_subdomain', 278 | 'test_https_trust_http', 279 | ] 280 | 281 | for fname in functions: 282 | func = getattr(self,fname) 283 | # Stop if we found a exploit case. 284 | if(func()): break 285 | 286 | return self.result 287 | 288 | def check_all_in_parallel(self): 289 | functions = [ 290 | 'test_reflect_origin', 291 | 'test_prefix_match', 292 | 'test_suffix_match', 293 | 'test_trust_null', 294 | 'test_include_match', 295 | 'test_not_escape_dot', 296 | 'test_custom_third_parties', 297 | 'test_special_characters_bypass', 298 | 'test_trust_any_subdomain', 299 | 'test_https_trust_http', 300 | ] 301 | 302 | threads = [] 303 | for fname in functions: 304 | func = getattr(self,fname) 305 | t = Thread(target=func) 306 | t.start() 307 | threads.append(t) 308 | 309 | for t in threads: 310 | t.join() 311 | 312 | return self.all_results -------------------------------------------------------------------------------- /common/logger.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import sys 4 | 5 | 6 | class Log: 7 | """Class Log for logging CORS misconfiguration message""" 8 | print_level = 0 9 | msg_level = {0: 'DEBUG', 1: 'INFO', 2: 'WARNING', 3: 'ALERT'} 10 | auto_timestamp = 1 11 | 12 | def __init__(self, filename, print_level, auto_timestamp=1): 13 | self.filename = filename 14 | self.print_level = print_level 15 | self.auto_timestamp = auto_timestamp 16 | 17 | def write(self, msg, level=0, auto_timestamp=1): 18 | try: 19 | if level >= self.print_level: 20 | if self.auto_timestamp == 1: 21 | timestamp = time.strftime("%Y-%m-%d %H:%M:%S", 22 | time.localtime()) 23 | record = "%s %s %s" % (timestamp, self.msg_level[level], 24 | msg) 25 | sys.stdout.write(record + "\r\n") 26 | else: 27 | sys.stdout.write(msg + "\r\n") 28 | sys.stdout.flush() 29 | except KeyboardInterrupt: 30 | self.close() 31 | 32 | def debug(self, msg): 33 | self.write(msg, 0) 34 | 35 | def info(self, msg): 36 | self.write(msg, 1) 37 | 38 | def warning(self, msg): 39 | record = "Found misconfiguration! " + json.dumps(msg) 40 | self.write("""%s%s%s""" % ('\033[91m', record, '\033[0m'), 2) 41 | 42 | def alert(self, msg): 43 | self.write(msg, 3) 44 | 45 | def close(self): 46 | if self.log: 47 | self.log.close() 48 | -------------------------------------------------------------------------------- /cors_scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import sys 5 | import os 6 | import argparse 7 | import threading 8 | 9 | from common.common import * 10 | from common.logger import Log 11 | from common.corscheck import CORSCheck 12 | 13 | import gevent 14 | from gevent import monkey 15 | monkey.patch_all() 16 | from gevent.pool import Pool 17 | from gevent.queue import Queue 18 | from colorama import init 19 | 20 | # Globals 21 | results = [] 22 | 23 | def banner(): 24 | print(("""%s 25 | ____ ___ ____ ____ ____ _ _ _ _ _ _____ ____ 26 | / ___/ _ \| _ \/ ___| / ___| / \ | \ | | \ | | ____| _ \ 27 | | | | | | | |_) \___ \| | / _ \ | \| | \| | _| | |_) | 28 | | |__| |_| | _ < ___) | |___ / ___ \| |\ | |\ | |___| _ < 29 | \____\___/|_| \_\____/ \____/_/ \_\_| \_|_| \_|_____|_| \_\ 30 | %s%s 31 | # Coded By Jianjun Chen - whucjj@gmail.com%s 32 | """ % ('\033[91m', '\033[0m', '\033[93m', '\033[0m'))) 33 | 34 | 35 | def parser_error(errmsg): 36 | banner() 37 | print(("Usage: python " + sys.argv[0] + " [Options] use -h for help")) 38 | print(("Error: " + errmsg)) 39 | sys.exit() 40 | 41 | 42 | def parse_args(): 43 | # parse the arguments 44 | parser = argparse.ArgumentParser( 45 | epilog='\tExample: \r\npython ' + sys.argv[0] + " -u google.com") 46 | parser.error = parser_error 47 | parser._optionals.title = "OPTIONS" 48 | parser.add_argument( 49 | '-u', '--url', help="URL/domain to check it's CORS policy") 50 | parser.add_argument( 51 | '-i', 52 | '--input', 53 | help='URL/domain list file to check their CORS policy') 54 | parser.add_argument( 55 | '-t', 56 | '--threads', 57 | help='Number of threads to use for CORS scan', 58 | type=int, 59 | default=50) 60 | parser.add_argument('-o', '--output', help='Save the results to json file') 61 | parser.add_argument( 62 | '-v', 63 | '--verbose', 64 | help='Enable Verbosity and display results in realtime', 65 | action='store_true', 66 | default=False) 67 | parser.add_argument('-d', '--headers', help='Add headers to the request.', default=None, nargs='*') 68 | parser.add_argument( 69 | '-T', 70 | '--timeout', 71 | help='Set requests timeout (default 5 sec)', 72 | type=int, 73 | default=10) 74 | parser.add_argument('-p', '--proxy', help='Enable proxy (http or socks5)') 75 | args = parser.parse_args() 76 | if not (args.url or args.input): 77 | parser.error("No url inputed, please add -u or -i option") 78 | if args.input and not os.path.isfile(args.input): 79 | parser.error("Input file " + args.input + " not exist.") 80 | return args 81 | 82 | 83 | # Synchronize results 84 | c = threading.Condition() 85 | 86 | def scan(cfg): 87 | log = cfg["logger"] 88 | global results 89 | 90 | while not cfg["queue"].empty(): 91 | try: 92 | item = cfg["queue"].get(timeout=1.0) 93 | cors_check = CORSCheck(item, cfg) 94 | msg = cors_check.check_one_by_one() 95 | 96 | # Keeping results to be written to file only if needed 97 | if log.filename and msg: 98 | c.acquire() 99 | results.append(msg) 100 | c.release() 101 | except Exception as e: 102 | print(e) 103 | break 104 | 105 | """ 106 | CORScanner library API interface for other projects to use. This will check 107 | the CORS policy for a given URL. Example Usage: 108 | 109 | >>> from CORScanner.cors_scan import cors_check 110 | >>> ret = cors_check("https://www.instagram.com", None) 111 | >>> ret 112 | {'url': 'https://www.instagram.com', 'type': 'reflect_origin',...} 113 | 114 | """ 115 | def cors_check(url, headers=None): 116 | # 0: 'DEBUG', 1: 'INFO', 2: 'WARNING', 3: 'ALERT', 4: 'disable log' 117 | log = Log(None, print_level=4) 118 | cfg = {"logger": log, "headers": headers, "timeout": 5} 119 | 120 | cors_check = CORSCheck(url, cfg) 121 | #msg = cors_check.check_all_in_parallel() 122 | msg = cors_check.check_one_by_one() 123 | return msg 124 | 125 | def main(): 126 | init() 127 | args = parse_args() 128 | #banner() 129 | 130 | queue = Queue() 131 | log_level = 1 if args.verbose else 2 # 1: INFO, 2: WARNING 132 | 133 | log = Log(args.output, log_level) 134 | cfg = {"logger": log, "queue": queue, "headers": parse_headers(args.headers), 135 | "timeout": args.timeout, "proxy": args.proxy} 136 | 137 | read_urls(args.url, args.input, queue) 138 | 139 | sys.stderr.write("Starting CORS scan...(Tips: this may take a while, add -v option to enable debug info)\n") 140 | sys.stderr.flush() 141 | threads = [gevent.spawn(scan, cfg) for i in range(args.threads)] 142 | 143 | try: 144 | gevent.joinall(threads) 145 | except KeyboardInterrupt as e: 146 | pass 147 | 148 | # Writing results file if output file has been set 149 | if log.filename: 150 | with open(log.filename, 'w') as output_file: 151 | output_file.write(json.dumps(results, indent=4)) 152 | output_file.close() 153 | sys.stderr.write("Finished CORS scanning...\n") 154 | sys.stderr.flush() 155 | 156 | 157 | if __name__ == '__main__': 158 | main() 159 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenjj/CORScanner/593043f836a158246fc6a13a89a5a1401cbff0b5/images/screenshot.png -------------------------------------------------------------------------------- /images/walmart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenjj/CORScanner/593043f836a158246fc6a13a89a5a1401cbff0b5/images/walmart.png -------------------------------------------------------------------------------- /origins.json: -------------------------------------------------------------------------------- 1 | { 2 | "origins":[ 3 | "https://whatever.github.io", 4 | "http://jsbin.com", 5 | "https://codepen.io", 6 | "https://jsfiddle.net", 7 | "http://www.webdevout.net", 8 | "https://repl.it" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | gevent 3 | tldextract 4 | argparse 5 | colorama 6 | future 7 | PySocks 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name='cors', 7 | version='1.0.1', 8 | description='Fast CORS misconfiguration vulnerabilities scanner', 9 | long_description=open('README.md').read(), 10 | long_description_content_type='text/markdown', 11 | author='Jianjun Chen', 12 | author_email= 'whucjj@hotmail.com', 13 | url='http://github.com/chenjj/CORScanner', 14 | project_urls={ 15 | 'Bug Reports': 'https://github.com/chenjj/CORScanner/issues', 16 | 'Source': 'https://github.com/chenjj/CORScanner/', 17 | }, 18 | license='MIT', 19 | packages=find_packages(), 20 | install_requires=['colorama', 'requests', 'argparse', 'gevent', 'tldextract', 'future', 'PySocks'], 21 | include_package_data=True, 22 | zip_safe=False, 23 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 24 | classifiers=[ 25 | 'Development Status :: 4 - Beta', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Natural Language :: English', 28 | 'Operating System :: OS Independent', 29 | 'Programming Language :: Python', 30 | 'Environment :: Console', 31 | 'Topic :: Security', 32 | ], 33 | entry_points={ 34 | 'console_scripts': [ 35 | 'cors = CORScanner.cors_scan:main', 36 | ], 37 | }, 38 | ) 39 | -------------------------------------------------------------------------------- /top_100_domains.txt: -------------------------------------------------------------------------------- 1 | google.com 2 | youtube.com 3 | facebook.com 4 | baidu.com 5 | wikipedia.org 6 | reddit.com 7 | yahoo.com 8 | qq.com 9 | taobao.com 10 | twitter.com 11 | google.co.in 12 | amazon.com 13 | sohu.com 14 | tmall.com 15 | instagram.com 16 | live.com 17 | vk.com 18 | jd.com 19 | sina.com.cn 20 | weibo.com 21 | google.co.jp 22 | yandex.ru 23 | 360.cn 24 | google.co.uk 25 | login.tmall.com 26 | google.ru 27 | google.com.br 28 | pornhub.com 29 | twitch.tv 30 | netflix.com 31 | google.com.hk 32 | linkedin.com 33 | google.de 34 | google.fr 35 | csdn.net 36 | microsoft.com 37 | t.co 38 | bing.com 39 | yahoo.co.jp 40 | office.com 41 | ebay.com 42 | google.it 43 | alipay.com 44 | google.ca 45 | mail.ru 46 | msn.com 47 | xvideos.com 48 | ok.ru 49 | microsoftonline.com 50 | google.es 51 | imgur.com 52 | aliexpress.com 53 | pages.tmall.com 54 | whatsapp.com 55 | google.com.mx 56 | imdb.com 57 | tumblr.com 58 | stackoverflow.com 59 | wordpress.com 60 | wikia.com 61 | github.com 62 | google.com.tw 63 | xhamster.com 64 | deloton.com 65 | hao123.com 66 | amazon.co.jp 67 | livejasmin.com 68 | google.com.tr 69 | blogspot.com 70 | paypal.com 71 | popads.net 72 | google.com.au 73 | apple.com 74 | bongacams.com 75 | googleusercontent.com 76 | tribunnews.com 77 | pinterest.com 78 | xnxx.com 79 | coccoc.com 80 | savefrom.net 81 | youth.cn 82 | google.pl 83 | diply.com 84 | fbcdn.net 85 | providr.com 86 | adobe.com 87 | txxx.com 88 | amazon.de 89 | dropbox.com 90 | detail.tmall.com 91 | thestartmagazine.com 92 | google.co.id 93 | pixnet.net 94 | tianya.cn 95 | quora.com 96 | bbc.co.uk 97 | cnn.com 98 | amazon.co.uk 99 | bbc.com 100 | amazonaws.com 101 | --------------------------------------------------------------------------------