ASAReaper: Grab Configs From Multiple Cisco Devices Over SSH (Demos PExpect and AES Encrypted INI Files in Python)

ASAReaper: Grab Configs From Multiple Cisco Devices Over SSH
(Demos PExpect and AES Encrypted INI Files in Python)

 

        I got put in charge of managing a bunch of Cisco ASAs (Adaptive Security Appliances [firewalls, VPNs and such]). It was a new experience for me, and one of the first problems I encountered was backing up the configs, or getting them into text files so they were easier to search through. I would have thought there would be a free tool for this, but I found nothing. I could have tried to SCP files off, but SCP was not always configured and change management was such that I could not just enable it on a whim. FTP was a possibility, but I did not want my passwords flying across the network in plain text. SSH was there, so I wanted to use it. I ended up writing a script using several Python libraries to accomplish my goal of automatically sucking down Cisco configs using "show run" over an SSH connection. Here are some of the features of this Python script:

Even if you don't manage Cisco ASAs, the code should be easy to modify for other Cisco devices you can access over SSH. You might even be interested in only using the encrypted INI portion.

Useage it is simple:

First run with non-existent config file to set it up (asa.txt contains the IPs, line by line, of the Cisco devices to backup the configs from).

irongeek@igbox:~/backups$ ./asareaper.py myconfig.ini
Give me the config password or Die!!! : <does not echo>
Host Names File: asas.txt
ASA User: irongeek
ASA User Password: <does not echo>
Enable Password: <does not echo>
Running on 192.168.2.2
Running on 192.168.2.3
Working on 192.168.2.2 in the context admin...
Working on 192.168.2.3 the context admin...
Working on 192.168.2.2 in the context context1...
Working on 192.168.2.3 in the context context1...
Working on 192.168.2.2 in the context context2...
Working on 192.168.2.3 in the context context2...
irongeek@igbox:~/backups$

You should now have a bunch of text files in the same directory with names in the form of <asa name>-<context>-<timestamp>.TXT. The next time you run it, you should only have to put in one password:

irongeek@igbox:~/backups$ ./asareaper.py myconfig.ini
Give me the config password or Die!!! : <does not echo>
Running on 192.168.2.2
Running on 192.168.2.3
Working on 192.168.2.2 in the context admin...
Working on 192.168.2.3 the context admin...
Working on 192.168.2.2 in the context context1...
Working on 192.168.2.3 in the context context1...
Working on 192.168.2.2 in the context context2...
Working on 192.168.2.3 in the context context2.
irongeek@igbox:~/backups$

 

Easy. You can also use the -d option to turn on more debugging. The script should be pretty harmless, but I take no liability if it screws something up. If you have a different setup, you can just use this script as a template for other tasks (I've used it to return the version number and serials of every ASA in a list before). Hope it is useful to someone.

Download Script

Or just read it:

  Exported from Notepad++

#!/usr/bin/env python

#Irongeek's Wacky script for harvesting configs from ASAs

#help from thes sites:

#http://stackoverflow.com/questions/9370886/pexpect-if-else-statement

#http://eli.thegreenplace.net/2010/06/25/aes-encryption-of-files-in-python-with-pycrypto/

#http://linux.byexamples.com/archives/346/python-how-to-access-ssh-with-pexpect/

#and many others

import pexpect, re, getpass, os, time, ConfigParser, base64, hashlib, sys, random, datetime

from Crypto.Cipher import AES

import threading

from threading import Thread

import thread

commandonall="more system:run"

prefixonall=""

tout=160

if len(sys.argv)<2:

sys.exit("I need a config file name to work with. If the file does not exist, we will create it. Use a '-d' after the file name to turn on debugging.")

configpassword = getpass.getpass('Give me the config password or Die!!! : ')

key = hashlib.sha256(configpassword).digest()

keyhash = hashlib.sha256(key).digest()

mode = AES.MODE_CFB

config = ConfigParser.RawConfigParser()

configfilename=sys.argv[1]

#If the file exists, we will read from it

if os.path.exists(configfilename):

config.read(configfilename)

#Check if password is correct

if keyhash != base64.b64decode(config.get('configs', 'Key_Hash')):

sys.exit ("Wrong Config Password! You Must Die!!!")

else:

#If it is correct we can decrypt the INI file

iv = base64.b64decode(config.get('configs', 'IV'))

encryptor = AES.new(key, mode, iv)

hostnamesfile = config.get('configs', 'Host_Names_File')

asauserpass = encryptor.decrypt(base64.b64decode(config.get('configs', 'ASA_Password')))

enpass = encryptor.decrypt(base64.b64decode(config.get('configs', 'Enable_Password')))

asauser = encryptor.decrypt(base64.b64decode(config.get('configs', 'ASA_User')))

else:

#If it did not exist, we prompt the user to create it

iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))

