Доброго времени суток, коллеги. В наличии есть Cisco ASA 5512 с настроенным сервером IPSEC туннелей к которому цепляются пользователи для доступа в корпоративную сеть. Поступила задача — выводить список активных пользователей в мониторинге, а также вести логирование кто, когда, с какого адреса и с каким профилем цеплялся.

Полазил по интернету, ничего подходящего для моей задачи не нашёл (может конечно плохо искал), и решил написать скрипт, который парсит вывод SNMP и складывает в таблицу.

Структура таблицы MySQL:

CREATE DATABASE `vpn_log` /*!40100 DEFAULT CHARACTER SET latin1 */;

CREATE TABLE `logins` (
  `id_l` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `login` varchar(90) NOT NULL,
  `id_s` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id_l`),
  UNIQUE KEY `id_l_UNIQUE` (`id_l`),
  UNIQUE KEY `login_UNIQUE` (`login`)
) ENGINE=InnoDB AUTO_INCREMENT=121 DEFAULT CHARSET=latin1;

CREATE TABLE `sessions` (
  `id_s` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `id_l` int(10) unsigned NOT NULL,
  `time_start` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `time_end` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `ip_source` varchar(45) NOT NULL,
  `ip_lan` varchar(45) NOT NULL,
  `s_index` int(10) unsigned NOT NULL,
  `profile` varchar(255) NOT NULL,
  PRIMARY KEY (`id_s`),
  UNIQUE KEY `id_s_UNIQUE` (`id_s`)
) ENGINE=InnoDB AUTO_INCREMENT=2923 DEFAULT CHARSET=latin1;

Сам скрипт обработки:

#!/usr/bin/php
<?php
##Database settings
$settings['sql_host']="localhost";
$settings['sql_db']="vpn_log";
$settings['sql_user']="vpn_log";
$settings['sql_password']="vpn_log";

#Подключение к базе#

function connectdb(){
    global $settings;
    $dbconn = mysqli_connect(
                $settings['sql_host'],
                $settings['sql_user'],
                $settings['sql_password'],
                $settings['sql_db']
            )
    or die('Could not connect: ' . mysqli_connect_errno());
    return $dbconn;
}

#Получение списка пользователей и параметров IPSEC сессий#

function get_users(){
    $ret = snmp3_real_walk(
                    '10.10.10.10', #IP адрес Cisco ASA
                    'snmpuser', #SNMP авторизация
                    'authNoPriv', #
                    'MD5', #
                    'authpassword', #
                    '', #
                    '', #
                    '1.3.6.1.4.1.9.9.392.1.3.21' # ветка OID в которой расположены пользователи
    );
    $result = [];
    $user = [];
    foreach  ($ret as $oid=>$value){
        $re = '/(SNMPv2-SMI::enterprises\.9\.9\.392\.1\.3\.21\.1\.[0-9]{1,2}\.[0-9]{1,2}\.)([\.0-9]*)\.([0-9]{4,10})/'; # регулярное выражение, которое парсит вывод и выцепляет логины пользователей
        $str = $oid;
        preg_match_all($re, $str, $matches);
        $oid = explode(".",$matches[2][0]);
        $value = explode(": ",$value);
        $login = "";
        foreach ($oid as $chr){
            $login.=chr($chr);
        };
        $result[strtolower($login)][$matches[3][0]][] = @str_ireplace("\"","",$value[1]);
    };
    return $result;
#на выходе получаем массив из объектов вида [login][s_index][value]
};

$x = get_users();
$connect = connectdb();

foreach ($x as $user=>$sessions){
    $user = addslashes($user);
    foreach ($sessions as $session=>$value ){
    if (preg_match("/((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)/",$value[7]) and preg_match("/((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)/",$value[5])){
        $sql = "SELECT * from logins where login = '$user'";
        $result = mysqli_query($connect, $sql);
        $res = mysqli_fetch_array($result);
        $time = date("Y-m-d H:i:s",time()-$value[3]);
        $id_l = $res['id_l'];
        if (mysqli_num_rows($result)==0){
            $sql_1 = "INSERT INTO logins value ('','".$user."','')";
            $result_1 = mysqli_query($connect, $sql_1);
            $id_l = mysqli_insert_id($connect);
            $sql_1 = "INSERT INTO sessions (id_l,time_start,ip_source,ip_lan,s_index, profile)value ('$id_l','$time','$value[7]','$value[5]','$session', '$value[0]')";
            $result_1 = mysqli_query($connect, $sql_1);
            $id_s = mysqli_insert_id($connect);
        }
        else{
            $sql_1 = "SELECT * from sessions where (id_l = (select id_l from logins where login = '$user'))and(s_index = '$session')and(time_end = '0')";
            $result_1 = mysqli_query($connect, $sql_1);
            if (mysqli_num_rows($result_1)==0){
                $sql_2 = "INSERT INTO sessions (id_l,time_start,ip_source,ip_lan,s_index, profile)value ('$id_l','$time','$value[7]','$value[5]','$session', '$value[0]')";
                $result_2 = mysqli_query($connect, $sql_2);
            }
        }
    }
    }
}
$sql = "SELECT l.login,s.s_index FROM logins as l left join sessions as s on l.id_l=s.id_l where (s.time_end=0)";
$rw = mysqli_query($connect,$sql);
$result = mysqli_fetch_array($rw);
while ($result['s_index']>0){
    if (@!$x[strtolower($result['login'])][$result['s_index']][0]){
        $sql_1 = "UPDATE sessions SET time_end = '".date("Y-m-d H:i:s",time())."' where s_index='".$result['s_index']."'";
        mysqli_query($connect, $sql_1);
    };
    $result = mysqli_fetch_array($rw);
};
mysqli_close($connect);
?>

Логика работы:

Скрипт запускается каждый 30 секунд через crone и опрашивает по SNMP оборудование. Так как логинов пользователей в открытом виде Cisco не хранит, то необходимо вытащить логины из динамически формируемого SNMP OID. Каждый символ логин хранится с помощью ASCII кода в части SNMP OID (это почерпнул отсюда).

После отработки функции get_user() переменная $x принимает значение в следующем формате:

    [login] => Array
        (
            [s_index] => Array
                (
                    [0] => profile_name
                    .............................
                   [34] => 0
                )
        )

Описание значений можно посмотреть тут.

Дальше скрипт проверяет — есть ли логин в таблице Logins. Если такого логина нет — добавляет его туда, если есть — получает его id_l. Затем, смотрит — есть ли у этого логина открытые сессии с неустановленным датой окончания. Если нет — создаёт в таблице sessions новую запись.

Затем скрипт получает из базы список пользователей, у которых есть незавершённые сессии. И проверяет наличие логинов в опросе. Если логина нет в опросе или номер сессии не совпадает с той, которая в базе (s_index) — ему проставляется время завершения сессии.

Готов к замечаниям/исправлениям/доработкам/вопросам.

UPD: добавил strtolower() в функциях парсинга логина и в проверке существования логина в активных сессиях (почти в конце уже). Иначе, если логин содержит заглавные буквы, тогда некорректно работает всё.
Поделиться с друзьями
-->

Комментарии (16)


  1. epicf4il
    23.01.2017 00:35
    +2

    Еще можно парсить логи асы. В части SVC они достаточно информативны, их много и они имеют соответсвующие ID, что облегчает процесс получения только необходимых сообщений.

    пример некоторых сообщений
    Jan 23 2017 23:50:32: %ASA-6-734001: DAP: User privetkakdela, Addr 289.123.44.12, Connection AnyConnect: The following DAP records were selected for this connection: DfltAccessPolicy
    Jan 23 2017 23:50:32: %ASA-6-113039: Group <GroupPolicy_CBS> User < privetkakdela> IP <289.123.44.12> AnyConnect parent session started.
    Jan 22 2017 23:53:25: %ASA-4-113019: Group = CBS, Username = privetkakdela, IP = 289.123.44.12, Session disconnected. Session Type: AnyConnect-Parent, Duration: 0h:05m:28s, Bytes xmt: 12562, Bytes rcv: 921, Reason: User Requested
    


    1. NvAriec
      23.01.2017 10:11

      Изначально хотел сделать через парсинг логов, но когда настроил phplogcon чтобы посмотреть что и как там происходит — не увидел в логах ни единого упоминания об IP адресе, выдаваемого клиенту. Может плохо смотрел.

      Креденшелы только на чтение. К тому же можно ограничить выдаваемые ветки для этого пользователя.


      1. epicf4il
        23.01.2017 10:34

        Скорее всего, просто не заметили сообщения 722051

        тык пример
        Jan 23 2017 00:05:32: %ASA-4-722051: Group <GroupPolicy_CBS> User <privetkakdela> IP <289.123.44.12> IPv4 Address <192.168.3.34> IPv6 address <::> assigned to session
        


        1. NvAriec
          23.01.2017 10:42

          Может быть просто уровень логирования был не тот или может какие дополнительные настройки необходимо было сделать, но таких логов 100% не видел. Делал поиск по подсети, которую получают клиенты и ничего не находилось.


          1. epicf4il
            23.01.2017 11:12

            Как видно из примеров, для сообщений 722051 и 113019 достаточно 4-ого уровня severity (warnings). Для более детальных потребуется 6-ой или уже 7-ой.


            1. NvAriec
              23.01.2017 11:14

              Включал последний уровень для дебага.
              Будет время — покопаюсь ещё, может переделаю и дополню вторым вариантом)


  1. neumeika
    23.01.2017 14:28

    мда, а вот radius для AAA не катит?


    1. NvAriec
      23.01.2017 15:48

      Катит. Он и настроен. Вся авторизация проходит через Active Directory. Не пойму что Вы предлагаете.


      1. gotch
        23.01.2017 16:53

        Не пойму что Вы предлагаете.

        Складывать логи радиуса в SQL?


        1. neumeika
          23.01.2017 21:38

          нативно :)
          но велосипеды выгодно отличаются простотой/удобством/скоростью


        1. Openmsk
          23.01.2017 23:44

          Я писал скрипт на powershell который запускался когда пользователь логинится, в логе венды есть и адрес откуда пользователь пришёл.
          В этом же скрипте не большое HTML тело, которое отправляется на почту пользователю и СБ, там видно кто, когда, откуда пришёл.
          Завтра могу показать рыбу скрипта если надо


          1. neumeika
            23.01.2017 23:49

            скрипт по крону, али на IAS изобразили, или в скуле какой-то триггер?


            1. Openmsk
              24.01.2017 18:00

              sql не используется.
              шедулер на радиус сервере запускается как только в логе появляется событие, и запускает скрипт, который хватает это событие и раскладывает на нужные нам поля.

              вот так выглядит начало скрипта:
              $event = Get-WinEvent -FilterHashtable @{Logname='Security';Id=6272} -MaxEvents 1
              $eventXML = [xml]$Event.ToXml()
              $ipaddrr = ((($eventXML.Event.EventData.Data) | ? Name -eq 'CallingStationID').("#text"))
              $userx = ((($eventXML.Event.EventData.Data) | ? Name -eq 'SubjectUserSid').("#text"))
              $mailx = (get-aduser $userx -properties *).mail
              $namex = (get-aduser $userx -properties *).name
              $logonname = (get-aduser $userx -properties *).SamAccountName
              


  1. VFedorV
    23.01.2017 18:04

    у нас безопасник настроил OSSIM (парсит логи ASA, которые ему пересылаются syslog-ом) грамотно:

    On 2017-01-23 05:18:10 (UTC time) user dmitriy.budylin connected to prd environment via AnyConnect VPN from 93.хх.20.х2 as a member of GroupPolicy_AnyConnect_ADMINS access group

    User: dmitriy.хххlin
    Src: 93.хх.20.х2 — #внешний IP клиента
    Group: GroupPolicy_AnyConnect_ADMINS

    Alert detail:
    * userdata2: GroupPolicy_AnyConnect_ADMINS
    * userdata3: 10.хх.12.хх # выданный внутр. IP
    * userdata1: ASA-4-722051
    * protocol: tcp
    * rep_rel_dst: 0
    * context_id: 8639cea8-58b0-11e6-ххх-a71c969e53c4
    * actions: 3
    * reliability: 2
    * plugin_sid: 722051
    * rep_prio_src: 0
    * priority: 1
    * src_port: 0
    * event_id: e12b11e6-81de-0015-5d03-2b0f4ce08bac
    * src_ip: 93.хх.20.х2
    * backlog_id: 5d032b0f-e12b-11e6-a547-00154ce75ab8
    * plugin_id: 1636
    * sensor: 10.х.5х.8
    * username: dmitriy.budylin
    * risk: 0
    * rep_prio_dst: 0
    * date: 2017-01-23 08:18:10
    * type: event
    * rep_rel_src: 0
    * dst_port: 0
    * dst_ip: 0.0.0.0
    * policy_id: dd932f97-da98-хххх-425c-32be769455bc


  1. ultraElephant
    24.01.2017 09:05

    Недавно делал менее красивое решение, но для заббикса.

    В конце — ссылка на скрипт и шаблон, если кому нужно.


    1. NvAriec
      24.01.2017 09:09

      Не поверите, как раз потом свою базу пихал в Zabbix отдельной страницей)
      Спасибо за ссылку.