Cerchiamo un host in Nexpose con Ruby

Nexpose è un prodotto commerciale per il vulnerability management, sviluppato da Rapid7, gli stessi dietro a Metasploit tanto per intenderci.

Photo by Olivier Bacquet

Nexpose è un prodotto commerciale per il vulnerability management, sviluppato da Rapid7, gli stessi dietro a Metasploit tanto per intenderci.

Una particolarità che non deve mancare in alcun tool di security, che Nexpose ha ed è per questo che l’ho scelto, è una serie di API per sviluppare script accessori che integrano la nostra piattaforma di vulnerability management.

Disclaimer

Non sono in alcun modo sponsorizzato da Rapid7 o dal suo distributore italiano. Quando ho dovuto scegliere uno strumento che mi aiutasse nel vulnerability management, mi sono basato su alcuni parametri per me importanti:

  • integrabilità con altri tool
  • possibilità di installazione su Linux
  • disponibilità di documentazione
  • community
  • supporto
  • disponibilità di API

Basandomi su questi parametri scelsi Nexpose e poi è stato un rapporto di amore/odio con alcune cose che il tool fa egregiamente ed altre che il tool fa un po’ meno bene… ed in questi casi mi tocca lavorare con le API.

Problema

Io tengo sotto vulnerability management un numero discreto di server, tra sviluppo, collaudo e produzione. Siamo nell’ordine delle migliaia di macchine.

Capita, ogni tanto, che mi si presenti un indirizzo IP o un FQDN che a memoria non mi dica niente. Per scoprire se la macchina è sotto scansione, dovrei ogni volta aprire un browser, puntare alla console, autenticarmi e cercare. Noioso. Ogni tanto i server che si presentano e che sono da validare sono un’estrazione di un inventario degli asset. Ricercare a mano, diventa impossibile e sinceramente poco divertente.

Soluzione

Regola numero 34, comma b, paragrafo 3 dello thesp0nge pensiero sul lavoro è: rendere le cose divertenti affinché la giornata pesi di meno.

Per applicare questa regola e risolvere il mio problema, uso le API che Nexpose mi mette a disposizione e scrivo uno script che mi cerca se un host è presente oppure no nel mio vulnerability management. Meglio ancora, leggo una lista di host da un file e li cerco uno ad uno. Sì perché il comma a, paragrafo 15, rigo f della regola numero 34 recita: automatizzare, automatizzare tutto il possibile.

Installare le gemme giuste

Per lavorare con ruby e Nexpose abbiamo bisogno della gemma nexpose sviluppata dai ragazzi di Rapid7.

Piccola storiella. In realtà spippolo con Nexpose e ruby da qualche anno e la loro gemma prima era molto limitata. Avevo fatto un fork ed alcuni pull request al repository originale. Tuttavia le mie modifiche, qualcosa legato alla ricerca degli asset, fu inglobato senza un merge ma col copia e incolla. L’approccio non mi piacque ed ebbi uno screzio con uno del team di sviluppo. Ora sembra che il progetto sia mantenuto meglio, ma continuo a non voler contribuire direttamente al loro codice. Scritto per altro in pessimo stile rubesco.

Torniamo a noi. L’installazione della gemma nexpose è molto semplice e segue lo stile ruby di installazione delle librerie accessorie.

$ gem install nexpose

Useremo anche un po’ di colori, quindi installiamo anche la gemma logger-colors.

$ gem install logger-colors

Ok, ambiente pronto. Si comincia!

Fatevi un account

Prima cosa, per non fare danni. Capite bene cosa devono fare i vostri script con le API e poi createvi un’utenza con le permission ristrette all’osso. Questo rispetta il principio del minimo privilegio e, soprattutto, vi evita di fare danni.

Suggerisco, per i meno esperti, di non avventurarvi per ora nella modifica di site, asset o nel lanciare scansioni da script. Per ora partiamo con operazioni in sola lettura.

Il mio account si chiama nexty_ro ed ha i privilegi di visualizzare i dati relativi a tutti gli asset e tutti i site.

Per Nexpose un asset è una macchina, che può avere anche più di un indirizzo IP, mentre il site è un raggruppamento logico di asset. Poi ci sono tante cose carine come asset group che mirano a complicarci ancora di più la vita. Per ora li ignoriamo.

Pronti, via!

Il nostro script ruby, parte in maniera abbastanza classica.

#!/usr/bin/env ruby

require 'getoptlong'
require 'logger'
require 'logger/colors'

require 'nexpose'
include Nexpose
APPNAME = "nexty"
VERSION = "0.10"

HOST    = "127.0.0.1"
USER    = "nexty_ro"
PASS    = "pippo123"

Per velocità ho cablato i paramatri di connessione a Nexpose nello script. Questo pone il problema di proteggere adeguatamente lo script da occhi indiscreti.

$ chmod go-rwx script_ficoso.rb

Ovviamente se ci bucano la macchina e si portano via lo script… bhé, fessacchiotti voi.

Io ho aggiunto un po’ di gestione della linea di comando, ma solo perché allo script voglio far fare un po’ di cose. Per il momento possiamo dedicarci ad una sola funzionalità. Ignorate pure le altre

$logger = Logger.new(STDOUT)
$logger.datetime_format = '%Y-%m-%d %H:%M:%S'

opts = GetoptLong.new(
  [ '--abandoned',  '-A',   GetoptLong::OPTIONAL_ARGUMENT],
  [ '--discovery',  '-d',   GetoptLong::REQUIRED_ARGUMENT],
  [ '--search',     '-s',   GetoptLong::REQUIRED_ARGUMENT],
  [ '--debug',      '-D',   GetoptLong::NO_ARGUMENT],
  [ '--version',    '-v',   GetoptLong::NO_ARGUMENT],
  [ '--help',       '-h',   GetoptLong::NO_ARGUMENT]
)

options={:action=>:none, :input=>nil}

opts.quiet=true
begin
  opts.each do |opt, val|
    case opt
    when '--version'
      puts "#{APPNAME} v#{VERSION}"
      Kernel.exit(0)
    when '--search'
      options={:action=>:search, :input=>val}
    when '--discovery'
      options={:action=>:discovery, :input=>val}
    when '--abandoned'
      options={:action=>:abandoned, :input=>val.to_i}  unless val.empty?
      options={:action=>:abandoned, :input=>90}   if val.empty?
      if options[:input] <= 0
        $logger.info "nexty is starting up"
        $logger.error "-A flag requires a positive number as parameter."
        Kernel.exit(-3)
      end
    end

  end
rescue GetoptLong::InvalidOption => e
  $logger.error e.message
  Kernel.exit(-2)
end

if options[:action] == :none
  $logger.error "nothing to do. Giving up"
  Kernel.exit(-3)
end

$logger.info "nexty is starting up"
do_search(options[:input])    if options[:action] == :search
do_discovery(options[:input]) if options[:action] == :discovery
do_abandoned(options[:input]) if options[:action] == :abandoned
$logger.info "nexty is leaving"

Ho raggruppato, ma serve un po’ di refactoring in effetti, le funzioni che fanno qualcosa con un namespace del tipo do_*. Essendo uno script semplice, per ora, non ho messo classi ed è tutto nello stesso file. Elminister mi punirà.

def do_search(input)
begin
  nsc = Connection.new(HOST, USER, PASS)
  nsc.login
  $logger.info "connected to #{HOST} as #{USER}"
  if File.file?(input)
    $logger.info "reading data from file #{input}"
    f = File.readlines(input)
    f.each do |name|
      tocheck = name.chomp+"*"
      old_assets = nsc.filter(Search::Field::ASSET, Search::Operator::IS, tocheck)
      old_assets.each { |a| STDERR.puts "#{name.chomp}, YES" } unless old_assets.empty?
      STDERR.puts "#{name.chomp}, NO" if old_assets.empty?
      STDERR.flush
    end
  else
    $logger.info "searching asset #{input}"
    tocheck = input+"*"
    assets = nsc.filter(Search::Field::ASSET, Search::Operator::IS, tocheck)
    assets.each do |a|
      STDERR.puts "#{a.name} (#{a.id}): #{a.risk_score}"
    end
    STDERR.flush
  end

  nsc.logout
  $logger.info "logout from #{HOST}"
  return 0
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
       Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, APIError => e
  $logger.error "#{e.to_s}"
  return -1
end
end

Ho aggiunto il carattere ‘*’ per fare una ricerca con wildcard. Se capita infatti di ricercare il nome macchina, invece censito con FQDN, potremmo anche non ottenere alcun risultato. In questo modo, se c’è lo troviamo. E’ importante che nel file hosts della macchina che usiamo per la scansione ci siano i nomi macchina aggiornati in modo che Nexpose possa risolvere sempre l’indirizzo IP. Ogni tanto i DNS interni non sono aggiornati…

Off by one

Poter interagire con il nostro sistema di scansione via API è di una comodità estrema e sinceramente molto divertente.

Presto vederemo come giocare con le scansioni scansioni di discovery per non lasciarci scappare neanche una macchina.

Enjoy!

comments powered by Disqus