ronin-rb/ronin-exploits

View on GitHub
lib/ronin/exploits/exploit.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true
#
# ronin-exploits - A Ruby library for ronin-rb that provides exploitation and
# payload crafting functionality.
#
# Copyright (c) 2007-2023 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-exploits is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-exploits is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-exploits.  If not, see <https://www.gnu.org/licenses/>.
#

require 'ronin/exploits/registry'
require 'ronin/exploits/advisory'
require 'ronin/exploits/test_result'
require 'ronin/exploits/exceptions'
require 'ronin/core/metadata/id'
require 'ronin/core/metadata/authors'
require 'ronin/core/metadata/summary'
require 'ronin/core/metadata/description'
require 'ronin/core/metadata/references'
require 'ronin/core/params/mixin'
require 'ronin/support/cli/printing'
require 'ronin/post_ex'

require 'chars/char_set'

module Ronin
  module Exploits
    #
    # The {Exploit} class allows for describing exploits for security
    # vulnerabilities, purely in Ruby. Exploits contain metadata about the
    # exploit/vulnerability and methods which defines the functionality
    # of the exploit. Exploits may also include additional
    # {Mixins mixin modules} to add additional functionality, such as defining
    # targets or loading in a payload.
    #
    # ## Philosophy
    #
    # Exploits are just programs with steps to build and launch the exploit.
    # Exploits also typically contain metadata that describes the exploit's
    # author(s), release date, what the exploit does, etc.
    #
    # The {Exploit} class defines six key parts:
    #
    # 1. Metadata - defines information about the exploit.
    # 2. [Params] - user configurable parameters.
    # 3. {Exploit#test test} - optional method that tests whether the target is
    #    vulnerable or not.
    # 4. {Exploit#build build} - method which builds the exploit.
    # 5. {Exploit#launch launch} - method which launches the exploit.
    # 6. {Exploit#cleanup cleanup} - optional Method which performs additional
    #    cleanup steps.
    #
    # [Params]: https://ronin-rb.dev/docs/ronin-core/Ronin/Core/Params/Mixin.html
    #
    # ## Example
    #
    #     require 'ronin/exploits/exploit'
    #     require 'ronin/exploits/mixins/remote_tcp'
    #
    #     module Ronin
    #       module Exploits
    #         class MyExploit < Exploit
    #
    #           include Mixins::RemoteTCP
    #
    #           register 'my_exploit'
    #
    #           summary 'My first exploit'
    #           description <<~EOS
    #             This is my first exploit.
    #             Bla bla bla bla.
    #           EOS
    #
    #           author '...'
    #           author '...', email: '...', twitter: '...'
    #
    #           disclosure_date 'YYY-MM-DD'
    #           release_date 'YYYY-MM-DD'
    #
    #           advisory 'CVE-YYYY-NNNN'
    #           advisory 'GHSA-XXXXXX'
    #           software 'TestHTTP'
    #           software_versions '1.0.0'..'1.5.4'
    #
    #           param :cmd, desc: 'The command to run'
    #
    #           def test
    #             # ...
    #           end
    #
    #           def build
    #             # ...
    #           end
    #
    #           def launch
    #             # ...
    #           end
    #
    #           def cleanup
    #             # ...
    #           end
    #
    #         end
    #       end
    #     end
    #
    # ### register
    #
    # Registers the exploit with {Exploits}.
    #
    #     register 'my_exploit'
    #
    # ### quality
    #
    # Defines the quality level of the exploit. Accepted values are:
    #
    # * `:testing`
    # * `:poc`
    # * `:weaponized`
    #
    #     quality :poc
    #
    # ### summary
    #
    # Defines a short one-sentence description of the exploit.
    #
    #     summary 'My first exploit'
    #
    # ### description
    #
    # Defines a longer multi-paragraph description of the exploit.
    #
    #     description <<~EOS
    #       This is my first exploit.
    #       Bla bla bla bla.
    #     EOS
    #
    # **Note:** that `<<~` heredoc, unlike the regular `<<` heredoc, removes
    # leading whitespace.
    #
    # ### author
    #
    # Add an author's name and additional information to the exploit.
    #
    #     author 'John Smith'
    #
    #     author 'doctor_doom', email: '...', twitter: '...'
    #
    # ### software
    #
    # Defines the software which the exploit targets.
    #
    #     software 'TestApp'
    #
    # ### software_versions
    #
    # Defines the software versions which the exploit targets:
    #
    #     software_versions %w[
    #       1.0.0
    #       1.0.1
    #       1.0.2
    #       1.1.0
    #     ]
    #
    #     software_versions '1.0.0'..'1.5.4'
    #
    # ### param
    #
    # Defines a user configurable param. Params may have a type class, but
    # default to `String`. Params must have a one-line description.
    #
    #     param :str, desc: 'A basic string param'
    #
    #     param :feature_flag, Boolean, desc: 'A boolean param'
    #
    #     param :enum, Enum[:one, :two, :three],
    #                  desc: 'An enum param'
    #
    #     param :num1, Integer, desc: 'An integer param'
    #
    #     param :num2, Integer, default: 42,
    #                          desc: 'A param with a default value'
    #
    #     param :num3, Integer, default: ->{ rand(42) },
    #                           desc: 'A param with a dynamic default value'
    #
    #     param :float, Float, 'Floating point param'
    #
    #     param :url, URI, desc: 'URL param'
    #
    #     param :pattern, Regexp, desc: 'Regular Expression param'
    #
    # Params may then be accessed in instance methods using `params` Hash.
    #
    #     param :padding, Integer, desc: 'Amount of additional padding'
    #
    #     def build
    #       # ...
    #
    #       if params[:padding]
    #         @buffer << 'A' * params[:padding]
    #       end
    #     end
    #
    # ### test
    #
    # The method which may define tests which confirm whether the target is
    # vulnerable. The method must return a {Exploit#Vulnerable Vulnerable},
    # {Exploit#NotVulnerable NotVulnerable}, or an {Exploit#Unknown} object.
    #
    #     def test
    #       case http.get_body('/')
    #       when /Powered by Foo 4\.19\./
    #         Vulnerable('host is vulnerable')
    #       when /Powered by Foo 4\.2[0-9]\./
    #         NotVulnerable('host is patched')
    #       else
    #         Unknown('cannot determine whether the host is vulnerable or not')
    #       end
    #     end
    #
    # ### build
    #
    # The method which defines the logic that builds the exploit before
    # launching it.
    #
    #     def build
    #       @buffer = "..."
    #       @buffer << "..."
    #     end
    #
    # ### launch
    #
    # The method which launches the built exploit against the target.
    #
    #     def launch
    #       @socket = tcp_connect do |socket|
    #         socket.write(@buffer)
    #       end
    #     end
    #
    # ### cleanup
    #
    # The method which defines additional cleanup tasks after the exploit has
    # successfully launched and any post-exploitation tasks have been completed.
    #
    #     def cleanup
    #       @socket.close
    #     end
    #
    class Exploit

      include Core::Metadata::ID
      include Core::Metadata::Authors
      include Core::Metadata::Summary
      include Core::Metadata::Description
      include Core::Metadata::References
      include Core::Params::Mixin
      include Support::CLI::Printing

      #
      # Registers the exploit with the given name.
      #
      # @param [String] exploit_id
      #   The exploit's `id`.
      #
      # @example
      #   register 'my_exploit'
      #
      # @api public
      #
      def self.register(exploit_id)
        id(exploit_id)
        Exploits.register(exploit_id,self)
      end

      #
      # Gets or sets the quality of the exploit.
      #
      # @param [:testing, :poc, :weaponized, nil] new_quality
      #   The optional new quality to set.
      #
      # @return [:testing, :poc, :weaponized, nil]
      #   The exploit's quality.
      #
      # @api public
      #
      def self.quality(new_quality=nil)
        if new_quality then @new_quality = new_quality
        else                @new_quality
        end
      end

      #
      # Gets or sets the release date for the exploit.
      #
      # @param [String, nil] new_date
      #   The optional new release date to set.
      #
      # @return [Date, nil]
      #   The exploit's release date.
      #
      def self.release_date(new_date=nil)
        if new_date then @release_date = Date.parse(new_date)
        else             @release_date
        end
      end

      #
      # Determines whether the exploit has been publicly released yet.
      #
      # @return [Boolean]
      #
      def self.released?
        !release_date.nil?
      end

      #
      # Gets or sets the disclosure date for the exploit.
      #
      # @param [String, nil] new_date
      #   The optional new disclosure date to set.
      #
      # @return [Date, nil]
      #   The exploit's disclosure date.
      #
      # @example
      #   disclosure_date '2022-04-20'
      #
      def self.disclosure_date(new_date=nil)
        if new_date then @disclosure_date = Date.parse(new_date)
        else             @disclosure_date
        end
      end

      #
      # Determines whether the exploit has been disclosed yet.
      #
      # @return [Boolean]
      #
      def self.disclosed?
        !disclosure_date.nil?
      end

      #
      # The advisory IDs for the exploit.
      #
      # @return [Set<Advisory>]
      #   The set of advisories for the exploit.
      #
      # @api semipublic
      #
      def self.advisories
        @advisories ||= Set.new
      end

      #
      # Adds an advisory for the exploit.
      #
      # @param [String] id
      #   The advisory ID.
      #
      # @param [String] url
      #   The optional advisory URL. If the advisory `id` begins with `CVE-`
      #   or `GHSA-`, then the URL will automatically be derived from the `id`.
      #
      # @api public
      #
      def self.advisory(id,url=Advisory.url_for(id))
        advisories << Advisory.new(id,url)
      end

      #
      # Gets or sets the software which the exploit targets.
      #
      # @param [String, nil] new_software
      #   the optional new software name to set.
      #
      # @return [String, nil]
      #   The name of the software which the exploit targets.
      #
      # @api public
      #
      def self.software(new_software=nil)
        if new_software
          @software = new_software
        else
          @software ||= if superclass < Exploit
                          superclass.software
                        end
        end
      end

      #
      # Gets or sets the software version(s) which the exploit targets.
      #
      # @param [Array<String>, Range<String,String>, nil] new_software_versions
      #   the optional new software version(s) to set.
      #
      # @return [Array<String>, Range<String,String>, nil]
      #   The name of the software version which the exploit targets.
      #
      # @api public
      #
      def self.software_versions(new_software_versions=nil)
        if new_software_versions
          @software_versions = new_software_versions
        else
          @software_versions ||= if superclass < Exploit
                                   superclass.software_versions
                                 end
        end
      end

      #
      # Returns the type or kind of exploit.
      #
      # @return [Symbol]
      #
      # @note
      #   This is used internally to map an exploit class to a printable type.
      #
      # @api private
      #
      def self.exploit_type
        :exploit
      end

      #
      # Initializes the exploit.
      #
      # @param [Hash{Symbol => Object}] kwargs
      #   Additional keyword arguments.
      #
      # @option kwargs [Hash{Symbol => Object}] :params
      #   The param values for the exploit.
      #
      def initialize(**kwargs)
        super(**kwargs)
      end

      #
      # Initializes and runs the exploit.
      #
      # @param [Hash{Symbol => Object}] kwargs
      #   Additional keyword arguments for {#initialize}.
      #
      # @yield [exploit]
      #   If a block is given, it will be yielded the exploit after it has been
      #   launched. Once the block has returned, {#cleanup} will automatically
      #   be called.
      #
      # @yieldparam [Exploit] exploit
      #   The launched exploit.
      #
      # @return [Exploit]
      #   The launched exploit.
      #
      # @since 1.0.0
      #
      def self.exploit(**kwargs,&block)
        new(**kwargs).exploit(&block)
      end

      #
      # Validates that the exploit is ready to be used.
      #
      # @raise [Ronin::Core::Params::RequiredParam]
      #   One of the required params was not set.
      #
      # @raise [ValidationError]
      #   Another kind of validation error occurred.
      #
      # @api semipublic
      #
      def perform_validate
        validate_params
        validate
      end

      #
      # Tests whether the target is vulnerable or not.
      #
      # @api semipublic
      #
      # @since 1.0.0
      #
      def perform_test
        test
      end

      #
      # Builds the exploit.
      #
      # @api semipublic
      #
      # @since 1.0.0
      #
      def perform_build
        build
      end

      #
      # Launches the exploit.
      #
      # @api semipublic
      #
      # @since 1.0.0
      #
      def perform_launch
        launch
      end

      #
      # Cleans up the exploit.
      #
      # @api semipublic
      #
      # @since 1.0.0
      #
      def perform_cleanup
        cleanup
      end

      #
      # Builds the exploit and then launches the exploit.
      #
      # @param [Boolean] dry_run
      #   If `true` performs a dry-run by only calling {#build} and **not**
      #   launching the exploit.
      #
      # @yield [exploit]
      #   If a block is given, it will be yielded the exploit after it has been
      #   launched. Once the block has returned, {#cleanup} will automatically
      #   be called.
      #
      # @yieldparam [Exploit] exploit
      #   The launched exploit.
      #
      # @return [Exploit]
      #   The launched exploit.
      #
      # @api public
      #
      # @since 1.0.0
      #
      def exploit(dry_run: false)
        perform_build

        unless dry_run
          perform_launch

          if block_given?
            yield self
            perform_cleanup
          end
        end

        return self
      end

      #
      # @group Exploit API Methods
      #

      #
      # Place holder methods for additional validation logic.
      #
      # @abstract
      #
      # @api public
      #
      # @since 1.0.0
      #
      def validate
      end

      #
      # Returns a vulnerable test result for the {#test} method.
      #
      # @return [TestResult::Vulnerable]
      #
      # @example
      #   def test
      #     # ...
      #     return Vulnerable("the host is vulnerable")
      #     # ...
      #   end
      #
      # @since 1.0.0
      #
      def Vulnerable(message)
        TestResult::Vulnerable.new(message)
      end

      #
      # Returns a not vulnerable test result for the {#test} method.
      #
      # @return [TestResult::NotVulnerable]
      #
      # @example
      #   def test
      #     # ...
      #     return NotVulnerable("the host is not vulnerable")
      #     # ...
      #   end
      #
      # @since 1.0.0
      #
      def NotVulnerable(message)
        TestResult::NotVulnerable.new(message)
      end

      #
      # Returns an unknown test result for the {#test} method.
      #
      # @return [TestResult::Unknown]
      #
      # @example
      #   def test
      #     # ...
      #     return Unknown("cannot determine whether the host is vulnerable")
      #     # ...
      #   end
      #
      # @since 1.0.0
      #
      def Unknown(message)
        TestResult::Unknown.new(message)
      end

      #
      # Place holder method for testing whether the target is vulnerable.
      #
      # @return [Test::Vulnerable, Test::NotVulnerable, Test::Unknown]
      #
      # @example
      #   def test
      #     case http.get_body('/')
      #     when /Powered by Foo 4\.19\./
      #       Vulnerable('host is vulnerable')
      #     when /Powered by Foo 4\.2[0-9]\./
      #       NotVulnerable('host is patched')
      #     else
      #       Unknown('cannot determine whether the host is vulnerable or not')
      #     end
      #   end
      #
      # @abstract
      #
      # @since 1.0.0
      #
      def test
        Unknown("no vulnerability testing logic defined")
      end

      #
      # Place holder method that builds the exploit.
      #
      # @abstract
      #
      # @api public
      #
      # @since 1.0.0
      #
      def build
      end

      #
      # Place holder method that launches the exploit.
      #
      # @abstract
      #
      # @api public
      #
      # @since 1.0.0
      #
      def launch
      end

      #
      # Place holder method that cleans up after the exploit.
      #
      # @abstract
      #
      # @api public
      #
      # @since 1.0.0
      #
      def cleanup
      end

      #
      # Indicates that the exploit has failed.
      #
      # @param [String] message
      #   The failure message.
      #
      # @raise [ExploitFailed]
      #
      def fail(message)
        raise(ExploitFailed,message)
      end

    end
  end
end