encryptor = AES.new(key, mode, iv)

hostnamesfile = raw_input('Host Names File: ')

asauser = raw_input('ASA User: ')

asauserpass = getpass.getpass('ASA User Password: ')

enpass = getpass.getpass('Enable Password: ')

config.add_section('configs')

config.set('configs', 'IV', base64.b64encode(iv))

config.set('configs', 'Host_Names_File', hostnamesfile)

config.set('configs', 'Key_Hash', base64.b64encode(keyhash))

config.set('configs', 'ASA_Password', base64.b64encode(encryptor.encrypt(asauserpass)))

config.set('configs', 'Enable_Password', base64.b64encode(encryptor.encrypt(enpass)))

config.set('configs', 'ASA_User', base64.b64encode(encryptor.encrypt(asauser)))

with open(configfilename, 'wb') as configfile:

config.write(configfile)

 

class GrabConfig(Thread):

def __init__ (self,host):

Thread.__init__(self)

self.host = host.replace("\n","").replace("\r","")

self.status = -1

def run(self):

try:

print "Running on " + self.host

child = pexpect.spawn ('ssh ' + asauser + '@' + self.host)

child.maxread=9999999

if "-d" in sys.argv:

child.logfile = sys.stdout

i=child.expect(['.*assword:.*',pexpect.EOF,pexpect.TIMEOUT],timeout=tout)

if i==0:

print "Sending SSH Password to " + self.host,

child.sendline(asauserpass)

elif i==1:

print "Connection to " + self.host + " Dropped"

thread.exit()

elif i==2: #timeout

print "Connection to " + self.host + " Timeout"

thread.exit()

child.sendline("\n")

child.expect('.*>.*', timeout=tout)

child.sendline('en')

child.expect('.*assword:.*', timeout=tout)

child.sendline(enpass)

child.expect(".*# ", timeout=tout)

if "/admin" in child.after: # Only need to do this if we have contexts

child.sendline('changeto system')

child.expect(".*# ", timeout=tout)

child.sendline("terminal pager 0") #So "show run" keeps going

child.expect(".*# ", timeout=tout)

asaname = child.after.replace("#","").replace("\n","").replace("\r","").replace(" ","").replace("terminalpager0","")

child.sendline("show run | grep context")

child.expect(".*#", timeout=tout)

#Plan to to replace the below when I have better regex

contexts = re.findall("^context .*", child.after, re.MULTILINE)

child.sendline(commandonall)

child.expect(".*# ", timeout=tout)

f = open(prefixonall+asaname+"-"+self.host+"-"+datetime.datetime.now().strftime("%Y-%m-%d-%H:%M")+".TXT", 'w')

configlines=child.after.splitlines()

f.writelines(["%s\r\n" % line for line in configlines[1:-1]])

f.close()

#Loops over each context, and grabs the ASA configs

for context in contexts:

context=context[8:].replace("#","").replace("\n","").replace("\r","").replace(" ","")

print "Working on " + self.host + " in the context " + context + "..."

child.sendline("changeto context "+context)

child.expect(".*# ", timeout=tout)

child.sendline(commandonall)

child.expect(".*# ", timeout=tout)

configlines=child.after.splitlines()

f = open(prefixonall+asaname+"-"+self.host+"-"+context+"-"+datetime.datetime.now().strftime("%Y-%m-%d-%H:%M")+".TXT", 'w')

f.writelines(["%s\r\n" % line for line in configlines[1:-1]])

f.close()

child.sendline('exit')

except:

print "Unexpected error:", sys.exc_info()[0]

print "Error on "+self.host

raise

#Main loop of the program that spawns threads to connect to multiple ASAs at the same time

asalist = open(hostnamesfile).readlines()

for host in asalist:

GrabConfig(host).start()

Change Log:

06/13/2013: Updated the code to make it easier to maintain and to fix a timeout issue. Also, Arne Lovius told me about a tool called Rancid (http://www.shrubbery.net/rancid) that can do the same thing as my script and more, but I figured the sample code is still of help to some.
03/10/2014: Mostly updated for longer timeouts and to use "more system:run" so you can save passwords in the configs too. You should now just have to edit the commandonall and prefixonall to set the script up to run a given command on a series of Cisco ASAs in every context.



If you would like to republish one of the articles from this site on your webpage or print journal please contact IronGeek.

Copyright 2020, IronGeek