Instantly share code, notes, and snippets.

Embed
What would you like to do?

[WiP] Scan script DSL/API for the new engine

Powered by DSeL.

DSL

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

options.set(
    url:    'http://test.com/',
    checks: '*'
)

# Could also be written as:
#
#   Options {
#
#       set(
#           url:    'http://test.com/',
#           checks: '*'
#       )
#
#   }

State {

    # 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.
    on :page, &:save_to_db

    # Shorthand for:
    #
    #   pages.on :new, &:save_to_db
    #
    # Or:
    #
    #   Pages {
    #       on :new, &:save_to_db
    #   }

    after 'Something descriptive and interesting', &:save_to_db
        also 'something descriptive but not that interesting', &:save_to_db
}

Scope {

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

    # Extend the scope of the scan based on Page content and override all other
    # relevant scope options that have been set -- URL-based for example.
    #
    # If this matches it will bypass all other scope restrictions.
    select :page, override: true, &: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

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

}

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

Checks {

    # Prevent certain pages from being audited by certain checks.
    reject :page, &:with_clients_for_emails
        also &:with_cusotom404_for_backup_files

    # Prevent certain elements from being audited from certain checks.
    # In this case, don't run any SQL injection checks for cookies.
    reject :element, &:cookies_from_sql_injection

    # 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_header

}

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

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

Session {
    to :login, &:fill_in_and_submit_the_login_form
    to :check, &:find_welcome_message
}

Input {

    # Vouchers and serial numbers need to come from an algo.
    value &:with_valid_code
    values &:with_valid_role_id

}

Browser {

    # 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

}

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.start_with? "#{options.url}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

# Checks

# Disable the e-mail grep check for the clients page only, we already
# know about these people.
def with_clients_for_emails_check( page, check )
    page.url.end_with?( 'clients.php' ) && check.shortname == :emails
end

# TEMP fix: Bug in custom-404 detection, developer has been notified.
def with_custom404_for_backup_files_check( page, check )
    page.url.end_with?( 'clients.php' ) && check.shortname == :backup_files
end

def cookies_from_sql_injection( element, check )
    element.type == :cookie &&
        check.shortname.to_s.start_with?( 'sql_injection' )
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    = framework.scan_seed

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

    http.post( url,
               body: framework.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

# 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 "#{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( 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

API

def save_to_db( obj )
    # Do stufff...
end
def save_js_data_to_db( data, element, event )
    # Do other stufff...
end
def generate_request_header
    # ...
end
def save_raw_http_response( response )
    # ...
end
def save_raw_http_request( request )
    # ...
end
def log_error( error )
    # ...
end
def log_exception( exception )
    # ...
end
def increment_http_performer_count( performer )
    # ...
end

# Set global options.
options.set(
    url:    'http://test.com/',
    checks: '*'
)

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

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

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

# Limit the scope of the scan based on URL.
scope.select :url do | url|
    url.start_with? "#{options.url}eshop/"
end

# Limit the scope of the scan based on Page.
scope.reject :page do |page|
    /ignore me/i.match? page.body
end

# Extend the scope of the scan based on Page content and override all other
# relevant scope options that have been set -- URL-based for example.
#
# If this matches it will bypass all other scope restrictions.
scope.select :page, override: true do |page|
    /scan me/i.match? page.body
end

# Limit the scope of the scan based on DOM events and DOM elements.
# In this case, never click the logout button!
scope.reject :event do |event, element|
    event == :click && element.tag_name == :button &&
        element.attributes['id'] == 'logout'
end

# Limit the scope of the scan based on Element.
scope.reject :element do |element|
    element.action.include? '/sensitive.php'
end.also do |element|
    element.inputs.include? 'weird_nonce'
end

# Prevent certain pages from being audited by certain checks.
checks.reject :page do |page, check|
    # Disable the e-mail grep check for the clients page only, we already
    # know about these people.
    page.url.end_with?( 'clients.php' ) && check.shortname == :emails

# TEMP fix: Bug in custom-404 detection, developer has been notified.
end.also do |page, check|
    page.url.includes?( '/custom404/' ) && check.shortname == :backup_files
end

# Prevent certain elements from being audited from certain checks.
# In this case, don't run any SQL injection checks for cookies.
checks.reject :element do |element, check|
    element.type == :cookie &&
        check.shortname.to_s.start_with?( 'sql_injection' )
end

# We always need this extra dynamic header...for some reason.
http.on :request do |request|
    request.headers['Special-Auth-Header'] ||= generate_request_header

# Collect some HTTP traffic data.
end.on :response do |response|
    # Count the amount of requests/responses this system component has
    # performed/received.
    #
    # Performers can be browsers, checks, plugins, session, etc.
    increment_http_performer_count( response.request.performer.class )

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

# Add a custom check on the fly to check for something simple specifically for
# this scan.
checks.as :missing_important_header,
   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
   } do
    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

# Been having trouble with this scan, collect some runtime statistics every
# 5 seconds.
plugins.as :remote_debug do
    address = '192.168.0.11'
    port    = 81
    auth    = framework.scan_seed

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

    http.post( url,
        body: framework.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

# Serves PHP scripts under the extension 'x'.
fingerprinters.as :php_x do
    next if extension != 'x'
    platforms << :php
end

# Set the login procedure.
session.to :login do |browser|
    browser.load "#{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

# Set the login check procedure.
end.to :check do
    http.get( options.url, mode: :sync ).body.include?(
        'Something important that appears to authorised users only.'
    )
end

# Vouchers and serial numbers need to come from an algo.
input.value do |name, current_value|
    {
        'voucher-code'  => voucher_code_generator( current_value ),
        'serial-number' => serial_number_generator( current_value )
    }[name]

# Missing role-id values depend on role-type.
end.values do |inputs|
    next if !inputs.include?( 'role-type' )
    inputs['role-id'] ||= (inputs['role-type'] == 'manager' ? 1 : 2)
    inputs
end

# Let's have a look inside the live JS env of those interesting pages,
# setup the data collection.
browser.before :load do |page, browser|
    next if !page.url.include?( 'something/interesting' )

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

# Retrieve JS data right after the page has loaded, prior to any events being
# triggered.
end.after :load do |page, browser|
    next if !page.url.include?( 'something/interesting' )

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

# Now retrieve data as each event is triggered to see how it affects the page.
end.after( :event ) do |event, element, browser|
    next if !browser.url.include?( 'something/interesting' )

    save_js_data_to_db(
        browser.javascript.run( 'return window.secretJSData' ),
        element, event
    )
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment