# 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 . # 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`. # # @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] # 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, Range, nil] new_software_versions # the optional new software version(s) to set. # # @return [Array, Range, 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