├── requirements.txt ├── setup.cfg ├── bugscanner ├── __init__.py ├── proxy_scanner.py ├── bug_scanner.py ├── udp_scanner.py ├── ssl_scanner.py ├── __main__.py └── direct_scanner.py ├── setup.py ├── .github └── workflows │ └── python-publish.yml ├── LICENSE ├── .gitignore └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | multithreading -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | -------------------------------------------------------------------------------- /bugscanner/__init__.py: -------------------------------------------------------------------------------- 1 | from .bug_scanner import BugScanner 2 | from .direct_scanner import DirectScanner 3 | from .ssl_scanner import SSLScanner 4 | from .proxy_scanner import ProxyScanner 5 | from .udp_scanner import UdpScanner 6 | -------------------------------------------------------------------------------- /bugscanner/proxy_scanner.py: -------------------------------------------------------------------------------- 1 | from .direct_scanner import DirectScanner 2 | 3 | 4 | class ProxyScanner(DirectScanner): 5 | proxy = [] 6 | 7 | def log_replace(self, *args): 8 | super().log_replace(':'.join(self.proxy), *args) 9 | 10 | def request(self, *args, **kwargs): 11 | proxy = self.get_url(self.proxy[0], self.proxy[1]) 12 | 13 | return super().request(*args, proxies={'http': proxy, 'https': proxy}, **kwargs) 14 | -------------------------------------------------------------------------------- /bugscanner/bug_scanner.py: -------------------------------------------------------------------------------- 1 | import multithreading 2 | 3 | 4 | class BugScanner(multithreading.MultiThreadRequest): 5 | threads: int 6 | 7 | def request_connection_error(self, *args, **kwargs): 8 | return 1 9 | 10 | def request_read_timeout(self, *args, **kwargs): 11 | return 1 12 | 13 | def request_timeout(self, *args, **kwargs): 14 | return 1 15 | 16 | def convert_host_port(self, host, port): 17 | return host + (f':{port}' if bool(port not in ['80', '443']) else '') 18 | 19 | def get_url(self, host, port, uri=None): 20 | port = str(port) 21 | protocol = 'https' if port == '443' else 'http' 22 | 23 | return f'{protocol}://{self.convert_host_port(host, port)}' + (f'/{uri}' if uri is not None else '') 24 | 25 | def init(self): 26 | self._threads = self.threads or self._threads 27 | 28 | def complete(self): 29 | pass 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="bugscanner", 8 | version="0.1.8", 9 | author="aztecrabbit", 10 | author_email="ars.xda@gmail.com", 11 | description="Bug Scanner for Internet Freedom (Domain Fronting, Server Name Indication, Etc)", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/aztecrabbit/bugscanner", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | python_requires='>=3.6', 22 | entry_points={ 23 | 'console_scripts': [ 24 | 'bugscanner=bugscanner.__main__:main', 25 | ], 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /.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 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.8' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aztec Rabbit 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 | -------------------------------------------------------------------------------- /bugscanner/udp_scanner.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from .bug_scanner import BugScanner 4 | 5 | 6 | class UdpScanner(BugScanner): 7 | udp_server_host: str 8 | udp_server_port: int 9 | 10 | host_list: list 11 | 12 | def get_task_list(self): 13 | for host in self.host_list: 14 | yield { 15 | 'host': host, 16 | } 17 | 18 | def log_info(self, color, status, hostname): 19 | super().log(f'{color}{status:<6} {hostname}') 20 | 21 | def init(self): 22 | super().init() 23 | 24 | self.log_info('', 'Status', 'Host') 25 | self.log_info('', '------', '----') 26 | 27 | def task(self, payload): 28 | host = payload['host'] 29 | 30 | self.log_replace(host) 31 | 32 | bug = f'{host}.{self.udp_server_host}' 33 | 34 | G1 = self.logger.special_chars['G1'] 35 | W2 = self.logger.special_chars['W2'] 36 | 37 | try: 38 | client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 39 | 40 | client.settimeout(3) 41 | client.sendto(bug.encode(), (bug, int(self.udp_server_port))) 42 | client.recv(4) 43 | 44 | client.settimeout(5) 45 | client.sendto(bug.encode(), (bug, int(self.udp_server_port))) 46 | client.recv(4) 47 | 48 | client.settimeout(5) 49 | client.sendto(bug.encode(), (bug, int(self.udp_server_port))) 50 | client.recv(4) 51 | 52 | self.log_info(G1, 'True', host) 53 | 54 | self.task_success(host) 55 | 56 | except (OSError, socket.timeout): 57 | self.log_info(W2, '', host) 58 | 59 | finally: 60 | client.close() 61 | -------------------------------------------------------------------------------- /bugscanner/ssl_scanner.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import ssl 3 | 4 | from .bug_scanner import BugScanner 5 | 6 | 7 | class SSLScanner(BugScanner): 8 | host_list = [] 9 | 10 | def get_task_list(self): 11 | for host in self.filter_list(self.host_list): 12 | yield { 13 | 'host': host, 14 | } 15 | 16 | def log_info(self, color, status, server_name_indication): 17 | super().log(f'{color}{status:<6} {server_name_indication}') 18 | 19 | def log_info_result(self, **kwargs): 20 | G1 = self.logger.special_chars['G1'] 21 | W2 = self.logger.special_chars['W2'] 22 | 23 | status = kwargs.get('status', '') 24 | status = 'True' if status else '' 25 | server_name_indication = kwargs.get('server_name_indication', '') 26 | 27 | color = G1 if status else W2 28 | 29 | self.log_info(color, status, server_name_indication) 30 | 31 | def init(self): 32 | super().init() 33 | 34 | self.log_info('', 'Status', 'Server Name Indication') 35 | self.log_info('', '------', '----------------------') 36 | 37 | def task(self, payload): 38 | server_name_indication = payload['host'] 39 | 40 | self.log_replace(server_name_indication) 41 | 42 | response = { 43 | 'server_name_indication': server_name_indication, 44 | } 45 | 46 | try: 47 | socket_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 48 | socket_client.settimeout(5) 49 | socket_client.connect(("77.88.8.8", 443)) 50 | socket_client = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2).wrap_socket( 51 | socket_client, server_hostname=server_name_indication, do_handshake_on_connect=True 52 | ) 53 | response['status'] = True 54 | 55 | self.task_success(server_name_indication) 56 | 57 | except Exception: 58 | response['status'] = False 59 | 60 | self.log_info_result(**response) 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User 2 | *.old 3 | *.json 4 | *.stackdump 5 | 6 | bugscanner/*.txt 7 | 8 | !.gitkeep 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # SageMath parsed files 91 | *.sage.py 92 | 93 | # Environments 94 | .env 95 | .venv 96 | env/ 97 | venv/ 98 | ENV/ 99 | env.bak/ 100 | venv.bak/ 101 | 102 | # Spyder project settings 103 | .spyderproject 104 | .spyproject 105 | 106 | # Rope project settings 107 | .ropeproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bug Scanner 2 | 3 | Bug Scanner for Internet Freedom 4 | 5 | 6 | Sub Finder 7 | ---------- 8 | 9 | - [subfinder](https://github.com/projectdiscovery/subfinder) (golang) 10 | 11 | 12 | Install 13 | ------- 14 | 15 | **Sub Finder** 16 | 17 | $ go get -v -u github.com/projectdiscovery/subfinder/cmd/subfinder 18 | 19 | **Bug Scanner** 20 | 21 | $ python3 -m pip install bugscanner 22 | 23 | or 24 | 25 | $ git clone https://github.com/aztecrabbit/bugscanner 26 | $ cd bugscanner 27 | $ python3 -m pip install -r requirements.txt 28 | $ python3 setup.py install 29 | 30 | 31 | Usage 32 | ----- 33 | 34 | **Sub Finder** 35 | 36 | Scan using regular quota 37 | 38 | $ ~/go/bin/subfinder -d bug.com -o bug.com.txt 39 | 40 | **Bug Scanner** 41 | 42 | Scan only when using the website quota you want to scan 43 | 44 | `--mode direct --port 80` 45 | 46 | $ bugscanner bug.com.txt 47 | 48 | 49 | 50 | $ bugscanner bug.com.txt --port 443 51 | 52 | `--mode ssl --deep 2` 53 | 54 | $ bugscanner bug.com.txt --mode ssl 55 | 56 | 57 | 58 | $ bugscanner bug.com.txt --mode ssl --deep 3 59 | 60 | `--mode proxy --port 80` 61 | 62 | $ bugscanner bug.com.txt --mode proxy --proxy proxy.example.com:8080 63 | 64 | 65 | 66 | $ bugscanner bug.com.txt --mode proxy --proxy proxy.example.com:8080 --port 443 67 | 68 | 69 | Updating 70 | -------- 71 | 72 | $ python3 -m pip install --upgrade bugscanner 73 | 74 | 75 | Note 76 | ---- 77 | 78 | - `--mode direct` for [brainfuck psiphon pro go](https://github.com/aztecrabbit/brainfuck-psiphon-pro-go) 79 | - `--mode ssl` for server name indication [brainfuck tunnel shadowsocks](https://github.com/aztecrabbit/brainfuck-tunnel-shadowsocks) 80 | - `--mode ssl --deep n` is like this (cdn.bug.example.com : deep 2 = example.com, deep 3 = bug.example.com, etc) 81 | - `--mode proxy` for [brainfuck tunnel go](https://github.com/aztecrabbit/brainfuck-tunnel-go) or [brainfuck tunnel openvpn](https://github.com/aztecrabbit/brainfuck-tunnel-openvpn) 82 | 83 | -------------------------------------------------------------------------------- /bugscanner/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | from .direct_scanner import DirectScanner 5 | from .ssl_scanner import SSLScanner 6 | from .proxy_scanner import ProxyScanner 7 | from .udp_scanner import UdpScanner 8 | 9 | 10 | def get_arguments(): 11 | parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=52)) 12 | parser.add_argument( 13 | 'filename', 14 | help='filename', 15 | type=str, 16 | ) 17 | parser.add_argument( 18 | '--mode', 19 | help='mode', 20 | dest='mode', 21 | choices=('direct', 'proxy', 'ssl', 'udp'), 22 | type=str, 23 | default='direct', 24 | ) 25 | parser.add_argument( 26 | '--method', 27 | help='method list', 28 | dest='method_list', 29 | type=str, 30 | default='head', 31 | ) 32 | parser.add_argument( 33 | '--port', 34 | help='port list', 35 | dest='port_list', 36 | type=str, 37 | default='80', 38 | ) 39 | parser.add_argument( 40 | '--proxy', 41 | help='proxy', 42 | dest='proxy', 43 | type=str, 44 | default='', 45 | ) 46 | # parser.add_argument( 47 | # '--deep', 48 | # help='subdomain deep', 49 | # dest='deep', 50 | # type=int, 51 | # ) 52 | parser.add_argument( 53 | '--output', 54 | help='output file name', 55 | dest='output', 56 | type=str, 57 | ) 58 | parser.add_argument( 59 | '--threads', 60 | help='threads', 61 | dest='threads', 62 | type=int, 63 | ) 64 | 65 | return parser.parse_args() 66 | 67 | 68 | def main(): 69 | arguments = get_arguments() 70 | 71 | method_list = arguments.method_list.split(',') 72 | host_list = open(arguments.filename).read().splitlines() 73 | port_list = arguments.port_list.split(',') 74 | proxy = arguments.proxy.split(':') 75 | 76 | if arguments.mode == 'direct': 77 | scanner = DirectScanner() 78 | 79 | elif arguments.mode == 'ssl': 80 | scanner = SSLScanner() 81 | 82 | elif arguments.mode == 'proxy': 83 | if not proxy or len(proxy) != 2: 84 | sys.exit('--proxy host:port') 85 | 86 | scanner = ProxyScanner() 87 | scanner.proxy = proxy 88 | 89 | elif arguments.mode == 'udp': 90 | scanner = UdpScanner() 91 | scanner.udp_server_host = 'bugscanner.tppreborn.my.id' 92 | scanner.udp_server_port = '8853' 93 | 94 | else: 95 | sys.exit('Not Available!') 96 | 97 | scanner.method_list = method_list 98 | scanner.host_list = host_list 99 | scanner.port_list = port_list 100 | scanner.threads = arguments.threads 101 | scanner.start() 102 | 103 | if arguments.output: 104 | with open(arguments.output, 'w+') as file: 105 | file.write('\n'.join([ str(x) for x in scanner.success_list() ]) + '\n') 106 | 107 | 108 | if __name__ == '__main__': 109 | main() 110 | -------------------------------------------------------------------------------- /bugscanner/direct_scanner.py: -------------------------------------------------------------------------------- 1 | from .bug_scanner import BugScanner 2 | 3 | 4 | class DirectScanner(BugScanner): 5 | method_list = [] 6 | host_list = [] 7 | port_list = [] 8 | 9 | def log_info(self, **kwargs): 10 | for x in ['color', 'status_code', 'server']: 11 | kwargs[x] = kwargs.get(x, '') 12 | 13 | W2 = self.logger.special_chars['W2'] 14 | G1 = self.logger.special_chars['G1'] 15 | P1 = self.logger.special_chars['P1'] 16 | CC = self.logger.special_chars['CC'] 17 | 18 | if not kwargs['status_code']: 19 | kwargs['color'] = W2 20 | 21 | kwargs['CC'] = CC 22 | 23 | location = kwargs.get('location') 24 | 25 | if location: 26 | if location.startswith(f"https://{kwargs['host']}"): 27 | kwargs['status_code'] = f"{P1}{kwargs['status_code']:<4}" 28 | else: 29 | kwargs['host'] += f"{CC} -> {G1}{location}{CC}" 30 | 31 | messages = [] 32 | 33 | for x in ['{method:<6}', '{status_code:<4}', '{server:<22}', '{port:<4}', '{host}']: 34 | messages.append(f'{{color}}{x}{{CC}}') 35 | 36 | super().log(' '.join(messages).format(**kwargs)) 37 | 38 | def get_task_list(self): 39 | for method in self.filter_list(self.method_list): 40 | for host in self.filter_list(self.host_list): 41 | for port in self.filter_list(self.port_list): 42 | yield { 43 | 'method': method.upper(), 44 | 'host': host, 45 | 'port': port, 46 | } 47 | 48 | def init(self): 49 | super().init() 50 | 51 | self.log_info(method='Method', status_code='Code', server='Server', port='Port', host='Host') 52 | self.log_info(method='------', status_code='----', server='------', port='----', host='----') 53 | 54 | def task(self, payload): 55 | method = payload['method'] 56 | host = payload['host'] 57 | port = payload['port'] 58 | 59 | response = self.request(method, self.get_url(host, port), retry=1, timeout=3, allow_redirects=False) 60 | 61 | G1 = self.logger.special_chars['G1'] 62 | G2 = self.logger.special_chars['G2'] 63 | 64 | data = { 65 | 'method': method, 66 | 'host': host, 67 | 'port': port, 68 | } 69 | 70 | if response is not None: 71 | color = '' 72 | status_code = response.status_code 73 | server = response.headers.get('server', '') 74 | location = response.headers.get('location', '') 75 | 76 | if server in ['AkamaiGHost']: 77 | if status_code == 400: 78 | color = G1 79 | else: 80 | color = G2 81 | 82 | elif server in ['Varnish']: 83 | if status_code == 500: 84 | color = G1 85 | 86 | elif server in ['AkamaiNetStorage']: 87 | color = G2 88 | 89 | data_success = { 90 | 'color': color, 91 | 'status_code': status_code, 92 | 'server': server, 93 | 'location': location, 94 | } 95 | 96 | data = self.dict_merge(data, data_success) 97 | 98 | self.task_success(data) 99 | 100 | self.log_info(**data) 101 | --------------------------------------------------------------------------------