Blocage dynamique des attaques web

Frédéric VANNIÈRE  - Homere

f.vanniere@planet-work.com

Sysadmin #6 - 18-19 février 2016

Présentation

  • responsable technique depuis 2001
  • hébergement mutualisé
  • serveurs infogérés

Contexte

  • 10 000 sites
  • 30% Wordpress
  • nombreuses attaques, sites piratés
  • NetApp qui rame (NFSv3, GETATTR)

Besoin d'une protection

ww.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:42 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.344
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:43 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.329
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:43 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 0.805
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:43 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.329
alteora.machin-virtuel.biz 86.66.32.177 - - [18/Feb/2016:02:54:43 +0100] "POST /modules/screensaver/rech_next_media.php HTTP/1.1" 200 289 "http://alteora.machin-virtuel.biz/modules/screensaver/index_ssaver.php" "Mozilla/5.0 (Windows NT 6.1; rv:41.0) Gecko/20100101 Firefox/41.0" 0.045
www.monsite.com 82.146.44.148 - - [18/Feb/2016:02:54:44 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57012 "-" "-" 1.487
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:44 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.329
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:44 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 1.054
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:44 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.336
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:45 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 0.792
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:45 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.335
lamar2.machinvirtuel.fr 83.204.221.120 - - [18/Feb/2016:02:54:45 +0100] "POST /modules/screensaver/rech_next_media.php HTTP/1.1" 200 301 "http://lamar2.machinvirtuel.fr/modules/screensaver/index_ssaver.php" "Mozilla/5.0 (Windows NT 6.1; rv:44.0) Gecko/20100101 Firefox/44.0" 0.045
solutruc.ci 66.249.66.64 - - [18/Feb/2016:02:54:45 +0100] "POST /fr/produits/EasyVistaITServiceManager/gestion-des-changements HTTP/1.1" 200 6331 "http://solutruc.ci/fr/produits/EasyVistaITServiceManager/gestion-des-changements" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" 0.087
www.monsite.com 82.146.44.148 - - [18/Feb/2016:02:54:46 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57012 "-" "-" 1.911
lamaranmaiaf.fr 80.11.89.63 - - [18/Feb/2016:02:54:46 +0100] "POST /calcul.php HTTP/1.1" 200 438 "http://lamaranmaiaf.fr/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36" 0.048
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:46 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.427
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:46 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 0.912
liguepacadessportsdeglisse.fr 195.154.240.184 - - [18/Feb/2016:02:54:46 +0100] "POST /wp-login.php HTTP/1.1" 200 5252 "http://liguepacadessportsdeglisse.fr/wp-login.php" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; 125LA; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)" 0.097
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:47 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.404
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:47 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 0.974
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:47 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.404
www.monsite.com 82.146.44.148 - - [18/Feb/2016:02:54:47 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57012 "-" "-" 1.559
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:48 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 0.967
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:48 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.333
www.xxaagroup.com 195.182.94.10 - - [18/Feb/2016:02:54:48 +0100] "POST / HTTP/1.0" 200 19988 "http://www.xxaagroup.com/" "Mozilla/5.0 (Windows NT 6.1; rv:40.0) Gecko/20100101 Firefox/40.0" 0.444
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:49 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.365
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:49 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 1.031
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:49 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.340
www.monsite.com 82.146.44.148 - - [18/Feb/2016:02:54:50 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57012 "-" "-" 1.998
lamaranmaiaf.fr 80.11.89.63 - - [18/Feb/2016:02:54:50 +0100] "POST /calcul.php HTTP/1.1" 200 438 "http://lamaranmaiaf.fr/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36" 0.057
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:50 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.331
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:50 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 0.960
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:51 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.365
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:51 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 0.882
www.monsite.com 82.146.44.148 - - [18/Feb/2016:02:54:51 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57012 "-" "-" 1.485
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:51 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.475
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:52 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 1.143
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:52 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.363
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:53 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 0.831
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:53 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.365
www.monsite.com 82.146.44.148 - - [18/Feb/2016:02:54:53 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57012 "-" "-" 1.845
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:54 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.334
alteora.machin-virtuel.biz 86.66.32.177 - - [18/Feb/2016:02:54:54 +0100] "POST /modules/screensaver/rech_next_media.php HTTP/1.1" 200 156 "http://alteora.machin-virtuel.biz/modules/screensaver/index_ssaver.php" "Mozilla/5.0 (Windows NT 6.1; rv:41.0) Gecko/20100101 Firefox/41.0" 0.044
www.site44.com 109.228.22.171 - - [18/Feb/2016:02:54:54 +0100] "POST /xmlrpc.php HTTP/1.0" 200 57292 "-" "-" 0.941
grep POST /var/log/nginx.access.log 

Nginx

  • reverse-proxy partout
  • gestion directe du contenu statique

Apache / PHP

Apache / PHP

Apache / PHP

Apache / PHP

Nginx

NFS (NetApp)

OpenResty

  • = Nginx + modules
  • Lua
  • soutenu par Cloudflare
worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen 80;
        location / {
            default_type text/html;
            access_by_lua_block {
                 -- check the client IP address is in our black list
                 if ngx.var.remote_addr == "132.5.72.3" then
                     ngx.exit(ngx.HTTP_FORBIDDEN)
                 end

                 -- check if the URI contains bad words
                 if ngx.var.uri and
                        string.match(ngx.var.request_body, "evil")
                 then
                     return ngx.redirect("/terms_of_use.html")
                 end
                 -- tests passed
            }

            content_by_lua '
                ngx.say("<p>hello, world</p>")
            ';
        }
    }
}

Blocage DNSBL

  • base clé/valeur simple
  • ajout d'entrée par API PW
  • blocage site piraté et IP cliente
lua_shared_dict blacklist_cache 50m;
lua_package_path "/usr/local/openresty/lib/?.lua;;";
access_by_lua_file /etc/nginx/lua/blacklist.lua;
/etc/nginx/conf.d/blacklist.conf
local blacklist_cache = ngx.shared.blacklist_cache
function check_access()
        ngx.var.blacklisted = ''
        -- Avoid DNS lookups when address in cache
        if blacklist_cache:get(ngx.var.remote_addr .. "_hostwl") then
             return
        end

        if blacklist_cache:get(ngx.var.remote_addr .. "_wl") and blacklist_cache:get(ngx.var.host .. "_wl") then
                return
        end

        if blacklist_cache:get(ngx.var.remote_addr .. "_bl") then
                blacklist(false)
        end

        if blacklist_cache:get(ngx.var.host .. "_bl") then
                blacklist_host(false)
        end

        local blacklisted_host = dnsbl_check_host(ngx.var.host,"web.dnsbl.pw")
        if blacklisted_host == 0 then
                whitelist_host()
        else
                blacklist_host(true)
        end

        local blacklisted_ip = dnsbl_check(ngx.var.remote_addr,"web.dnsbl.pw")
        if blacklisted_ip == 0 then
           whitelist()
        else
           blacklist(true)
        end
end

check_access()

Gestion des logs

  • rsyslog
  • centralisation
  • authentification source et destination
/var/log/nginx.log

rsyslog

rsyslog

/home/syslog/host/jerry/2016/02/16/nginx-access.log

rfc5424  / TLS

$InputFile

HekaD

  • 1er essai avec Logstash 1.4 ... nope !
  • projet Mozilla
  • gestion de messages
  • go + sandboxes en Lua
# Lecture fichier log de rsyslog
[SyslogFileInput]
type = "LogstreamerInput"
log_directory = "/home/syslog/global/"
file_match = 'syslog-(?P<Year>\d+)-(?P<Month>\d+)-(?P<Day>\d+)-(?P<Hour>\d+)\.log'
priority = ["Year", "Month", "Day","Hour"]
decoder = "JsonSyslogDecoder"

# Décodage
[JsonSyslogDecoder]
type = "SandboxDecoder"
memory_limit = 64384000
instruction_limit = 100000000000
filename = "lua_decoders/syslog_pw.lua"
    [JsonSyslogDecoder.config]
    type = "syslog"

# Encodage pour ES
[ESJsonEncoder]
index = "%{Type}-%{%Y.%m.%d}"
es_index_from_timestamp = true
type_name = "%{Type}"
fields = ["Timestamp","Payload","Severity","Fields","Hostname"]
[ESJsonEncoder.field_mappings]
    Timestamp = "timestamp"
    Severity = "severity"
    Payload = "message"
    Hostname = "hostname"


[ElasticSearchOutput]
message_matcher = "Type == 'syslog'"
server = "http://es1:9200"
flush_interval = 10000
flush_count = 1000
http_timeout = 10000
connect_timeout = 10000
encoder = "ESJsonEncoder"
function process_message()
    local payload = read_message("Payload")
    json = syslog_grammar:match(payload)
    msg.EnvVersion = 1
    msg.Severity = json.pri.severity
    msg.Hostname = json.hostname
    msg.Timestamp = json.timestamp
    tenant, tenant_id = pwlog.get_tenant(msg.Hostname)
    msg.Fields["tenant"] = tenant
    msg.Fields["appname"] = json["app-name"]
    msg.Fields["syslog_pid"] = tonumber(json["procid"])

    http_access.extract_fields(msg)
    apache_error.extract_fields(msg)
    mail_dovecot.extract_fields(msg)
    mail_exim.extract_fields(msg)
    mail_spamd.extract_fields(msg)
    cron.extract_fields(msg)
    ssh_shell.extract_fields(msg)
    ftpd.extract_fields(msg)

    if msg.Fields["remoteAddr"] then
         res = geoip_country_lookup:query_by_addr(msg.Fields["remoteAddr"])
         msg.Fields["remoteCountry"] = res["code"]
    end

    inject_message(msg)
end
      
  • message immuable une fois décodé
  • communication par injection de messages
www.aabb.fr 189.89.5.120 - - [18/Feb/2016:02:54:46 +0100] "POST /administrator/index.php HTTP/1.1" 200 5005 "-" "-" 0.427<134>1 2016-02-17T17:00:19.792783+01:00 jerry nginx-access - -
[7edd1926-ad91-414b-b09e-8bbb1880c08a@34200 tag="nginx"] www.monclub-forme.com
213.62.432.223 - - [17/Feb/2016:17:00:12 +0100] "GET /faq-monclub.htm
HTTP/1.1" 200 9834 "http://www.monclub-forme.com/contacts-monclub.htm"
"Mozilla/5.0 (Windows NT 6.1; rv:44.0) Gecko/20100101 Firefox/44.0" 0.066

{
   "timestamp":"2016-02-17T16:00:12",
   "message":"www.monclub-forme.com 213.62.432.223 - - [17/Feb/2016:17:00:12 +0100] \u0022GET /faq-monclub.htm HTTP/1.1\u0022 200 9834 \u0022http://www.monclub-forme.com/contacts-monclub.htm\u0022 \u0022Mozilla/5.0 (Windows NT 6.1; rv:44.0) Gecko/20100101 Firefox/44.0\u0022 0.066",
   "severity":6,
   "http_userAgent":"Mozilla/5.0 (Windows NT 6.1; rv:44.0) Gecko/20100101 Firefox/44.0",
   "http_vhost":"www.monclub-forme.com",
   "http_uri":"/faq-monclub.htm",
   "http_status":200,
   "http_outBytes":9834,
   "http_referer":"http://www.monclub-forme.com/contacts-monclub.htm",
   "user":"sitemonclub",
   "remoteAddr":"213.62.432.223",
   "appname":"nginx-access",
   "http_protocol":"HTTP/1.1",
   "http_host":"www.monclub-forme.com",
   "tenant":"mutupw",
   "http_method":"GET",
   "remotePort":0,
   "http_requestTime":0.066,
   "http_remoteUser":"",
   "remoteCountry":"FR",
   "hostname":"jerry"
}

Detection et blocage

[CheckWebActivity]
    type = "SandboxFilter"
    ticker_interval = 5
    message_matcher = "Fields[appname] == 'nginx-access'"
    filename = "lua_filters/check_web_activity.lua"
    preserve_data = true
    [CheckWebActivity.config]
        preservation_version = 6
        max_duration = 7200
        ### Heka ne gère pas les tableaux dans la conf des sandbox Lua !! codé en dur dans le script lua
        countries_good = ['BE','LU','GP','TN','RE','CH','DZ','IT']
        countries_bad = ['TR','RU','UA','CN','AR','BY','NL']
        whitelist_ips = ['79.99.16','82.223.72.5', '78.64.152.153', '83.153.215.142', '78.273.72.25']
        protect_uri = ['/wp-login.php', '/wp-comments-post.php', 'ucp.php?mode=login','/administrator/','/login.php?action=in', '/xmlrpc.php', 'plugin_googlemap2_proxy.php']
function process_message()
    local limit = 50

    for i,v in ipairs(countries_bad) do
       if v == ip_country then
          limit = limit / 2
       end
    end

    if http_protocol == "HTTP/1.0" then
       limit = limit / 10
    end

    if numpost >= limit then
        add_to_payload(" BLOCKED")
        pwlog.blacklist_ip(ip,"web")
    end

    inject_payload("txt")
    return 0
end

Openresty <3

  • Software Defined Server
  • gestion HTTP et maintenant  TCP
  • validation automatique Let's Encrypt
  • SSL dynamique
function get_cert()
    local certs = ngx.shared.certs
    local certs_data = ngx.shared.certs_data
    ssl.clear_certs()
    key  = certs:get(server_name .. "_k")
    cert = certs:get(server_name .. "_c")

    if key ~= nil and cert ~= nil then
    else
        key = "/etc/ssl/private/default.key"
        cert = "/etc/ssl/certs/default.pem"
    end
    local ok, err = ssl.set_der_cert(ssl.cert_pem_to_der(certs_data:get(cert)))
    local ok, err = ssl.set_der_priv_key(certs_data:get(key))
end


function load_certs()
    local json = require "cjson"
    local certs = ngx.shared.certs
    local certs_data = ngx.shared.certs_data
    f = io.popen("/etc/nginx/lua/get_certs.py","r")
    line = f:read ("*l")
    local data = json.decode(line)
    for server_name,v in pairs(data.hosts) do
            local success, err, forcible = certs:set(server_name .. "_k", data.hosts[server_name].key_file,0)
            success, err, forcible = certs:set(server_name .. "_c", data.hosts[server_name].cert_file,0)
    end
    for cert_file,der in pairs(data.certs) do
        local success, err, forcible = certs_data:set(cert_file, ngx.decode_base64(der),0)
    end
end

if ngx.get_phase() == "init" then
    load_certs()
else
    get_cert()
end

Hekad ++

  • métrologie
  • collectd → aggrégation → influxd
  • détection d'anomalie
  • monitoring, alerting 

Questions

On recrute aussi !

Paris / Rennes

→ http://pw.fr

Made with Slides.com