diff --git a/lib/ronin/vulns/ssti.rb b/lib/ronin/vulns/ssti.rb index aa8ddde..7469e8b 100644 --- a/lib/ronin/vulns/ssti.rb +++ b/lib/ronin/vulns/ssti.rb @@ -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, :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. # @@ -58,21 +66,39 @@ class SSTI < WebVuln # @param [String, URI::HTTP] url # The URL to exploit. # - # @param [Proc, nil] escape + # @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, 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 @@ -102,9 +128,9 @@ def self.random_test # @param [URI::HTTP, String] url # The URL to scan. # - # @param [Array, Proc, nil] escape + # @param [Array, 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}. @@ -145,11 +171,11 @@ def self.random_test # @return [Array] # 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 diff --git a/spec/ssti_spec.rb b/spec/ssti_spec.rb index 2ad7a3e..6351b38 100644 --- a/spec/ssti_spec.rb +++ b/spec/ssti_spec.rb @@ -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 @@ -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 @@ -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