Skip to content

Instantly share code, notes, and snippets.

@Zapotek
Last active April 25, 2023 11:35
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zapotek/a5393018391c7cc2e91c680797478a75 to your computer and use it in GitHub Desktop.
Save Zapotek/a5393018391c7cc2e91c680797478a75 to your computer and use it in GitHub Desktop.

Ecsypno Codename SCNR scripting interface

Powered by DSeL.

Scanner

require 'scnr/engine/api'

require "#{Options.paths.root}/tmp/scripts/with_helpers/helpers"

SCNR::Engine::API.run do

Scan {

  # Can also be written as:
  #
  # options.set(
  #   url:    'http://my-site.com',
  #   audit:  {
  #     elements: [:links, :forms, :cookies, :ui_inputs, :ui_forms]
  #   },
  #   checks: ['*']
  # )
  Options {
    set url:    'http://my-site.com',
        audit:  {
          elements: [:links, :forms, :cookies, :ui_inputs, :ui_forms]
        },
        checks: ['*']
  }

  # Scan session configuration.
  Session {
    # Login using the #fill_in_and_submit_the_login_form method from the helpers.rb file.
    to :login, :fill_in_and_submit_the_login_form

    # Check for a valid session using the #find_welcome_message method from the helpers.rb file.
    to :check, :find_welcome_message
  }

  # Scan scope configuration.
  Scope {

    # Limit the scope of the scan based on URL.
    select :url, :within_the_eshop

    # Limit the scope of the scan based on Element.
    reject :element, :with_sensitive_action; also :with_weird_nonce

    # Only select pages that are in the admin panel.
    select :page, :in_admin_panel

    # Limit the scope of the scan based on Page.
    reject :page, :with_error

    # Limit the scope of the scan based on DOM events and DOM elements.
    # In this case, never click the logout button!
    reject :event, :that_clicks_the_logout_button

  }

  # Run the scan and handle the results (in this case print to STDOUT) using #handle_results.
  run! :handle_results
}

Logging {

  # Error and exception handling.
  on :error,     :log_error
  on :exception, :log_exception

}

Data {

  # Don't store issues in memory, we'll send them to the DB.
  issues.disable(:storage).on :new, :save_to_db

  # Could also be written as:
  #
  #   Issues {
  #       disable(:storage)
  #       on :new, :save_to_db)
  #   }
  #
  # Or:
  #
  #   Issues { disable(:storage); on :new, :save_to_db)  }

  # Store every page in the DB too for later analysis.
  pages.on :new, :save_to_db

  # Or:
  #
  #   Pages {
  #       on :new, :save_to_db
  #   }

}

Http {
  on :request, :add_special_auth_header
  on :response, :gather_traffic_data; also :increment_http_performer_count
}

Checks {

  # Add a custom check on the fly to check for something simple specifically
  # for this scan.
  as :missing_important_header, with_missing_important_header_info,
     :log_pages_with_missing_important_headers

}

# Been having trouble with this scan, collect some runtime statistics.
plugins.as :remote_debug, send_debugging_info_to_remote_server_info,
           :send_debugging_info_to_remote_server

# Serves PHP scripts under the extension 'x'.
fingerprinters.as :php_x, :treat_x_as_php

Input {

  # Vouchers and serial numbers need to come from an algorithm.
  values :with_valid_role_id

}

Dom {

  # Let's have a look inside the live JS env of those interesting pages,
  # setup the data collection.
  before :load, :start_js_data_gathering
  after  :load, :retrieve_js_data; also :event, :retrieve_event_js_data

}

end

Helpers

# State

def log_error( error )
  # ...
end
def log_exception( exception )
  # ...
end

# Data

def save_to_db( obj )
  # Do stufff...
end
def save_js_data_to_db( data, element, event )
  # Do other stufff...
end

# Scope

def within_the_eshop( url )
  url.path.start_with? '/eshop'
end

def with_error( page )
  /Error/i.match? page.body
end

def in_admin_panel( page )
  /Admin panel/i.match? page.body
end

def that_clicks_the_logout_button( event, element )
  event == :click && element.tag_name == :button &&
    element.attributes['id'] == 'logout'
end

def with_sensitive_action( element )
  element.action.include? '/sensitive.php'
end

def with_weird_nonce( element )
  element.inputs.include? 'weird_nonce'
end

# HTTP

def generate_request_header
  # ...
end
def save_raw_http_response( response )
  # ...
end
def save_raw_http_request( request )
  # ...
end

def add_special_auth_header( request )
  request.headers['Special-Auth-Header'] ||= generate_request_header
end

