# 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