├── VERSION.md ├── preview.png ├── .github └── FUNDING.yml ├── class.host.php ├── dnsexpire.php ├── LICENSE.md ├── README.md ├── class.dnsexpire.php ├── dnsexpire.py └── class.utils.php /VERSION.md: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwen001/dnsexpire/HEAD/preview.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [gwen001] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /class.host.php: -------------------------------------------------------------------------------- 1 | host; 18 | } 19 | public function setHost( $v ) { 20 | $this->host = trim( $v ); 21 | } 22 | 23 | 24 | public function getIp() { 25 | return $this->ip; 26 | } 27 | public function setIp( $v ) { 28 | $this->ip = trim( $v ); 29 | } 30 | 31 | 32 | public function setParent( $v ) { 33 | $this->parent = $v->getHost(); 34 | } 35 | public function getParent() { 36 | return $this->parent; 37 | } 38 | 39 | 40 | public function getAlias() { 41 | return $this->t_alias; 42 | } 43 | public function setAlias( $v ) { 44 | $this->is_alias = (bool)$v; 45 | } 46 | public function addAlias( $v ) { 47 | $this->t_alias[] = $v->getHost(); 48 | } 49 | public function hasAlias() { 50 | return count($this->t_alias); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /dnsexpire.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | setAlert( (int)$_SERVER['argv'][$i + 1] ); 22 | $i++; 23 | break; 24 | 25 | case '-f': 26 | $dnsexpire->setDomain( $_SERVER['argv'][$i + 1] ); 27 | $i++; 28 | break; 29 | 30 | case '-h': 31 | Utils::help(); 32 | break; 33 | 34 | default: 35 | Utils::help('Unknown option: '.$_SERVER['argv'][$i]); 36 | } 37 | } 38 | 39 | if( !$dnsexpire->getDomain() ) { 40 | Utils::help('Domain not found!'); 41 | } 42 | } 43 | // --- 44 | 45 | 46 | // main loop 47 | { 48 | $cnt_host = $dnsexpire->run(); 49 | 50 | if( $cnt_host ) { 51 | $dnsexpire->printResult(); 52 | } 53 | } 54 | // --- 55 | 56 | 57 | exit(); 58 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2022 Gwendal Le Coguic 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 |

dnsexpire

2 | 3 |

PHP tool to test CNAME expiration date of a subdomain list.

4 | 5 |

6 | python badge 7 | php badge 8 | MIT license badge 9 | twitter badge 10 |