def increment_http_performer_count( response )
  # Count the amount of requests/responses this system component has
  # performed/received.
  #
  # Performers can be browsers, checks, plugins, session, etc.
  stuff( response.request.performer.class )
end

def gather_traffic_data( response )
  # Collect raw HTTP traffic data.
  save_raw_http_response( response.to_s )
  save_raw_http_request( response.request.to_s )
end

# Checks

def with_missing_important_header_info
  {
    name:        'Missing Important-Header',
    description: %q{Checks pages for missing `Important-Header` headers.},
    elements:    [ Element::Server ],
    issue:       {
      name:        %q{Missing 'Important-Header' header},
      severity:    Severity::INFORMATIONAL
    }
  }
end

# This will run from the context of a Check::Base.
def log_pages_with_missing_important_headers
  return if audited?( page.parsed_url.host ) ||
    page.response.headers['Important-Header']

  audited( page.parsed_url.host )

  log(
    vector: Element::Server.new( page.url ),
    proof:  page.response.headers_string
  )
end

# Plugins

# This will run from the context of a Plugin::Base.
def send_debugging_info_to_remote_server
  address = '192.168.0.11'
  port    = 81
  auth    = Utilities.random_seed

  url = `start_remote_debug_server.sh -a #{address} -p #{port} --auth #{auth}`
  url.strip!

  http.post( url,
             body: SCNR::Engine::SCNR::Engine::Options.to_h.to_json,
             mode: :sync
  )

  while framework.running? && sleep( 5 )
    http.post( "#{url}/statistics",
               body: framework.statistics.to_json,
               mode: :sync
    )
  end
end

def send_debugging_info_to_remote_server_info
  {
    name: 'Debugger'
  }
end

# Fingerprinters

# This will run from the context of a Fingerprinter::Base.
def treat_x_as_php
  return if extension != 'x'
  platforms << :php
end

# Session

def fill_in_and_submit_the_login_form( browser )
  browser.load "#{SCNR::Engine::SCNR::Engine::Options.url}/login"

  form = browser.form
  form.text_field( name: 'username' ).set 'john'
  form.text_field( name: 'password' ).set 'doe'

  form.input( name: 'submit' ).click
end

def find_welcome_message
  http.get( SCNR::Engine::Options.url, mode: :sync ).body.include?( 'Welcome user!' )
end

# Inputs

def with_valid_code( name, current_value )
  {
    'voucher-code'  => voucher_code_generator( current_value ),
    'serial-number' => serial_number_generator( current_value )
  }[name]
end

def with_valid_role_id( inputs )
  return if !inputs.include?( 'role-type' )

  inputs['role-id'] ||= (inputs['role-type'] == 'manager' ? 1 : 2)
  inputs
end

# Browser

def start_js_data_gathering( page, browser )
  return if !page.url.include?( 'something/interesting' )

  browser.javascript.inject <<JS
    // Gather JS data from listeners etc.
    window.secretJSData = {};
JS
end

def retrieve_js_data( page, browser )
  return if !page.url.include?( 'something/interesting' )

  save_js_data_to_db(
    browser.javascript.run( 'return window.secretJSData' ),
    page, :load
  )
end

def retrieve_event_js_data( event, element, browser )
  return if !browser.url.include?( 'something/interesting' )

  save_js_data_to_db(
    browser.javascript.run( 'return window.secretJSData' ),
    element, event
  )
end

def handle_results( report, statistics )
  puts
  puts '=' * 80
  puts

  puts "[#{report.sitemap.size}] Sitemap:"
  puts
  report.sitemap.sort_by { |url, _| url }.each do |url, code|
    puts "\t[#{code}] #{url}"
  end

  puts
  puts '-' * 80
  puts

  puts "[#{report.issues.size}] Issues:"
  puts

  report.issues.each.with_index do |issue, idx|

    s = "\t[#{idx+1}] #{issue.name} in `#{issue.vector.type}`"
    if issue.vector.respond_to?( :affected_input_name ) &&
      issue.vector.affected_input_name
      s << " input `#{issue.vector.affected_input_name}`"
    end
    puts s << '.'

    puts "\t\tAt `#{issue.page.dom.url}` from `#{issue.referring_page.dom.url}`."

    if issue.proof
      puts "\t\tProof:\n\t\t\t#{issue.proof.gsub( "\n", "\n\t\t\t" )}"
    end

    puts
  end

  puts
  puts '-' * 80
  puts

  puts "Statistics:"
  puts
  puts "\t" << statistics.ai.gsub( "\n", "\n\t" )
end
Copy link

ghost commented Mar 16, 2022

Gu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment