├── Screenshots ├── Dummy ├── antitakeover_1.PNG ├── antitakeover_2.PNG └── antitakeover_3.PNG ├── config.conf ├── LICENSE ├── README.md └── AntiTakeover.py /Screenshots/Dummy: -------------------------------------------------------------------------------- 1 | Dummy 2 | -------------------------------------------------------------------------------- /Screenshots/antitakeover_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strikergoutham/Anti-Takeover/HEAD/Screenshots/antitakeover_1.PNG -------------------------------------------------------------------------------- /Screenshots/antitakeover_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strikergoutham/Anti-Takeover/HEAD/Screenshots/antitakeover_2.PNG -------------------------------------------------------------------------------- /Screenshots/antitakeover_3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strikergoutham/Anti-Takeover/HEAD/Screenshots/antitakeover_3.PNG -------------------------------------------------------------------------------- /config.conf: -------------------------------------------------------------------------------- 1 | [Properties] 2 | CF_EMAIL = 3 | CF_MonitorSingleAccount = false 4 | CF_AccountID = 5 | Monitor_Mode = 1 6 | slack_integration = true 7 | slack_Webhook = https://hooks.slack.com/services/yourslackwebhookurl 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 strikergoutham 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anti-Takeover 2 | Anti-Takeover is a sub domain monitoring tool for (blue/purple) team / internal security team which uses cloud flare. Currently Anti-Takeover monitors more than a dozen third party services for dangling subdomain pointers. 3 | 4 | 5 | ![Anti-Takeover](/Screenshots/antitakeover_1.PNG) 6 | 7 | Anti-Takeover is a subdomain takeover monitoring tool But for Blue team/internal security team who manages DNS config on cloudflare. Currently it has capability to check 15+ external services for possible dangling/takeover issues. 8 | 9 | ## Features : 10 | 11 | > Monitors more than a dozen external service pointed CNAME records for subdomain takeover issues. 12 | 13 | > Capability to scan either a single cloudflare group or multiples one(single account). 14 | 15 | > Capability to monitor for newly added sub domains. 16 | 17 | > Integration with slack for realtime alerts/notification. 18 | 19 | ## Overview : 20 | 21 | Rough high level Overview of the tool is shown below : 22 | 23 | ![Anti-Takeover](/Screenshots/antitakeover_2.PNG) 24 | 25 | ## Setup : 26 | 27 | ### Prerequisites : 28 | 29 | >> Requires Python 3 30 | 31 | >> Runs on both Windows / Linux . 32 | 33 | >> install dependencies : 34 | ```bash 35 | pip3 install requests 36 | ``` 37 | #### setup Environment variable CF_APIKEY with the cloudflare API key. 38 | ```bash 39 | export CF_APIKEY="yourapikeyhere" 40 | ``` 41 | #### setup the required options in the config.conf file. 42 | Example Config File : 43 | 44 | ```bash 45 | [Properties] 46 | CF_EMAIL = #REQUIRED 47 | CF_MonitorSingleAccount = false #REQUIRED values : false / true ( true : monitors only single CF account. false : monitors every account associated with email ID ) 48 | CF_AccountID = #REQUIRED if CF_MonitorSingleAccount set to true 49 | Monitor_Mode = 1 #REQUIRED ( values : 1 or 2 ( 1 - complete notification , 2 - delta notification ) 50 | slack_integration = true #REQUIRED ( values : false / true (case sensitive) ) 51 | slack_Webhook = https://hooks.slack.com/services/yourslackwebhookurl #REQUIRED if slack_integration is true 52 | ``` 53 | #### Option Details : 54 | ##### CF_EMAIL - This is the email associated with the cloudflare account. 55 | 56 | ##### CF_MonitorSingleAccount - 57 | 58 | Values : 59 | > true 60 | > false 61 | Description : 62 | if set to false, one needs to provide cloud flare account ID specifically in ####CF_AccountID for which monitoring is required. By default , /its set to true. which monitors all accounts which are associated with the email. 63 | ##### CF_AccountID - 64 | 65 | Values: AccountID of the cloudflare which requires monitoring. 66 | Description : 67 | This needs to be provided if CF_MonitorSIngleAccount is set to true. 68 | ##### Monitor_Mode - 69 | Values: 70 | > 1 71 | > 2 72 | Description : 73 | if set to '1', for each scan, all the dangling/ misconfigured cname results are notified to the user. 74 | if set to '2', Only newly added cnames which are misconfigured which were not present in previous scans are notified / alerted. ( for base scan /first scan even if value is set to 2, it does a full scan.) 75 | 76 | ##### slack_integration - 77 | Values: 78 | > true 79 | > false 80 | 81 | Description : 82 | If value is set to 'true' slack alerts / notifications are trigerred. 83 | if set to 'false' slack notifications are disabled. 84 | 85 | ##### slack_Webhook - 86 | Values : slack web hook URL. 87 | 88 | Description : Slack web Hook URL generated for recieving incoming messages from anti-takeover.This is mandatory if slack_integration is set to value /'true'. 89 | 90 | ##### Note: All options are case sensitive! 91 | 92 | #### Now you are ready to run Auto-Takeover! Set it up as cron job for real time monitoring or run it as a standalone script. 93 | 94 | >> Results are stored in files named "edgecases.json" and "vulnerable.json". ( Edge case scenarios are stored in edgecases.json.) 95 | >> Removing both the files after the base scan / any scan , triggers in full scan . 96 | 97 | #### Snapshot of test results: 98 | ![Anti-Takeover](/Screenshots/antitakeover_3.PNG) 99 | 100 | 101 | Feel free to Fork the project, contribute, add new rules / notify for addition of new subdomains.( will be updated over the time.) 102 | 103 | ##### Developed with ♥️ by: Goutham Madhwaraj 104 | 105 | A big thanks to everyone who has contributed to https://github.com/EdOverflow/can-i-take-over-xyz :) 106 | ##### Do not use this tool for any malicious purpose. I am not responsible for any damage you cause / any non desirable consequences with the help of this tool. 107 | -------------------------------------------------------------------------------- /AntiTakeover.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import configparser 3 | import json 4 | import os 5 | from datetime import date 6 | 7 | 8 | config = configparser.ConfigParser() 9 | config.read('config.conf') 10 | 11 | subdom_list = ['.github.io','.animaapp.com','.bitbucket.io','.cargocollective.com','.createsend.com','.feedpress.me','.helpjuice.com', '.helpscoutdocs.com', 12 | '.intercom.help', '.myjetbrains.com', '.kinsta.cloud', '.launchrock.com', 'stats.uptimerobot.com', '.surge.sh', '.hatenablog.com', '.readme.io', 's3.amazonaws.com'] 13 | edge_list = ['.unbouncepages.com', '.map.fastly.net', '.netlify.com', '.netlify.app', '.webflow.com', '.webflow.io', '.cname.ngrok.io'] 14 | 15 | 16 | result_confirm = {} 17 | 18 | result_edge = {} 19 | 20 | CF_APIKEY = os.getenv("CF_APIKEY") 21 | CF_EMAIL = config['Properties']['CF_EMAIL'] 22 | Notification_Mode = config['Properties']['Monitor_Mode'] 23 | slack_integration = config['Properties']['slack_integration'] 24 | if slack_integration == "true": 25 | slack_webhookURL = config['Properties']['slack_Webhook'] 26 | 27 | CF_MonitorSingleAccount = config['Properties']['CF_MonitorSingleAccount'] 28 | SoloMode = 0 29 | CF_AccountID = '' 30 | cnameList = [] 31 | zoneList = [] 32 | 33 | msg1 = "[+]Dangling Subdomains which might be vulnerable to takeover(EDGE CASE) :" 34 | msg2 = "[+]Dangling Subdomains which are vulnerable to takeover :" 35 | 36 | 37 | if CF_MonitorSingleAccount == "true": 38 | SoloMode = 1 39 | CF_AccountID = config['Properties']['CF_AccountID'] 40 | 41 | CF_headers = { 42 | 'Content-Type': 'application/json', 43 | 'X-Auth-Email' : CF_EMAIL, 44 | 'X-Auth-Key' : CF_APIKEY 45 | } 46 | 47 | slack_headers = { 48 | 'Content-Type': 'application/json' 49 | } 50 | 51 | def getActiveZones(): 52 | getZOneEndpoint = "https://api.cloudflare.com/client/v4/zones" 53 | if SoloMode == 1: 54 | query = {'status': 'active', 'account.id': CF_AccountID,'match': 'all'} 55 | else: 56 | query = {'status': 'active'} 57 | response = requests.request(method='GET', url=getZOneEndpoint, params=query, headers=CF_headers) 58 | jsonResp = json.loads(response.text) 59 | 60 | if jsonResp['result_info']['total_pages'] != 0: 61 | num_pages = jsonResp['result_info']['total_pages'] 62 | for x in range(1,num_pages+1): 63 | new_query = query 64 | new_query['page'] = x 65 | response2 = requests.request(method='GET', url=getZOneEndpoint, params=new_query, headers=CF_headers) 66 | jsonResp2 = json.loads(response2.text) 67 | for y in range(0,len(jsonResp2['result'])): 68 | zoneList.append(jsonResp2['result'][y]['id']) 69 | else: 70 | print("[-]No zones found for the following account.") 71 | exit() 72 | 73 | def validateCNAME(): 74 | cname_query = {'type': 'CNAME'} 75 | global result_confirm, result_edge 76 | 77 | for record in zoneList: 78 | RecID = record 79 | DNSEndpoint = "https://api.cloudflare.com/client/v4/zones/" + record + "/dns_records" 80 | response = requests.request(method='GET', url=DNSEndpoint, params=cname_query, headers=CF_headers) 81 | jsonResp = json.loads(response.text) 82 | 83 | if "total_count" in jsonResp["result_info"]: 84 | if jsonResp["result_info"]["total_count"] != 0: 85 | num_pages = jsonResp['result_info']['total_pages'] 86 | for x in range(1, num_pages + 1): 87 | new_query2 = cname_query 88 | new_query2['page'] = x 89 | response2 = requests.request(method='GET', url=DNSEndpoint, params=new_query2,headers=CF_headers) 90 | jsonResp2 = json.loads(response2.text) 91 | for z in range(0, len(jsonResp2['result'])): 92 | for slist in subdom_list: 93 | if slist in jsonResp2["result"][z]["content"]: 94 | url1 = 'https://' + jsonResp2["result"][z]["name"] 95 | try: 96 | resp = requests.request(method='GET', url=url1, verify=False) 97 | if resp.status_code == 404: 98 | print("CNAME RECORD : " + jsonResp2["result"][z]["name"] + " Value: " + 99 | jsonResp2["result"][z]["content"]) 100 | print("sub domain is vulnerable to takeover:", url1) 101 | result_confirm[jsonResp2["result"][z]["name"]] = jsonResp2["result"][z]["content"] 102 | 103 | except requests.ConnectionError: 104 | print("[-]Unable to connect to the subdomain.") 105 | continue 106 | 107 | for vlist in edge_list: 108 | if vlist in jsonResp2["result"][z]["content"]: 109 | url1 = 'https://' + jsonResp2["result"][z]["name"] 110 | try: 111 | resp = requests.request(method='GET', url=url1, verify=False) 112 | if resp.status_code == 404: 113 | print("CNAME RECORD : " + jsonResp2["result"][z]["name"] + " Value: " + 114 | jsonResp2["result"][z]["content"]) 115 | print("Sub domain might be vulnerable to takeover(EDGE CASE):",url1,"\n") 116 | result_edge[jsonResp2["result"][z]["name"]] = jsonResp2["result"][z]["content"] 117 | 118 | except requests.ConnectionError: 119 | print("[-]Unable to connect to the subdomain.") 120 | continue 121 | 122 | else: 123 | print("\n[-]Something Wrong. Please try again later.\n") 124 | exit() 125 | 126 | 127 | def SendSLackMessage(result,msgtype): 128 | 129 | if len(result) > 0: 130 | today = date.today() 131 | Msg1 = "Scan Date : "+ str(today) + "\n" + "*"+msgtype+"*"+"\n" 132 | data = { 133 | "text": Msg1 134 | } 135 | resp = requests.request(method='POST', url=slack_webhookURL, headers=slack_headers,json=data) 136 | for key in result.keys(): 137 | Msg2 = "[+]Subdomain : "+ key + " " + "points to dangling pointer/value : " + result[key] 138 | data2 ={"text":Msg2} 139 | resp = requests.request(method='POST', url=slack_webhookURL, headers=slack_headers, json=data2) 140 | 141 | 142 | def parseResult(): 143 | if len(result_edge) > 0: 144 | if Notification_Mode == "1": 145 | if slack_integration == "true": 146 | SendSLackMessage(result_edge,msg1) 147 | if Notification_Mode == "2": 148 | if os.path.exists('edgecases.json') == False: 149 | if slack_integration == "true": 150 | SendSLackMessage(result_edge, msg1) 151 | else: 152 | with open('edgecases.json', 'r') as fp1: 153 | exist_list_edge = json.load(fp1) 154 | new_edge = {} 155 | for key in result_edge.keys(): 156 | if not key in exist_list_edge: 157 | new_edge[key] = result_edge[key] 158 | if slack_integration == "true": 159 | SendSLackMessage(new_edge, msg1) 160 | with open('edgecases.json', 'w') as wp1: 161 | json.dump(result_edge, wp1) 162 | else: 163 | if os.path.exists('edgecases.json') == True: 164 | os.remove("edgecases.json") 165 | 166 | 167 | if len(result_confirm) > 0: 168 | if Notification_Mode == "1": 169 | if slack_integration == "true": 170 | SendSLackMessage(result_confirm, msg2) 171 | if Notification_Mode == "2": 172 | if os.path.exists('vulnerable.json') == False: 173 | if slack_integration == "true": 174 | SendSLackMessage(result_confirm, msg2) 175 | else: 176 | with open('vulnerable.json', 'r') as fp2: 177 | exist_list_confirm = json.load(fp2) 178 | new_confirm = {} 179 | for key in result_confirm.keys(): 180 | if not key in exist_list_confirm: 181 | new_confirm[key] = result_confirm[key] 182 | if slack_integration == "true": 183 | SendSLackMessage(new_confirm, msg2) 184 | with open('vulnerable.json', 'w') as wp2: 185 | json.dump(result_confirm, wp2) 186 | else: 187 | if os.path.exists('vulnerable.json') == True: 188 | os.remove("vulnerable.json") 189 | 190 | 191 | if __name__ == "__main__": 192 | print(''' 193 | _ _ _______ _ 194 | /\ | | (_) |__ __| | | 195 | / \ _ __ | |_ _ | | __ _| | _____ _____ _____ _ __ 196 | / /\ \ | '_ \| __| | | |/ _` | |/ / _ \/ _ \ \ / / _ \ '__| 197 | / ____ \| | | | |_| | | | (_| | < __/ (_) \ V / __/ | 198 | /_/ \_\_| |_|\__|_| |_|\__,_|_|\_\___|\___/ \_/ \___|_| 199 | 200 | @barriersec.com''') 201 | print("[+]Fetching Zone List....") 202 | getActiveZones() 203 | print("[+]Validating CNAME Records....") 204 | validateCNAME() 205 | print("[+]Parsing result.....") 206 | parseResult() 207 | 208 | --------------------------------------------------------------------------------