Аудит актуальных уязвимостей без регистрации и смс
Вступление
Как известно каждому, кто хоть раз подписывался на рассылки по ИБ, количество найденных за день уязвимостей часто превышает возможности человека по их разбору. Особенно, если серверов — много, особенно если там зоопарк из ОС и версий.
В этом топике я расскажу о том, как мы решили эту проблему. И да, Perl* жив:)
Цели и задачи
При проектировании системы мы с mkhlystun** решали два параллельные задачи:
- Сбор информации о версиях пакетов на системах
- Сбор информации о текущих уязвимостях на системах
Обе эти активности преследовали общую цель: правильно расставить приоритеты при обновлениях и выполнить эти обновления в гарантированный даунтайм, чего мы и достигли.
Схема работы
- Раз в час каждый хост собирает информацию о текущих пакетах и отправляет её очередь
- Из очереди сообщения выгребаются и помещаются в базу
- Раз в 3 часа в базу приходит робот и прогоняет пакеты через сервис аудита
- По итогам за сутки генерируется отчет, который высылается всем заинтересованным
Схема базы
pkgs.sql
CREATE TABLE hosts (
hostname character varying(255) NOT NULL PRIMARY KEY,
os character varying(255),
pkg_id integer[]
);
CREATE INDEX hosts_pkg_id_idx ON hosts USING gin (pkg_id);
CREATE TABLE pkg (
id SERIAL NOT NULL PRIMARY KEY,
name character varying(255) NOT NULL
);
CREATE UNIQUE INDEX pkg_name_idx ON pkg USING btree (name);
CREATE TABLE vulners (
id character varying(255) NOT NULL PRIMARY KEY,
cvss_score double precision DEFAULT 0.0 NOT NULL,
cvss_vector character varying(255),
description text,
cvelist text
);
CREATE TABLE v2p (
pkg_id integer NOT NULL REFERENCES pkg(id),
vuln_id character varying(255) NOT NULL REFERENCES vulners(id)
);
В таблицу hosts заносится информация о хостах (один хост = одна запись), параллельно заполняется информация в pkg (один пакет — одна запись), во избежание дублирования информации. Массив был выбран исторически, жить с ним вполне можно.
Параллельно в таблицу vulners заносится информация о текущих уязвимостях для пакетов, таблица связи v2p позволяет делать привязку many-to-many.
Сбор информации о пакетах
grabber.pl
#!/usr/bin/perl
# (C) kreon 2016
use strict;
use warnings;
use JSON;
# config
our %grabs = (
'centos|oraclelinux|redhat|fedora' => q(rpm -aq),
'debian|ubuntu' => q(dpkg-query -W -f='${Package} ${Version} ${Architecture}\n'),
'osx' => q(pkgutil --pkgs)
);
our %unames = (
'linux' => q(lsb_release -a),
'darwin' => q(echo "Distributor ID: OSX")
);
# global vars
our $hostname = `hostname -f`;
our ($vercmd, $grabcmd, $operatingsystem, $version);
# do uname
my $uname = `uname`;
chomp $uname;
foreach (keys %unames) {
$vercmd = $unames{$_} if $uname =~ /$_/i;
}
die "Version CMD not found" unless $vercmd;
# do version check
foreach (`$vercmd`) {
chomp;
/^Distributor ID:\s*(\S[\S\s]+)$/ and $operatingsystem = $1;
/^Release:\s*(\S[\S\s]+)$/ and $version = $1;
}
die "Opetating System not found" unless $operatingsystem;
foreach (keys %grabs) {
$grabcmd = $grabs{$_} if $operatingsystem =~ /$_/i;
}
# grab pkgs
die "Opetating System not found" unless $grabcmd;
my @pkgs;
foreach (`$grabcmd`) {
chomp;
push @pkgs, $_;
}
chomp $hostname;
my $result = {
hostname => $hostname,
os => $version ? qq($operatingsystem $version) : $operatingsystem,
pkgs => [ sort @pkgs ]
};
#
print JSON->new->encode($result);
# done
1;
Для тех, кто не знает Perl: просто последовательно выполняются команды hostname -f, lsb_release -a и rpm -aq|dpkg-query -W, все это упаковывается в JSON и выводится для отправки в очередь сообщений.
Трансформация JSON в базу
transform.pl
#!/usr/bin/perl
# (C) kreon 2016
use strict;
use warnings;
use JSON;
use DBI;
use constant DB => 'dbi:Pg:dbname=pkgs';
# 0. create connection
my $dbh
= DBI->connect( DB, "", "", { RaiseError => 1, AutoCommit => 0 } );
# 1. read from stdin and parse
my $data = JSON->new->decode( join( "", ) );
# if data == array parse foeach
if ( ref($data) eq "ARRAY" ) {
foreach (@$data) {
parse_host($_);
}
}
else {
parse_host($data);
}
# Done
1;
### SUBS ###
sub parse_host {
$_ = shift;
# do parse packages
my ( $hostname, $os, @pkgs )
= ( $_->{hostname}, $_->{os}, @{ $_->{pkgs} } );
my @pkgids;
eval {
foreach (@pkgs) {
my $sth = $dbh->prepare("SELECT id FROM pkg WHERE name=?");
$sth->execute( ($_) );
if ( my ($id) = $sth->fetchrow_array ) {
push @pkgids, int($id);
}
else {
$dbh->do( "INSERT INTO pkg (name) VALUES(?)", undef, $_ );
push @pkgs, $_;
}
}
};
$dbh->rollback and die "$@" if $@;
$dbh->commit;
# do parse host
eval {
my $sth = $dbh->prepare("SELECT os FROM hosts WHERE hostname=?");
$sth->execute( ($hostname) );
if ( my ($os2) = $sth->fetchrow_array ) {
if ( lc($os2) ne lc($os) ) {
$dbh->do( "UPDATE hosts SET os=? WHERE hostname=?",
undef, $os, $hostname );
}
}
else {
$dbh->do( "INSERT INTO hosts (hostname, os) VALUES(?, ?)",
undef, $hostname, $os );
}
};
$dbh->rollback and die "$@" if $@;
$dbh->commit;
# do set packages
eval {
$dbh->do( "UPDATE hosts SET pkg_id=? WHERE hostname=?",
undef, [@pkgids], $hostname );
};
$dbh->rollback and die "$@" if $@;
$dbh->commit;
}
Данный скрипт получает на вход json, полученный из очереди, а затем раскладывает его в базу в три этапа:
— Сначала кладет пакеты, проверяя их уникальность
— Затем кладет хосты, обновляя версию при необходимости
— Затем связывает хосты и пакеты через массив
Аудит
Когда мы искали способ аудировать наши пакеты, мы долго перебирали варианты, пока на какой-то конфе нам на глаза не попалась визитка vulners. Это агрегатор уязвимостей, который делают isox и videns. Я связался с ними и попросил помочь. Итогом стало Audit API.
Audit API
Для получения информации об уязвимостях достаточно собрать блоб пакетов в json и отправить его на /api/v3/audit/audit/.
В ответ сервер отдаст json со списом текущих уязвимостей в следующем формате:
POST /api/v3/audit/audit/ HTTP/1.0
Host: vulners.com
Content-Type: application/json
Content-Length: 377
{ "os":"CentOS", "version":"7", "package":["kernel-3.10.0-229.el7.x86_64"]}
В ответ сервер отдаст json со списом текущих уязвимостей в следующем формате:
{
"result": "OK",
"data": {
"packages": {
"kernel-3.10.0-229.el7.x86_64": {
"CESA-2015:2152": [
{
"package": "kernel-3.10.0-229.el7.x86_64",
"providedVersion": "0:3.10.0-229.el7",
"bulletinVersion": "3.10.0-327.el7",
"providedPackage": "kernel-3.10.0-229.el7.x86_64",
"bulletinPackage": "kernel-3.10.0-327.el7.x86_64.rpm",
"operator": "lt",
"bulletinID": "CESA-2015:2152"
}
],
"CESA-2015:1978": [
{
"package": "kernel-3.10.0-229.el7.x86_64",
"providedVersion": "0:3.10.0-229.el7",
"bulletinVersion": "3.10.0-229.20.1.el7",
"providedPackage": "kernel-3.10.0-229.el7.x86_64",
"bulletinPackage": "kernel-3.10.0-229.20.1.el7.src.rpm",
"operator": "lt",
"bulletinID": "CESA-2015:1978"
},
// skipped
],
"CESA-2016:0064": [
{
"package": "kernel-3.10.0-229.el7.x86_64",
"providedVersion": "0:3.10.0-229.el7",
"bulletinVersion": "3.10.0-327.4.5.el7",
"providedPackage": "kernel-3.10.0-229.el7.x86_64",
"bulletinPackage": "kernel-3.10.0-327.4.5.el7.src.rpm",
"operator": "lt",
"bulletinID": "CESA-2016:0064"
},
// skipped
],
// skipped
],
// skipped
"cvss": {
"score": 10.0,
"vector": "AV:NETWORK/AC:LOW/Au:NONE/C:COMPLETE/I:COMPLETE/A:COMPLETE/"
},
"cvelist": [
"CVE-2014-9644",
"CVE-2016-2384",
// skipped
],
"id": "F777"
}
}
После обкатки API был написан код, который пушит список пакетов по OS и в ответ получает список уязвимостей и кладет их в базу.
audit.pl
#!/usr/bin/perl
# (C) kreon 2016
use strict;
use warnings;
use lib 'perl5';
use HTTP::Tiny;
use DBI;
use JSON;
use constant VULNERS_AUDIT_API => 'http://vulners.com/api/v3/audit/audit/';
use constant VULNERS_ID_API => 'http://vulners.com/api/v3/search/id/';
use constant DB => 'dbi:Pg:dbname=pkgs';
our %VULNS;
our $dbh;
our %pkgs = ();
# 0. connect to DB
$dbh = DBI->connect( DB, "", "", { RaiseError => 1, AutoCommit => 0 } );
# get all OS variations
my @os = get_os();
# for each OS get all packages and ask vulners for its vulnerabilities
foreach my $os (@os) {
eval {
my ( $o, $ver ) = split( / /, $os );
my $res = HTTP::Tiny->new->request(
'POST',
VULNERS_AUDIT_API,
{ headers => { 'Content-Type' => 'application/json' },
content => JSON->new->encode(
{ os => $o,
version => $ver,
package => [ get_packages($os) ]
}
)
}
);
if ( !$res->{success} ) {
die "HTTP Error: $res->{content}";
}
my $data = JSON->new->decode( $res->{content} );
my $vulns = $data->{data}->{packages};
return undef unless defined $vulns;
foreach ( keys %$vulns ) {
my $o = $vulns->{$_};
if ( defined( $pkgs{$_} ) ) {
$VULNS{ $pkgs{$_} } = [ keys %$o ];
}
}
};
print $@ if $@;
}
# Now get info on each vuln ID ( CESA, USN, etc ) ...
my @result;
my $res = HTTP::Tiny->new->request(
'POST',
VULNERS_ID_API,
{ headers => { 'Content-Type' => 'application/json' },
content => JSON->new->encode( { id => [ map {@$_} values %VULNS ] } )
}
);
if ( !$res->{success} ) {
die "HTTP Error: $res->{content}";
}
my $data = JSON->new->decode( $res->{content} );
foreach ( values %{ $data->{data}->{documents} } ) {
push @result,
{
id => $_->{id},
cvss_score => $_->{cvss}->{score},
cvss_vector => $_->{cvss}->{vector},
description => $_->{description},
cvelist => join( ', ', @{ $_->{cvelist} } ),
};
}
# Insert the data to DB
eval {
$dbh->do( "DELETE FROM v2p", undef );
$dbh->do( "DELETE FROM vulners", undef );
# insert prepared data to vulners table
foreach (@result) {
$dbh->do(
"INSERT INTO vulners (id, cvss_score, cvss_vector, description, cvelist) VALUES (?,?,?,?,?)",
undef,
$_->{id},
$_->{cvss_score},
$_->{cvss_vector},
$_->{description},
$_->{cvelist}
);
}
# and link pkg and vuls into v2p
foreach my $pkg_id ( keys %VULNS ) {
foreach my $vuln_id ( @{ $VULNS{$pkg_id} } ) {
$dbh->do( "INSERT INTO v2p(pkg_id,vuln_id) VALUES(?,?)",
undef, $pkg_id, $vuln_id );
}
}
};
$dbh->rollback and die "Error $@" if $@;
$dbh->commit;
# All done
1;
### SUBS ####
sub get_os {
my @os;
my $sth = $dbh->prepare("SELECT DISTINCT os FROM hosts");
$sth->execute();
while ( my ($os) = $sth->fetchrow_array ) {
push @os, $os;
}
return @os;
}
sub get_packages {
my $os = shift;
my $sth
= $dbh->prepare(
"select DISTINCT p.id,p.name FROM pkg p RIGHT JOIN hosts h ON (p.id=ANY(h.pkg_id)) WHERE h.os=?"
);
$sth->execute( ($os) );
my @pkgs;
while ( my ( $id, $name ) = $sth->fetchrow_array ) {
$pkgs{$name} = $id;
push @pkgs, $name;
}
return @pkgs;
}
Алгорим:
— Берем список OS из таблицы hosts
— По каждой OS получаем список пакетов
— Отправляем пакеты в Audit API, получаем список (id) уязвимостей
— Отправляем уязвимости в ID Api, получаем по каждой метаданные
— Пишем в базу метаданные уязвимостей в таблицу vulners
— Пишем в базу связи пакетов и уязвимостей в таблицу v2p
Отчеты
Поскольку основной нашей целью было получать список хостов для приоритетного обновления, первый отчет, который я сделал, был 'top10 hosts to update', выбранный по суммарному CVSS Score***.
report.pl
#!/usr/bin/perl
# (C) kreon 2016
use strict;
use warnings;
use lib 'perl5';
use HTTP::Tiny;
use DBI;
use JSON;
use constant DB => 'dbi:Pg:dbname=pkgs';
our $dbh;
our @hosts;
# 0. connect to DB
$dbh = DBI->connect( DB, "", "", { RaiseError => 1, AutoCommit => 0 } );
# get top10 hosts
my $sth = $dbh->prepare("SELECT h.hostname,SUM(v.cvss_score) as sum FROM hosts h INNER JOIN pkg p ON(p.id=ANY(h.pkg_id)) INNER JOIN v2p vp ON(vp.pkg_id=p.id) INNER JOIN vulners v ON (v.id=vp.vuln_id) GROUP BY h.hostname ORDER BY sum DESC LIMIT 10");
$sth->execute();
while (my ($host, $sum) = $sth->fetchrow_array) {
push @hosts, { hostname => $host, score => $sum, pkgs => [] };
}
foreach (@hosts) {
$sth = $dbh->prepare("SELECT p.name,SUM(v.cvss_score) AS score FROM pkg p RIGHT JOIN hosts h ON (p.id=ANY(h.pkg_id)) INNER JOIN v2p ON (v2p.pkg_id=p.id) INNER JOIN vulners v ON (v.id=v2p.vuln_id) WHERE h.hostname=? GROUP BY p.name ORDER BY score DESC LIMIT 10");
$sth->execute(($_->{hostname}));
while(my ($pkg,$sum) = $sth->fetchrow_array) {
push @{$_->{pkgs}}, { package => $pkg, score => $sum };
}
}
print <{hostname}
Score : $_->{score}
Packages:
EOF
;
foreach (@{$_->{pkgs}}) {
print <{package}
Score: $_->{score}
EOF
}
}
Данный код просто берет топ10 хостов и для каждого из них берет топ 10 пакетов с суммарным скором. Дальше можно создавать задачи и обновлять :)
Итоги года использования
Уже больше года мы отправляем наши данные ребятам из Vulners. На текущий момент они постоянно аудируют более 30 000 уникальных пакетов. Что приятно, все найденные баги оперативно исправлялись, а скорость обработки каждой тысячи выросла с 30 секунд до 400 милисекунд. Именно благодаря им этот топик назван »… без регистрации и смс» :)
Что касаемо бизнес-целей, то только с внедрением данной системы у нас начал появляться процесс постоянных обновлений. Обновить все — слишком большая задача для дежурного инженера, а обновить первые 10 — вполне посильная. За год мы уронили суммарный cvss score более чем вдвое, чего и вам желаем****:)
Сноски и пояснения
* — Так сложилось исторически, я начал писать автоматизацию на Perl и никто не успел меня остановить.
** — Миша не слишком активный пользователь хабра, но как инженер он незаменим :)
*** — Цифровая метрика опасности уязвимости, от 1.0 (можно забить) до 10.0 (критичная уязвимость)
**** — Весь код можно найти тут: github.com/kreon/freeaudit
P.S. И тем не менее — Perl жив!