At CDA we love automation. It is an efficient way to perform. Especially those we can incorporate into a DevOps CI/CD pipeline. Many of our customers have deployed both Cisco ASA and Cisco ISE appliances in production. If you are not familiar with these technologies, that’s OK. This approach works with most firewalls and other network access control devices from other vendors if they have a REST API available with their appliances. If they do not, “get rid of them” just kidding! We work with many customers with the following business challenges:
Legacy opposed to “Next Generation” Firewall: Tend to neglect the need to identify the user accessing the internet and consuming bandwidth. There is no username to IP connection mapping.
Network Access Control (NAC): Many customers have implemented industry standard network access control solutions that do provide visibility into the devices and users connected to the network. I will discuss how we integrate the two solutions (Legacy Firewall and NAC) to provide visibility into the user’s identity.
Which user/users are hogging up my bandwidth? With the number of years I have been working in this industry, I am still puzzled how long it takes to figure out who or what is consuming all the corporate internet bandwidth. I will not be focusing on this challenge however; I will provide you with the framework to answer this question with some basic modifications to the script.
Who in my organization is connected to which external IP address? Is it a known malicious site? It is not uncommon to have the enterprise security team identify suspicious traffic or web traffic via threat intelligence feeds. However, it is not always easy to figure out who or which systems are accessing the suspicious sites. With the solution I am presenting, you could use the output to enrich the threat hunting process, especially when coupled with other services including your SIEM.
Enriched Incident Response Data: There is often a need to enrich incident response data and rarely an easy way to correlate the traffic without the proper integration and tooling. I will present a solution to help enrich the data when necessary.
QUICK BACKGROUND
Chances are, if you are reading this, you know what ASA and ISE acronyms mean. If not, you can research the basics here.
Cisco ASA: What is Cisco Adaptive Security Appliance (ASA)? It’s one of the most widely deployed Layer 4 firewalls in corporate America. It is considered legacy and not the best/first choice with the advent of “Next Generation” firewalls like Cisco Firepower. Read more on the value here:
- Cisco Product Information: https://www.cisco.com/c/en/us/products/security/adaptive-security-appliance-asa-software/index.html
Cisco ISE: What is Cisco Identity Services Engine (ISE)? This is an enterprise class security solution that provides network access control and identity at the switch port level. ISE does a lot more than this, for more information, visit the Cisco website:
- Cisco Product Information: https://www.cisco.com/c/en/us/products/security/identity-services-engine/index.html
USE CASE
The game plan for building this solution is based on this simple use case. The solution must allow for automated mapping between username and IP address. This information can be used for other use cases. For example, we could use this data to refine remediation during a malicious attack or for security incident response processes. This should be relatively simple since we know the data we need exist on Cisco ISE and Cisco ASA systems in our “production” environment.
To prove the concept requested in the use case, I want to verify the data I need is where I think it should be on the ASA and the ISE appliances. When a user is connected to the outside world via the Cisco ASA firewall, I can see the following information when typing the “show conn” command:
Verify Cisco ASA data exists via the command line.
ASA-Temp# show conn
2 in use, 2 most used
UDP outside 8.8.8.8:53 inside 10.0.100.101:61422, idle 0:00:00, bytes 44, flags -
TCP outside 198.148.79.55:443 inside 10.0.100.200:50460, idle 0:00:00, bytes 0, flags saA
Table 1 Output from “show conn” command on Cisco ASA
The use case calls for automation, so I need to make sure I can get the same output from the Cisco ASA REST API. The image below represents the necessary information from the “/doc/” site on the Cisco ASA. The “/doc/” site is the REST API client and documentation. If the REST API is not setup in your environment, you can find steps to automate the deployment using Ansible here: https://www.criticaldesign.net/post/automating-the-deployment-of-the-cisco-asa-rest-api
The screenshot below shows one of the examples from the “/doc/” site. It appears that I can use the “/api/monitoring/connections” endpoint for our HTTP requests. This will return the following values for each connection in JSON format. I will use the “sourceIP” value to match on the ISE output to find the user for each connection:
Protocol
Source IP
Source Port
Destination Security Tag
Destination IP
Destination Port
Duration
Bytes Sent
Validate Cisco ASA REST API data is available.
My direct test to the “/api/monitoring/connections” endpoint via the browser yielded satisfactory results in JSON format as depicted in the image below.
As per the use case I want to verify the data we need to map the username and IP is readily available in the Cisco ISE console and REST API. To verify this information, I need to make sure the REST API is enabled on the Cisco ISE appliance.
This screenshot below represents the necessary and minimal configuration parameters for Cisco ISE to enable ERS (“External RESTful Services”).
Validate Cisco ISE ERS is enabled
Table 4 Output from "show conn" command on Cisco ASA
Then I need to verify the data exists in the ISE ERS (REST API). I can do this with the browser pointed to the “/admin/API/mnt/Session/ActiveList” endpoint for ERS. After I authenticate, I get the following response. For fun 😊 Cisco gave me a JSON response on the Cisco ASA and on Cisco ISE they gave me an XML response. We will need to account for the different response formats in the script we used to automate the use case. We will need to support HTTP requests, authentication to Cisco ISE, Cisco ASA, and be able to handle XML and JSON responses.
Table 5 Cisco ISE REST API test for connections data
I have verified all the necessary data via the API to match an IP address on the Cisco ASA to the username in the output from the Cisco ISE.
LAB INFRASTRUCTURE OVERVIEW
Next, I’ll give you a quick tour of the lab I used to build this solution. The environment is on VMWare ESXi with EVE-NG installed.
Special Note: Thank you, Kevin Rodgers and Christian Myers for setting up the lab environment to build this script.
My EVE-NG Lab
The image below and subsequent bullet points represent the lab:
The virtual lab consists of the following components in EVE-NG
Workstations: Two workstations represent user connections to both Cisco ISE for NAC and Cisco ASA for internet connectivity and perimeter security. (HQ-PC, Satellite-PC)
Access Switches: Two access layer switches were provisioned with 802.1x authentication to Cisco ISE. One switch is provided for each segment with one test workstation.
Core Switching: Core switching was provisioned to support connectivity between the remote locations, the pseudo data center, servers, and Internet connectivity.
Cisco ASA to Internet: The Cisco ASA is provisioned as the gateway to access all external connections.
Cisco ISE: The ISE appliance is hosted in the Servers segment as a separate virtual machine on VMWare ESXi.
DEVELOPMENT PROCESS OVERVIEW
To address this use case, I took the approach to build a solution that will allow for an automated and scheduled process to map Cisco ASA connections to the corresponding username. This gives us a better understanding of who is using the internet on the internal network. The steps are simple:
Understand Business/Use Case
Build Use Case/Pseudo Code
- Connect to Cisco ASA REST API
- Authenticate
- Connect to Cisco ISE REST API
- Authenticate
- Capture JSON and XML request/response data in variables.
- Parse the variables for interesting data and
match the IP to the corresponding user identity (username).
- Print the output
Design and setup the lab for testing
Explore Cisco ASA and ISE REST APIs
Test Cisco ASA and ISE REST API with browser
Build the script
Test the script
Document and post the script
The table below gives you a general idea of what the minimal configuration should be on each firewall. Each of the Cisco ASAv in my lab have the following configuration with some minor differences such as IP Address and Host Name.
Cisco ASA-V + Configuration (Base Example)
enable password $sha512$5000$... == pbkdf2
passwd Uzp6JyHt8i6QEG34 encrypted
interface GigabitEthernet0/0
nameif inside
security-level 100
ip address <Your IP Here> 255.255.255.0
logging enable
logging timestamp
logging facility 23
logging device-id context-name
aaa authentication ssh console LOCAL
aaa authentication http console LOCAL
aaa authentication login-history
http server enable
http 0.0.0.0 0.0.0.0 inside
http 172.16.0.0 255.255.0.0 inside
ssh scopy enable
ssh stricthostkeycheck
ssh 172.16.0.0 255.255.0.0 inside
ssh timeout 60
ssh version 2
ssh key-exchange group dh-group1-sha1
management-access inside
rest-api image disk0:/asa-restapi-7141-lfbff-k8.SPA
rest-api agent
SCRIPTED WORKFLOW OVERVIEW
Now that I have dissected and tested the REST API for my purposes, it’s time to build something useful to automate the process. The process is simple and documented in the table below:
Connect ASA
Authenticate
Connect ISE
Authenticate
Make REST API Request for Cisco ASA on endpoint “/”
Make REST API Request for Cisco ISE on endpoint “/”
Parse Cisco ASA response text for IP addresses
Parse Cisco ISE response for IP addresses found in Cisco ASA response and return the corresponding username for each IP connection.
Print output to the screen. This could go to syslog, log file, or any other output facility you like
NOTE: In the script below you will notice that I have staged some JSON and XML responses for demo purposes. Cisco ASA and ISE REST API Match Firewall IP to Username
import random
import sys
import string
import requests
import urllib3
from pprint import pprint
import json
import getpass
import xml.etree.ElementTree as ET
from ipwhois import IPWhois
from ipwhois.utils import get_countries
from ipwhois import __version__
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
asa_headers = {'Content-Type': 'application/json', 'User-Agent':'REST API Agent'}
ise_headers = {'Content-Type': 'application/xml', 'User-Agent':'REST API Agent'}
asa_uri = "/api/monitoring/connections"
ise_uri = "/admin/API/mnt/Session/ActiveList"
asa_host="172.16.122.10"
ise_host="172.16.122.250"
asaurl="https://"+asa_host+asa_uri
print(asaurl)
iseurl="https://"+ise_host+ise_uri
print(iseurl)
asa_user=input("ASA Username: ")
ise_user=input("ISE Username: ")
try:
asa_pwd=getpass.getpass(prompt='ASA Password: ', stream=None)
ise_pwd=getpass.getpass(prompt='ISE Password: ', stream=None)
except Exception as error:
print ('ERROR',error)
asareq=requests.get(asaurl,auth=(asa_user, asa_pwd),headers=asa_headers,verify=False)
print(asareq)
print("==================")
print(asareq.text)
print("==================")
isereq=requests.get(iseurl,auth=(ise_user, ise_pwd),headers=ise_headers,verify=False)
print(isereq)
print("==================")
print(isereq.text)
print("==================")
#Sample JSON from ASA Rest API for testing
asa_json=r"""{
"kind": "collection#ConnectionDetails",
"selfLink": "https://192.168.0.102/api/monitoring/connections",
"rangeInfo": {
"offset": 0,
"limit": 1,
"total": 3
},
"items": [
{
"protocol": "TCP",
"sourceSecurityTag": "",
"sourceIp": "192.168.0.60",
"sourcePort": "9338",
"destinationSecurityTag": "",
"destinationIp": "192.168.0.102",
"destinationPort": "443",
"duration": "0:00:00",
"bytesSent": "0 KB"
},
{
"protocol": "TCP",
"sourceSecurityTag": "",
"sourceIp": "192.168.0.61",
"sourcePort": "9438",
"destinationSecurityTag": "",
"destinationIp": "192.168.0.102",
"destinationPort": "443",
"duration": "0:00:00",
"bytesSent": "0 KB"
},
{
"protocol": "TCP",
"sourceSecurityTag": "",
"sourceIp": "192.168.0.62",
"sourcePort": "9538",
"destinationSecurityTag": "",
"destinationIp": "192.168.0.102",
"destinationPort": "443",
"duration": "0:00:00",
"bytesSent": "0 KB"
}
]
}"""
#Sample JSON from ISE Rest API for testing
ise_xml="""
<activeList noOfActiveSession="3">
<activeSession>
<user_name>test_user1</user_name>
<calling_station_id>50:00:00:01:00:00</calling_station_id>
<nas_ip_address>192.168.0.60</nas_ip_address>
<acct_session_id>00000001</acct_session_id>
<audit_session_id>0A00640100000011000DE3E9</audit_session_id>
<server>ISE</server>
<framed_ip_address>10.0.10.20</framed_ip_address>
<framed_ipv6_address/>
</activeSession>
<activeSession>
<user_name>test_user2</user_name>
<calling_station_id>50:00:00:02:00:00</calling_station_id>
<nas_ip_address>192.168.0.61</nas_ip_address>
<acct_session_id>00000002</acct_session_id>
<audit_session_id>0A00640100000011000DE3E8</audit_session_id>
<server>ISE</server>
<framed_ip_address>10.0.10.20</framed_ip_address>
<framed_ipv6_address/>
</activeSession>
<activeSession>
<user_name>test_user3</user_name>
<calling_station_id>50:00:00:03:00:00</calling_station_id>
<nas_ip_address>192.168.0.62</nas_ip_address>
<acct_session_id>00000003</acct_session_id>
<audit_session_id>0A00640100000011000DE3E7</audit_session_id>
<server>ISE</server>
<framed_ip_address>10.0.10.20</framed_ip_address>
<framed_ipv6_address/>
</activeSession>
</activeList>"""
#print(ise_xml)
#print("")
#print(asa_json)
def get_isesessions():
ise_xml_sess = ET.fromstring(ise_xml)
for session in ise_xml_sess.iter('activeSession'):
session_ip=session.find('framed_ip_address').text
session_user=session.find('user_name').text
session_mac=session.find('calling_station_id').text
print("ID: %s\tName: %s\tMAC: %s" % (session_ip, session_user, session_mac))
def get_isesessions2():
ise_xml_sess2 = ET.fromstring(isereq.text)
for session in ise_xml_sess2.iter('activeSession'):
session_ip=session.find('framed_ip_address').text
session_user=session.find('user_name').text
session_mac=session.find('calling_station_id').text
print("ID: %s\tName: %s\tMAC: %s" % (session_ip, session_user, session_mac))
def get_asasessions():
asa_json_sess=json.loads(asa_json)
for asession in asa_json_sess['items']:
asession_ip=asession['sourceIp']
asession_sport=asession['sourcePort']
print("Source IP : " + asession_ip,",Source Port : ",asession_sport)
def get_asasessions2():
asa_json_sess=json.loads(asareq.text)
for asession in asa_json_sess['items']:
asession_ip=asession['sourceIp']
asession_sport=asession['sourcePort']
print("Source IP : " + asession_ip,",Source Port : ",asession_sport)
def match_sessions():
asa_json_sess=json.loads(asa_json)
ise_xml_sess = ET.fromstring(ise_xml)
for session in ise_xml_sess.iter('activeSession'):
session_ip=session.find('nas_ip_address').text
session_user=session.find('user_name').text
session_mac=session.find('calling_station_id').text
for asession in asa_json_sess['items']:
asession_ip=asession['sourceIp']
asession_sport=asession['sourcePort']
if session_ip==asession_ip:
print(session_user +" has a session on the firewall with the ip: " + asession_ip)
def match_sessions2():
asa_json_sess=json.loads(asareq.text)
ise_xml_sess = ET.fromstring(isereq.text)
for session in ise_xml_sess.iter('activeSession'):
session_ip=session.find('framed_ip_address').text
session_user=session.find('user_name').text
session_mac=session.find('calling_station_id').text
for asession in asa_json_sess['items']:
asession_ip=asession['sourceIp']
asession_sport=asession['sourcePort']
if session_ip==asession_ip:
print(session_user +" has a session on the firewall with the ip: " + asession_ip)
def finduser_sessions():
asa_json_sess=json.loads(asareq.text)
ise_xml_sess = ET.fromstring(isereq.text)
for session in ise_xml_sess.iter('activeSession'):
session_ip=session.find('framed_ip_address').text
session_user=session.find('user_name').text
session_mac=session.find('calling_station_id').text
for asession in asa_json_sess['items']:
asession_ip=asession['sourceIp']
asession_sport=asession['sourcePort']
if session_ip==asession_ip:
print(session_user +" has a session on the firewall with the ip: " + asession_ip)
if __name__ == "__main__":
print("Getting ISE Sessions from test variables")
print("========================================")
print("")
get_isesessions()
print("Getting ASA Sessions from test variables")
print("========================================")
print("")
get_asasessions()
print("Matching Sessions from test variables for demo purposes")
print("=======================================================")
print("")
match_sessions()
print("=========End Demo, Move on to testing REST API=========")
print("Get ISE Data for Real")
print("====================")
print("")
get_isesessions2()
print("Get ASA Data for Real")
print("====================")
print("")
get_asasessions2()
print("Matching Sessions for Real")
print("====================")
print("")
match_sessions2()
#####Bonus Functions######
#finduser=input("Which user are you looking for?")
#print(finduser)
#finduser_sessions()
Table 6 CDA-Blog-GetUserFromCiscoISE-ASA.py
The output should look something like the table below:
Sample Output
Getting ISE Sessions
====================
ID: 10.0.10.20 Name: test_user1 MAC: 50:00:00:01:00:00
ID: 10.0.10.20 Name: test_user2 MAC: 50:00:00:02:00:00
ID: 10.0.10.20 Name: test_user3 MAC: 50:00:00:03:00:00
Getting ASA Sessions
====================
Source IP : 192.168.0.60 ,Source Port : 9338
Source IP : 192.168.0.61 ,Source Port : 9438
Source IP : 192.168.0.62 ,Source Port : 9538
Matching Sessions
====================
test_user1 has a session on the firewall with the ip: 192.168.0.60
test_user2 has a session on the firewall with the ip: 192.168.0.61
test_user3 has a session on the firewall with the ip: 192.168.0.62
Get ISE Data for Real
====================
ID: 10.0.20.123 Name: TEST\satuser MAC: 50:00:00:06:00:00
ID: 10.0.10.123 Name: TEST\hquser MAC: 50:00:00:05:00:00
Get ASA Data for Real
====================
Source IP : 10.0.10.123 ,Source Port : 60042
Source IP : 10.0.10.123 ,Source Port : 60028
Source IP : 10.0.20.123 ,Source Port : 60780
Source IP : 10.0.10.123 ,Source Port : 59720
Source IP : 10.0.10.123 ,Source Port : 60104
Source IP : 10.0.10.123 ,Source Port : 60105
Source IP : 10.0.10.123 ,Source Port : 51827
Source IP : 172.16.122.123 ,Source Port : 59934
Matching Sessions for Real
====================
TEST\satuser has a session on the firewall with the ip: 10.0.20.123
TEST\hquser has a session on the firewall with the ip: 10.0.10.123
TEST\hquser has a session on the firewall with the ip: 10.0.10.123
TEST\hquser has a session on the firewall with the ip: 10.0.10.123
TEST\hquser has a session on the firewall with the ip: 10.0.10.123
TEST\hquser has a session on the firewall with the ip: 10.0.10.123
TEST\hquser has a session on the firewall with the ip: 10.0.10.123
This script can be added to any job scheduler or automation engine (Jenkins), with some production ready refinements. For now, this is merely a prototype you can use with your security automation. This script will also work if you want to conquer some of the other challenges mentioned above using different data in the output. If you need help building a production ready automation tool please contact me at adidonato@criticaldesign.net.
SCRIPT/SOLUTION NOTES
If you plan on using this script, you will want to make sure you have done the following:
Install Python 3.6x
Install and create a Python Virtual Environment
Clone the repository from github
Test Functions (IN A TESTING ENVIRONMENT, don’t be that guy or gal!)
SCRIPT DOWNLOAD/ SOURCE
Disclaimer: While this may go without saying, “Do NOT test this in your production environment”
Script Execution and Testing
adidonato@ML-UBUNTU:~$python CDA-Blog-GetUserFromCiscoISE-ASA.py
NOTE: You will be prompted for Cisco ASA <username> <password> and Cisco ISE<username> <password>
Comments