# frozen_string_literal: true
#
# ronin-vulns - A Ruby library for blind vulnerability testing.
#
# Copyright (c) 2022-2024 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 .
#
require 'ronin/vulns/web_vuln'
require 'ronin/vulns/ssti/test_expression'
module Ronin
module Vulns
#
# Represents a Server Side Template Injection (SSTI) vulnerability.
#
class SSTI < WebVuln
# List of common Server Side Template Injection (SSTI) escapes.
#
# @api private
ESCAPES = {
nil => nil, # does not escape the expression
double_curly_braces: ->(expression) { "{{#{expression}}}" },
dollar_curly_braces: ->(expression) { "${#{expression}}" },
dollar_double_curly_braces: ->(expression) { "${{#{expression}}}" },
pound_curly_braces: ->(expression) { "\#{#{expression}}" },
angle_brackets_percent: ->(expression) { "<%= #{expression} %>" }
}
# The type of SSTI escape used.
#
# @return [:double_curly_braces, :dollar_curly_braces, :dollar_double_curly_braces, :pound_curly_braces, :angle_brackets_percent, :custom, nil]
#
# @since 0.2.0
attr_reader :escape_type
# How to escape the payload so that it's executed.
#
# @return [Proc, nil]
# The proc that will accept a String and return a String, or `nil` to
# indicate that the payload will not be escaped.
attr_reader :escape
# The test expression to use when testing the URL for SSTI.
#
# @return [TestExpression]
attr_reader :test_expr
#
# Initializes the Server Side Template Injection (SSTI) vulnerability.
#
# @param [String, URI::HTTP] url
# The URL to exploit.
#
# @param [:double_curly_braces, :dollar_curly_braces, :dollar_double_curly_braces, :pound_curly_braces, :angle_brackets_percent, :custom, Proc, nil] escape
# How to escape a given payload. Either a proc that will accept a String
# and return a String, a Symbol describing the template syntax to use,
# or `nil` to indicate that the payload will not be escaped.
#
# @param [TestExpression] test_expr
# The test payload and expected result to check for when testing the URL
# for SSTI.
#
# @raise [ArgumentError]
# An unknown `escape_type:` or `escape:` value was given, or no
# `test_expr:` was given.
#
def initialize(url, escape: nil,
test_expr: self.class.random_test,
**kwargs)
super(url,**kwargs)
case escape
when Symbol
@escape_type = escape
@escape = ESCAPES.fetch(escape) do
raise(ArgumentError,"unknown template syntax: #{escape_type.inspect}")
end
when Proc
@escape_type = :custom
@escape = escape
when nil # no-op
else
raise(ArgumentError,"invalid escape type, must be a Symbol, Proc, or nil: #{escape.inspect}")
end
@test_expr = test_expr
unless @test_expr
raise(ArgumentError,"must specify both a test expression")
end
end
#
# Generates a random `N*M` SSTI test.
#
# @return [TestExpression]
# A random test expression.
#
def self.random_test
int1 = rand(1_000..1_999)
int2 = rand(1_000..1_999)
string = "#{int1}*#{int2}"
result = (int1 * int2).to_s
return TestExpression.new(string,result)
end
#
# Tests the URL and a specific query param, header name, cookie param, or
# form param for a Server Side Template Injection (SSTI) vulnerability
# by enumerating over various SSTI syntaxes.
#
# @param [URI::HTTP] url
# The URL to test.
#
# @param [Array, Symbol, Proc, nil] escape
# The escape method to use. If `escape:` is not given, then all escapes
# names in {ESCAPES} will be tested..
#
# @param [Ronin::Support::Network::HTTP] http
# The HTTP session to use for testing the URL.
#
# @param [Hash{Symbol => Object}] kwargs
# Additional keyword arguments for {#initialize}.
#
# @option kwargs [Symbol, String, true, nil] :query_param
# The query param name to test.
#
# @option kwargs [Symbol, String, nil] :header_name
# The header name to test.
#
# @option kwargs [Symbol, String, true, nil] :cookie_param
# The cookie param name to test.
#
# @option kwargs [Symbol, String, nil] :form_param
# The form param name to test.
#
# @return [SSTI, nil]
# The first discovered web vulnerability for the specific query param,
# header name, cookie param, or form param.
#
# @api private
#
# @since 0.2.0
#
def self.test_param(url, escape: ESCAPES.keys,
# initialize keyword arguments
http: , **kwargs)
Array(escape).each do |escape_value|
vuln = new(url, escape: escape_value, http: http, **kwargs)
return vuln if vuln.vulnerable?
end
return nil
end
#
# Escapes the payload using {#escape}.
#
# @param [String] payload
#
# @return [String]
#
def encode_payload(payload)
if @escape then @escape.call(payload)
else payload
end
end
#
# Determine whether the URL is vulnerable to Server Side Template
# Injection (SSTI).
#
# @return [Boolean]
#
def vulnerable?
response = exploit(@test_expr.string)
body = response.body
return body.include?(@test_expr.result)
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
:ssti
end
end
end
end