├── 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 |
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 |
--------------------------------------------------------------------------------