Skip to content

Commit

Permalink
Support specifying the SSTI escape type as a Symbol name (closes #55).
Browse files Browse the repository at this point in the history
* Added `Ronin::Vulns::SSTI#escape_type`.
  • Loading branch information
postmodern committed Nov 22, 2023
1 parent bf7598c commit 842bffa
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 32 deletions.
64 changes: 45 additions & 19 deletions lib/ronin/vulns/ssti.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,22 @@ class SSTI < WebVuln
# List of common Server Side Template Injection (SSTI) escapes.
#
# @api private
ESCAPES = [
nil, # does not escape the expression
->(expression) { "{{#{expression}}}" },
->(expression) { "${#{expression}}" },
->(expression) { "${{#{expression}}}" },
->(expression) { "\#{#{expression}}" },
->(expression) { "<%= #{expression} %>" }
]
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, :ndollar_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.
#
Expand All @@ -58,21 +66,39 @@ class SSTI < WebVuln
# @param [String, URI::HTTP] url
# The URL to exploit.
#
# @param [Proc, nil] escape
# @param [:double_curly_braces, :ndollar_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, or `nil` to indicate that the payload will not
# be escaped.
# 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.
#
def initialize(url, escape: nil,
test_expr: self.class.random_test,
# @raise [ArgumentError]
# An unknown `escape_type:` or `escape:` value was given, or no
# `test_expr:` was given.
#
def initialize(url, escape_type: nil,
escape: nil,
test_expr: self.class.random_test,
**kwargs)
super(url,**kwargs)

@escape = escape
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
Expand Down Expand Up @@ -102,9 +128,9 @@ def self.random_test
# @param [URI::HTTP, String] url
# The URL to scan.
#
# @param [Array<Proc>, Proc, nil] escape
# @param [Array<Symbol, Proc>, Symbol, Proc, nil] escape
# The escape method to use. If `escape:` is not given, then all escapes
# in {ESCAPES} will be tested..
# names in {ESCAPES} will be tested..
#
# @param [Hash{Symbol => Object}] kwargs
# Additional keyword arguments for {#initialize}.
Expand Down Expand Up @@ -145,11 +171,11 @@ def self.random_test
# @return [Array<SSTI>]
# All discovered SSTI vulnerabilities.
#
def self.scan(url, escape: ESCAPES, **kwargs,&block)
def self.scan(url, escape: ESCAPES.keys, **kwargs,&block)
vulns = []

Array(escape).each do |escape_char|
vulns.concat(super(url, escape: escape_char, **kwargs, &block))
Array(escape).each do |escape_value|
vulns.concat(super(url, escape: escape_value, **kwargs, &block))
end

return vulns
Expand Down
116 changes: 103 additions & 13 deletions spec/ssti_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
describe "#initialize" do
include_examples "Ronin::Vulns::WebVuln#initialize examples"

it "must default #escape_type to nil" do
expect(subject.escape_type).to be(nil)
end

it "must default #escape to nil" do
expect(subject.escape).to be(nil)
end
Expand All @@ -27,12 +31,54 @@
end

context "when the escape: keyword argument is given" do
let(:escape) { described_class::ESCAPES[1] }

subject { described_class.new(url, escape: escape) }

it "must set #escape" do
expect(subject.escape).to be(escape)
context "and it's a Symbol" do
let(:escape) { :double_curly_braces }

it "must set #escape_type to the escape Symbol" do
expect(subject.escape_type).to eq(escape)
end

it "must resolve the Symbol name and set #escape to the value in #{described_class}::ESCAPES" do
expect(subject.escape).to be(described_class::ESCAPES.fetch(escape))
end
end

context "and it's a Proc" do
let(:escape) do
->(expr) { "{#{expr}}" }
end

it "must set #escape_type to :custom" do
expect(subject.escape_type).to be(:custom)
end

it "must set #escape" do
expect(subject.escape).to be(escape)
end
end

context "when it's nil" do
let(:escape) { nil }

it "must set #escape_type to nil" do
expect(subject.escape_type).to be(nil)
end

it "must set #escape" do
expect(subject.escape).to be(nil)
end
end

context "when it's another kind of Object" do
let(:escape) { Object.new }

it do
expect {
described_class.new(url, escape: escape)
}.to raise_error(ArgumentError,"invalid escape type, must be a Symbol, Proc, or nil: #{escape.inspect}")
end
end
end
end
Expand Down Expand Up @@ -110,18 +156,62 @@
end

context "when the escape: keyword argument is given" do
let(:escape) { subject::ESCAPES[1] }
context "and it's a Symbol" do
let(:escape) { :double_curly_braces }

it "must scan the URL using only the given escape" do
stub_request(:get,"https://example.com/page?bar=2&baz=3&foo={{#{test_string}}}")
stub_request(:get,"https://example.com/page?bar={{#{test_string}}}&baz=3&foo=1")
stub_request(:get,"https://example.com/page?bar=2&baz={{#{test_string}}}&foo=1")
it "must scan the URL using only the given escape type" do
stub_request(:get,"https://example.com/page?bar=2&baz=3&foo={{#{test_string}}}")
stub_request(:get,"https://example.com/page?bar={{#{test_string}}}&baz=3&foo=1")
stub_request(:get,"https://example.com/page?bar=2&baz={{#{test_string}}}&foo=1")

subject.scan(url, escape: escape, test_expr: test_expr)
subject.scan(url, escape: escape, test_expr: test_expr)

expect(WebMock).to have_requested(:get,"https://example.com/page?bar=2&baz=3&foo={{#{test_string}}}")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar={{#{test_string}}}&baz=3&foo=1")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar=2&baz={{#{test_string}}}&foo=1")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar=2&baz=3&foo={{#{test_string}}}")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar={{#{test_string}}}&baz=3&foo=1")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar=2&baz={{#{test_string}}}&foo=1")
end
end

context "and it's a Proc" do
let(:escape) do
->(expr) { "{#{expr}}" }
end

it "must scan the URL using only the given escape Proc" do
stub_request(:get,"https://example.com/page?bar=2&baz=3&foo={#{test_string}}")
stub_request(:get,"https://example.com/page?bar={#{test_string}}&baz=3&foo=1")
stub_request(:get,"https://example.com/page?bar=2&baz={#{test_string}}&foo=1")

subject.scan(url, escape: escape, test_expr: test_expr)

expect(WebMock).to have_requested(:get,"https://example.com/page?bar=2&baz=3&foo={#{test_string}}")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar={#{test_string}}&baz=3&foo=1")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar=2&baz={#{test_string}}&foo=1")
end
end

context "and it's an Array" do
let(:escape) do
[:double_curly_braces, :dollar_curly_braces]
end

it "must scan the URL using the escape types" do
stub_request(:get,"https://example.com/page?bar=2&baz=3&foo={{#{test_string}}}")
stub_request(:get,"https://example.com/page?bar={{#{test_string}}}&baz=3&foo=1")
stub_request(:get,"https://example.com/page?bar=2&baz={{#{test_string}}}&foo=1")
stub_request(:get,"https://example.com/page?bar=2&baz=3&foo=${#{test_string}}")
stub_request(:get,"https://example.com/page?bar=${#{test_string}}&baz=3&foo=1")
stub_request(:get,"https://example.com/page?bar=2&baz=${#{test_string}}&foo=1")

subject.scan(url, escape: escape, test_expr: test_expr)

expect(WebMock).to have_requested(:get,"https://example.com/page?bar=2&baz=3&foo={{#{test_string}}}")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar={{#{test_string}}}&baz=3&foo=1")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar=2&baz={{#{test_string}}}&foo=1")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar=2&baz=3&foo=${#{test_string}}")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar=${#{test_string}}&baz=3&foo=1")
expect(WebMock).to have_requested(:get,"https://example.com/page?bar=2&baz=${#{test_string}}&foo=1")
end
end
end
end
Expand Down

0 comments on commit 842bffa

Please sign in to comment.