11 | 12 | 17 | 18 | --- 19 | 20 | ## Install 21 | 22 | ``` 23 | git clone https://github.com/gwen001/dnsexpire 24 | ``` 25 | 26 | ## Usage 27 | 28 | ``` 29 | Usage: php dnsexpire.php [OPTIONS] -f 30 | 31 | Options: 32 | -a set alert for result output, default=30 days 33 | -f subdomains list source file 34 | -h print this help 35 | 36 | Examples: 37 | php dnsexpire.php -f example.com 38 | php dnsexpire.php -a 10 -f dns.txt 39 | ``` 40 | 41 | ## Note 42 | 43 | `dnsexpire.py` is the Python version of the script. 44 | It's much faster but not sure how reliable it is. 45 | 46 | ``` 47 | $ python3 dnsexpire.py -o 48 | ``` 49 | 50 | ``` 51 | usage: dnsexpire.py [-h] [-a] [-o HOST] [-t THREADS] [-v VERBOSE] 52 | 53 | options: 54 | -h, --help show this help message and exit 55 | -a, --all also test dead hosts and non alias 56 | -o HOST, --host HOST set host, can be a file or single host 57 | -t THREADS, --threads THREADS 58 | threads, default 10 59 | -v VERBOSE, --verbose VERBOSE 60 | display output, can be: 0=everything, 1=only alias, 2=only possible vulnerable, default 1 61 | ``` 62 | 63 | --- 64 | 65 | 66 | 67 | --- 68 | 69 | Feel free to [open an issue](/../../issues/) if you have any problem with the script. 70 | 71 | -------------------------------------------------------------------------------- /class.dnsexpire.php: -------------------------------------------------------------------------------- 1 | input_file ) { 22 | return $this->input_file; 23 | } else { 24 | return $this->domain; 25 | } 26 | } 27 | public function setDomain( $v ) { 28 | $v = trim( $v ); 29 | if( is_file($v) ) { 30 | $this->input_file = $v; 31 | } else { 32 | $this->domain = $v; 33 | } 34 | return true; 35 | } 36 | 37 | 38 | public function rHost() { 39 | return $this->r_host; 40 | } 41 | 42 | 43 | public function setAlert( $v ) { 44 | $v = (int)$v; 45 | if( $v > 0 ) { 46 | $this->alert = (int)$v * 60*60*24; 47 | return true; 48 | } else { 49 | return false; 50 | } 51 | } 52 | 53 | 54 | private function addHost( $o ) { 55 | $this->r_host[ $o->getHost() ] = $o; 56 | } 57 | 58 | 59 | public function run() 60 | { 61 | if( $this->input_file ) { 62 | echo "Loading data file...\n"; 63 | $t_result = file( $this->input_file, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES ); 64 | } else { 65 | $t_result = [ $this->domain ]; 66 | } 67 | 68 | echo "\n"; 69 | 70 | echo "Looking for aliases...\n"; 71 | foreach( $t_result as $h ) { 72 | $this->computeHost( $h ); 73 | } 74 | 75 | $cnt_host = count( $this->r_host ); 76 | echo $cnt_host." host found.\n\n"; 77 | 78 | echo "Looking for expiration dates...\n"; 79 | if( $cnt_host ) { 80 | $this->getExpire(); 81 | } 82 | 83 | echo "\n"; 84 | 85 | return $cnt_host; 86 | } 87 | 88 | 89 | private function computeHost( $host, $son=null ) 90 | { 91 | if( isset($this->r_host[$host]) ) { 92 | return $this->r_host[$host]; 93 | } 94 | 95 | exec( 'host -t CNAME '.$host, $tmp ); 96 | usleep( 300000 ); 97 | // var_dump($tmp); 98 | $tmp = implode( "\n", $tmp ); 99 | 100 | $m = preg_match( '#not found: 3(NXDOMAIN)#', $tmp ); 101 | if( $m ) { 102 | return false; 103 | } 104 | 105 | //preg_match( '#.* has address (.*)#i', $tmp, $matches ); 106 | 107 | $o = new Host(); 108 | $o->setHost( $host ); 109 | //$o->setIp( $matches[1] ); 110 | 111 | if( $son ) { 112 | $o->addAlias( $son ); 113 | } 114 | 115 | preg_match( '#(.*) is an alias for (.*)#i', $tmp, $matches ); 116 | 117 | if( count($matches) ) { 118 | // this host is an alias 119 | $o->setAlias( true ); 120 | $gg = $this->computeHost( trim($matches[2],'.'), $o ); 121 | $o->setParent( $gg ); 122 | } 123 | 124 | $this->addHost( $o ); 125 | 126 | return $o; 127 | } 128 | 129 | 130 | public function getExpire() 131 | { 132 | // var_dump($this->r_host); 133 | foreach( $this->r_host as $host ) 134 | { 135 | $domain = Utils::extractDomain( $host->getHost() ); 136 | // echo $host->getHost().' -> '.$domain."\n"; 137 | 138 | if( $domain !== false && !isset($this->t_expire[$domain]) ) { 139 | $whois = ''; 140 | $this->t_expire[$domain] = array('date' => '', 'host' => $host); 141 | echo 'WHOIS ' . $domain . "\n"; 142 | exec( 'whois ' . $domain, $whois ); 143 | // var_dump($whois); 144 | $str_whois = implode( "\n", $whois ); 145 | usleep( 500000 ); 146 | 147 | if( preg_match('#No match for "'.strtoupper($domain).'"\.#',$str_whois) ) { 148 | $this->t_expire[$domain]['date'] = 'closed'; 149 | } else { 150 | $k = Utils::_array_search( $whois, $this->t_expire_string ); 151 | if( $k !== false ) { 152 | $this->t_expire[$domain]['date'] = trim( preg_replace('#\s+#', ' ', $whois[$k]) ); 153 | } 154 | } 155 | } 156 | } 157 | 158 | ksort( $this->t_expire ); 159 | } 160 | 161 | 162 | public function printResult() 163 | { 164 | $current = time(); 165 | 166 | foreach( $this->t_expire as $h=>$d ) 167 | { 168 | echo 'Domain: '.$h.' '; 169 | $info = ''; 170 | $time = self::_strtotime( $d['date'] ); 171 | 172 | if( (!$d['date'] || $d['date'] == '' || !(int)$time) && $d['date']!='closed' ) { 173 | $color = 'dark_grey'; 174 | $txt = '*date not found*'; 175 | } 176 | else { 177 | $txt = trim( $d['date'] ); 178 | $txt .= ' ('.date('d/m/Y',$time).')'; 179 | 180 | if ($current > $time || $d['date']=='closed' ) { 181 | $color = 'red'; 182 | $txt .= ' -> BAZINGA !!!'; 183 | } elseif( ($current+$this->alert) > $time ) { 184 | $color = 'yellow'; 185 | $txt .= ' -> ACHTUNG !'; 186 | } else { 187 | $color = 'green'; 188 | $txt .= ' -> OK'; 189 | } 190 | } 191 | 192 | Utils::_print( $txt, $color ); 193 | 194 | $info = $this->getAliasPath( $d['host']->getHost() ); 195 | if( count($info) ) { 196 | echo "\n"; 197 | foreach( $info as $i ) { 198 | echo "\t-> ".$i."\n"; 199 | } 200 | } 201 | echo "\n"; 202 | } 203 | 204 | echo "Done.\n\n"; 205 | } 206 | 207 | 208 | private function getAliasPath( $host, $path=array() ) 209 | { 210 | $path[] = $host; 211 | 212 | foreach( $this->r_host[$host]->getAlias() as $a ) { 213 | $path = $this->getAliasPath( $a, $path ); 214 | } 215 | 216 | return $path; 217 | } 218 | 219 | 220 | private static function _strtotime( $str ) 221 | { 222 | if( strstr($str,':') ) { 223 | list($null, $str) = explode(':', $str); 224 | } 225 | if( strstr($str,'T') ) { 226 | list($str,$null) = explode('T', $str); 227 | } 228 | 229 | $str = trim( $str ); 230 | 231 | if( strstr($str,' ') ) { 232 | list($str,$null) = explode(' ', $str); 233 | } 234 | 235 | $str = str_replace( '.', '-', $str ); 236 | $str = str_replace( 'UTC', '', $str ); 237 | 238 | if( strstr($str,'/') ) { 239 | list($d,$m,$y) = explode( '/', $str ); 240 | $str = $y.'-'.$m.'-'.$d; 241 | } 242 | 243 | $time = strtotime( $str ); 244 | return $time; 245 | } 246 | } 247 | 248 | -------------------------------------------------------------------------------- /dnsexpire.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | import re 6 | import socket 7 | # import whois 8 | import pythonwhois 9 | import subprocess 10 | import argparse 11 | import tldextract 12 | from colored import fg, bg, attr 13 | from datetime import datetime 14 | from threading import Thread 15 | from queue import Queue 16 | from multiprocessing.dummy import Pool 17 | 18 | 19 | def banner(): 20 | print(""" 21 | _ _ 22 | __| |_ __ ___ _____ ___ __ (_)_ __ ___ _ __ _ _ 23 | / _` | '_ \/ __|/ _ \ \/ / '_ \| | '__/ _ \ | '_ \| | | | 24 | | (_| | | | \__ \ __/> <| |_) | | | | __/ _ | |_) | |_| | 25 | \__,_|_| |_|___/\___/_/\_\ .__/|_|_| \___| (_) | .__/ \__, | 26 | |_| |_| |___/ 27 | 28 | by @gwendallecoguic 29 | 30 | """) 31 | pass 32 | 33 | banner() 34 | 35 | 36 | ALERT_LIMIT = 30 37 | 38 | parser = argparse.ArgumentParser() 39 | parser.add_argument( "-a","--all",help="also test dead hosts and non alias", action="store_true" ) 40 | parser.add_argument( "-o","--host",help="set host, can be a file or single host" ) 41 | parser.add_argument( "-t","--threads",help="threads, default 10" ) 42 | parser.add_argument( "-v","--verbose",help="display output, can be: 0=everything, 1=only alias, 2=only possible vulnerable, default 1" ) 43 | parser.parse_args() 44 | args = parser.parse_args() 45 | 46 | if args.threads: 47 | _threads = int(args.threads) 48 | else: 49 | _threads = 10 50 | 51 | if args.verbose: 52 | _verbose = int(args.verbose) 53 | else: 54 | _verbose = 1 55 | 56 | if args.all: 57 | _testall = True 58 | else: 59 | _testall = False 60 | 61 | t_hosts = [] 62 | if args.host: 63 | if os.path.isfile(args.host): 64 | fp = open( args.host, 'r' ) 65 | t_hosts = fp.read().strip().split("\n") 66 | fp.close() 67 | else: 68 | t_hosts = [args.host] 69 | 70 | n_host = len(t_hosts) 71 | 72 | if not n_host: 73 | parser.error( 'hosts list missing' ) 74 | 75 | sys.stdout.write( '%s[+] %d hosts loaded: %s%s\n' % (fg('green'),n_host,args.host,attr(0)) ) 76 | sys.stdout.write( '[+] resolving...\n\n' ) 77 | 78 | 79 | def resolve( host ): 80 | try: 81 | cmd = 'host ' + host 82 | # print(cmd) 83 | output = subprocess.check_output( cmd, stderr=subprocess.STDOUT, shell=True ).decode('utf-8') 84 | # print( output ) 85 | except Exception as e: 86 | # sys.stdout.write( "%s[-] error occurred: %s%s\n" % (fg('red'),e,attr(0)) ) 87 | output = '' 88 | 89 | return output 90 | 91 | 92 | def getDomain( host ): 93 | t_host_parse = tldextract.extract( host ) 94 | return t_host_parse.domain + '.' + t_host_parse.suffix 95 | 96 | 97 | def getWhois( domain ): 98 | if not domain in t_whois_history: 99 | try: 100 | w = pythonwhois.get_whois( domain ) 101 | # w = whois.whois( domain ) 102 | t_whois_history[ domain ] = w 103 | except Exception as e: 104 | # sys.stdout.write( "%s[-] error occurred: %s (%s)%s\n" % (fg('red'),e,domain,attr(0)) ) 105 | return False 106 | 107 | return t_whois_history[domain] 108 | 109 | 110 | def getExpirationDate( domain ): 111 | whois = getWhois( domain ) 112 | # print(type(whois)) 113 | 114 | if not type(whois) is bool and 'expiration_date' in whois: 115 | # if type(whois.expiration_date) is list: 116 | # return whois.expiration_date[0] 117 | # else: 118 | # return whois.expiration_date 119 | if type(whois['expiration_date']) is list: 120 | return whois['expiration_date'][0] 121 | else: 122 | return whois['expiration_date'] 123 | return False 124 | else: 125 | return False 126 | 127 | 128 | def getColor( expiration_date ): 129 | # expiration_date = datetime(2019, 12, 29, 6, 56, 55) 130 | timedelta = expiration_date - datetime.now() 131 | # print(timedelta) 132 | 133 | if timedelta.days < -1: # to avoid false positive from smart whois who always return the current date 134 | return 'light_red' 135 | elif timedelta.days < ALERT_LIMIT: 136 | return 'light_yellow' 137 | else: 138 | return 'light_green' 139 | 140 | 141 | def printExpirationDate( domain ): 142 | expiration_date = getExpirationDate( domain ) 143 | # print(type(expiration_date)) 144 | 145 | if type(expiration_date) is datetime: 146 | color = getColor( expiration_date ) 147 | if color == 'light_red': 148 | alert = 'TRY TAKEOVER!!' 149 | elif color == 'light_yellow': 150 | alert = 'WARNING!' 151 | else: 152 | alert = '' 153 | return '%s%s %s%s' % (fg(color),expiration_date,alert,attr(0)) 154 | else: 155 | return '%snot found%s' % (fg('red'),attr(0)) 156 | 157 | 158 | def dnsexpire( host ): 159 | # sys.stdout.write( 'progress: %d/%d\r' % (t_multiproc['n_current'],t_multiproc['n_total']) ) 160 | t_multiproc['n_current'] = t_multiproc['n_current'] + 1 161 | output = '' 162 | 163 | resolution = resolve( host ) 164 | if resolution == '': 165 | is_alias = False 166 | if not _testall: 167 | if not _verbose: 168 | sys.stdout.write( '%s%s doesn\'t resolve%s\n' % (fg('dark_gray'),host,attr(0)) ) 169 | return 170 | else: 171 | is_alias = re.findall( r'(.*) is an alias for (.*)\.', resolution ); 172 | # print(is_alias) 173 | 174 | if not _testall and not is_alias: 175 | if not _verbose: 176 | sys.stdout.write( '%s%s is not an alias%s\n' % (fg('dark_gray'),host,attr(0)) ) 177 | return 178 | 179 | if _testall: 180 | domain = getDomain( host ) 181 | output = output + "%s -> %s -> " % (host,domain) 182 | output = output + printExpirationDate( domain ) 183 | 184 | if is_alias: 185 | for alias in is_alias: 186 | domain = getDomain( alias[1] ) 187 | output = output + ("%s is an alias for %s -> %s -> " % (alias[0],alias[1],domain)) 188 | output = output + printExpirationDate( domain ) 189 | 190 | if _verbose < 2 or ('WARNING' in output or 'TAKEOVER' in output): # remove the "progress:" text 191 | sys.stdout.write( '\n%s\n' % output ) 192 | # sys.stdout.write( '%s\n%s' % (' '.rjust(100,' '),output) ) 193 | 194 | # if not _testall: 195 | # sys.stdout.write( '\n' ) 196 | 197 | 198 | def doWork(): 199 | while True: 200 | host = q.get() 201 | dnsexpire( host ) 202 | q.task_done() 203 | 204 | 205 | 206 | t_whois_history = {} 207 | t_multiproc = { 208 | 'n_current': 0, 209 | 'n_total': n_host 210 | } 211 | 212 | q = Queue( _threads*2 ) 213 | 214 | for i in range(_threads): 215 | t = Thread( target=doWork ) 216 | t.daemon = True 217 | t.start() 218 | 219 | try: 220 | for host in t_hosts: 221 | q.put( host ) 222 | q.join() 223 | except KeyboardInterrupt: 224 | sys.exit(1) 225 | 226 | 227 | sys.stdout.write( '\n%s[+] finished%s\n' % (fg('green'),attr(0)) ) 228 | 229 | exit() 230 | 231 | -------------------------------------------------------------------------------- /class.utils.php: -------------------------------------------------------------------------------- 1 | '0', 12 | 'black' => '0;30', 13 | 'red' => '0;31', 14 | 'green' => '0;32', 15 | 'orange' => '0;33', 16 | 'blue' => '0;34', 17 | 'purple' => '0;35', 18 | 'cyan' => '0;36', 19 | 'light_grey' => '0;37', 20 | 'dark_grey' => '1;30', 21 | 'light_red' => '1;31', 22 | 'light_green' => '1;32', 23 | 'yellow' => '1;33', 24 | 'light_blue' => '1;34', 25 | 'light_purple' => '1;35', 26 | 'light_cyan' => '1;36', 27 | 'white' => '1;37', 28 | ); 29 | 30 | 31 | public static function help( $error='' ) 32 | { 33 | if( is_file(__DIR__.'/README.md') ) { 34 | $help = file_get_contents( __DIR__.'/README.md' )."\n"; 35 | preg_match_all( '#```(.*)```#s', $help, $matches ); 36 | if( count($matches[1]) ) { 37 | echo trim($matches[1][0])."\n\n"; 38 | } 39 | } else { 40 | echo "No help found!\n"; 41 | } 42 | 43 | if( $error ) { 44 | echo "Error: ".$error."!\n"; 45 | } 46 | 47 | exit(); 48 | } 49 | 50 | 51 | public static function isIp( $str ) { 52 | return filter_var( $str, FILTER_VALIDATE_IP ); 53 | } 54 | 55 | 56 | public static function isEmail( $str ) 57 | { 58 | return filter_var( $str, FILTER_VALIDATE_EMAIL ); 59 | } 60 | 61 | 62 | public static function _print( $str, $color ) 63 | { 64 | echo "\033[".self::T_SHELL_COLORS[$color]."m".$str."\033[0m"; 65 | } 66 | 67 | 68 | public static function _array_search( $array, $search, $ignore_case=true ) 69 | { 70 | if( $ignore_case ) { 71 | $f = 'stristr'; 72 | } else { 73 | $f = 'strstr'; 74 | } 75 | 76 | if( !is_array($search) ) { 77 | $search = array( $search ); 78 | } 79 | 80 | foreach( $array as $k=>$v ) { 81 | foreach( $search as $str ) { 82 | if( $f($v, $str) ) { 83 | return $k; 84 | } 85 | } 86 | } 87 | 88 | return false; 89 | } 90 | 91 | 92 | public static function extractDomain( $host ) 93 | { 94 | $t_host = explode( '.', $host ); 95 | // var_dump( $t_host ); 96 | $cnt = count($t_host) - 1; 97 | // var_dump( $cnt ); 98 | 99 | if( in_array('.'.$t_host[$cnt-1].'.'.$t_host[$cnt],self::T_TLD_2) ) { 100 | $domain = $t_host[$cnt-2].'.'.$t_host[$cnt-1].'.'.$t_host[$cnt]; 101 | } else if( in_array('.'.$t_host[$cnt],self::T_TLD_1) ) { 102 | $domain = $t_host[$cnt-1].'.'.$t_host[$cnt]; 103 | } else { 104 | $domain = false; 105 | } 106 | 107 | // var_dump( $domain ); 108 | return $domain; 109 | } 110 | } 111 | --------------------------------------------------------------------------------