Creating a Real-time HoneyPot Attack Map

Every device connected to the internet is open for cyber attacks. It takes less than one minute before a system is attacked once it is connected to the internet. Recently, I worked on a hackathon project to visualize honeypot attacks on a map in real-time.

A honeypot is a computer system that mimics a target for hackers. It tries to fool hackers into thinking it is a real computer system, distracting them from other targets.

Initial Setup

There are many honeypot systems around but for this project I used T-Pot created by Deutsche Telekom Security. It consists of many honeypot daemons and tools out-of-the-box and is easy to setup.

To create a live map, we need to have the following:

  • Running T-Pot instance
  • Small server with a webserver (nginx) and Node.js/NPM

Follow the instructions to install T-Pot. Confirm your T-Pot instance is running and you see attacks appearing in the dashboards.

There are many instructions on how to install a server with Nginx and Node.js (one example can be found here).

Node.js Application to Receive Logs

On the webserver, we create a small Node.js application that will do two simple tasks:

  • Receive data from the T-Pot installation (logstash)
  • Run a small websocket server to broadcast the received data to connected clients

Install required packages
In our Node.js application we use two packages: `express` and `ws`. First install both packages:

npm install ws express

Now we create a small application called `server.js`:

vi server.js

Insert the following code into the file:

#!/usr/bin/env nodejs
const http = require('http');
const WebSocket = require('ws');
const express = require('express');
const app = express();

const PORT = 8080;
const WS_PORT = 8081;

app.use(express.json());

// Create a WebSocket Server so clients can connect to it
const wss = new WebSocket.Server({ port: WS_PORT })
wss.on('connection', function connection(ws) {
});

// Now we create a simple HTTP server which receives a message
// and forwards the message to the connected WebSocket clients
app.get('/', (req, res) => {
  res.send('Hello');
});

app.post('/', (req, res) => {
  wss.clients.forEach(function each(client) {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify(req.body));
    }
  });
  res.sendStatus(200);
});

app.listen(PORT, () => {
   console.log('The server is running at port 8080!');
});

This small Node.js application simply listens on port 8080 to receive a POST message. WebSockets can connect to port 8081. Once a message is received on port 8080, it is sent to all connected WebSocket clients.

Test Application

To test your application, make it executable:

chmod +x server.js

Now run the application:

./server.js

The output will be:

The server is running at port 8080!

You can use a process manager like PM2 to daemonize your application:

sudo npm install -g pm2
pm2 start server.js

PM2 will restart applications automatically if the application crashed or is killed. In order to have your application to run after a system (re)boot, you will need to execute another command:

pm2 startup systemd

This command output might include a command which needs to be run with superuser privileges:

sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u your_user — hp /home/your_user

Webpage Showing the Map

Now we will create a small webpage, and add some Javascript code. This code will open a websocket to receive updates and plot them on a map. To create a world map, I used Mapbox GL JS. You will need to create a (free) account in order to create an API key that will be used to create a map.

If you have a server running default Nginx, create a new `index.html` in the web-root folder:

cd /var/www/html
vi index.html

Insert the following HTML code into the file:

<!DOCTYPE HTML>
<html>
   <head> 
   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
   <script src='https://api.mapbox.com/mapbox-gl-js/v2.0.1/mapbox-gl.js'></script>
    <link href='https://api.mapbox.com/mapbox-gl-js/v2.0.1/mapbox-gl.css' rel='stylesheet' />  
    <style>
        @import url(https://fonts.googleapis.com/css?family=Inconsolata:400,500);

        body { background-color: black }
        #map { height: calc(100vh - 275px);; width: ; z-index: 1; }
        .table { color: #fff; font-family: Inconsolata,sans-serif; font-size: 15px; border-color: #525252;}    
        .thead { font-weight: 700; color: #525252; }            
        
        @-webkit-keyframes flashrow {
           from { background-color: #525252; }
           to { background-color: var(--bs-table-bg); }
         }
         @-moz-keyframes flashrow {
           from { background-color: #525252; }
           to { background-color: var(--bs-table-bg); }
         }
         @-o-keyframes flashrow {
           from { background-color: #525252; }
           to { background-color: var(--bs-table-bg); }
         }
         @keyframes flashrow {
           from { background-color: #525252; }
           to { background-color: var(--bs-table-bg); }
         }
         .flashrow {
           -webkit-animation: flashrow 1.5s; /* Safari 4+ */
           -moz-animation:    flashrow 1.5s; /* Fx 5+ */
           -o-animation:      flashrow 1.5s; /* Opera 12+ */
           animation:         flashrow 1.5s; /* IE 10+ */
         }
    </style>   
   </head>   
   <body>
   <div class="container">   
    <div class="row">
      <div class="col">
         <div id="map"></div>
      </div>
   </div>
   <div class="row">
      <div class="col" id="ticker">
         <table id="tickettable" class="table table-black ticker">
            <thead class="text-uppercase thead">
               <tr>
                  <th class="col-lg-1 thead">Time</th>
                  <th class="col-lg-2 thead">Country</th>
                  <th class="col-lg-3 thead">AS Organisation</th>
                  <th class="col-lg-6 thead">TYPE</th>
               </tr>
            </thead>
            <tbody>            
            </tbody>
         </table>         
      </div>
   </div>
</div>
<script src='map.js'></script>
</html>

This page simple loads Mapbox GL Javascript libraries and some styles. It also loads another Javascript file (at the bottom) which will open the Websocket and update the map.
Let’s create this Javascript file:

vi map.js

and insert the following code into the file:

// Set the IP to your webserver IP
const WEBSOCKET_SERVER = 'ws://<YOUR_WEBSERVERIP:8081>';
// Set your mapboxGL AccessToken
const MAPBOX_TOKEN = 'YOUR_ACCESS_TOKEN';

// Remove points from map after x-seconds
var displayTime = 300;

// Set some defaults for the map

var framesPerSecond = 15; 
var initialOpacity = 1;
var opacity = initialOpacity;
var initialRadius = 3;
var radius = initialRadius;
var maxRadius = 15;
let points = new Map();
var timers = [];

//Set your accessToken here
mapboxgl.accessToken = MAPBOX_TOKEN;

//Create new mapboxGl Map. Set your used style
var map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/leaseweb/ckkiepmg40ds717ry6l0htwag',
    center: [0, 0],
    zoom: 1.75
});

// Create a popup, but don't add it to the map yet.
var popup = new mapboxgl.Popup({
   closeButton: false,
   closeOnClick: false
});

// Once the map is loaded, we open the Websockets
map.on('load', function () {   
   openWebSockets(map); 
});

function openWebSockets(map) { 
   if ("WebSocket" in window) {
      // Let us open a web socket
      var ws = new WebSocket( WEBSOCKET_SERVER); 

      ws.onopen = function() {
         // Web Socket is connected, send data using send()         
         console.log("WS Open...");
      };
   
      ws.onmessage = function (event) { 
         var received_msg = JSON.parse(event.data);             
         addPoint(received_msg);            
      };

      ws.onerror = function(error) {
         console.log('Websocket error: ');
         console.log(error);
      }

      ws.onclose = function() { 
         // websocket is closed.
         console.log("Connection is closed..."); 
      };

   } else {
      // The browser doesn't support WebSocket
      alert("WebSocket NOT supported by your Browser!");
   }    
}

function animateMarker(timestamp, pointId) {
   if(!(pointId === undefined)) {
      if (points.has(pointId)) {         
        timers[pointId] = setTimeout(function() {
            requestAnimationFrame(function(timestamp) {
              animateMarker(timestamp, pointId);
            });
            

            radius = points.get(pointId)[0];
            opacity = points.get(pointId)[1];
            
            radius += (maxRadius - radius) / framesPerSecond;            
            opacity -= ( .9 / framesPerSecond );
            if (opacity < 0) {
               opacity = 0;
            }

            map.setPaintProperty('point-'+pointId, 'circle-radius', radius);
            map.setPaintProperty('point-'+pointId, 'circle-opacity', opacity);
         
            if (opacity <= 0) {
                radius = initialRadius;
                opacity = initialOpacity;
            } 
            points.set(pointId,[radius, opacity ]);        
        }, 1000 / framesPerSecond);
     } else {
      //The point is removed, we don't do anything at this moment
     }
  }
}

function addPoint(msg) {
   geo = JSON.parse(msg.geoip);     
   var ip = geo.ip;
   //Create a geohash based on the lat/lon of the IP. We used factor 7 to prevent overlapping point animations
   var geohash = encodeGeoHash(geo.latitude, geo.longitude, 7);
   //Get the AS Organisation name (or unknown)
   var ASORG = (geo.as_org === undefined ? 'Unknown' : geo.as_org);

   //Remove the flashrow style from last added row
   var flashrows = document.getElementById("tickettable").getElementsByClassName('flashrow');
   while (flashrows[0]) {
      flashrows[0].classList.remove('flashrow');
   }
   
   //Get table to add the newly added point information
   var tbody = document.getElementById("tickettable").getElementsByTagName('tbody')[0];
   
   tbody.insertRow().innerHTML = '<tr><td class="flashrow">' + new Date().toLocaleTimeString() + '</td>' +
         '<td class="flashrow">' + geo.country_name + '</td>' +
         '<td class="flashrow">' + ASORG + '</td>' +
         '<td class="flashrow">' + msg.protocol.toUpperCase() + ' Attack on port ' + msg.dest_port +'</td>' +    
         '</tr>';
   
   //If we have more than 5 items in the list, remove the first one
   if (tbody.rows.length > 5) {
      tbody.deleteRow(0);
   }

   //Add the point to the map if it is not already on the map
   if (!(geohash === undefined)) {               
      if (!(points.has(geohash))) {
         
         //Add the point to hash to keep of all active points and prevent duplicate points.
         points.set(geohash, [initialRadius, initialOpacity ]);               

         //Set a timer to remove the poinrt after 5minutes
         setTimeout(function() { removePoint(geohash) }, displayTime * 1000);            

         map.addSource('points-'+geohash, {
           "type": "geojson",
           "data": {
               "type": "Feature",
               "geometry": {
                  "type": "Point",
                  "coordinates": [ geo.longitude, geo.latitude]
               },
               "properties": {
                  "description": "<strong>" + ASORG + " (AS " + geo.asn +")</strong><p>IP: " + ip + "<BR>City: " + (geo.city_name === undefined ? 'Unknown' : geo.city_name) + 
                     "<BR>Region: " + (geo.region_name === undefined ? 'Unknown' : geo.region_name) + "<BR>Country: " + (geo.country_name === undefined ? 'Unknown' : geo.country_name) + "</P>"
               }
           }
         });
         
         map.addLayer({
           "id": "point-"+geohash,
           "source": "points-"+geohash,
           "type": "circle",
           "paint": {
               "circle-radius": initialRadius,
               "circle-radius-transition": {duration: 0},
               "circle-opacity-transition": {duration: 0},
               "circle-color": "#dd7cbf"
           }
         });  

         map.on('mouseenter', 'point-'+geohash, function (e) {
            // Change the cursor style as a UI indicator.
            map.getCanvas().style.cursor = 'pointer';
             
            var coordinates = e.features[0].geometry.coordinates.slice();
            var description = e.features[0].properties.description;
             
            // Ensure that if the map is zoomed out such that multiple
            // copies of the feature are visible, the popup appears
            // over the copy being pointed to.
            while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
               coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
            }
             
            // Populate the popup and set its coordinates
            // based on the feature found.
            popup.setLngLat(coordinates).setHTML(description).addTo(map);
         });
             
         map.on('mouseleave', 'point-'+geohash, function () {
            map.getCanvas().style.cursor = '';
            popup.remove();
         });

         //Animate the added point.
         animateMarker(0, geohash);               
      }
   }
}

function removePoint(ip) {
   clearTimeout(timers[ip]);
   points.delete(ip);
   map.removeLayer('point-'+ip);
   map.removeSource('points-'+ip);         
}

function encodeGeoHash(latitude, longitude, precision) {
  var BITS = [16, 8, 4, 2, 1];

  var BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";
  var isEven = 1;
  var lat = [-90.0, 90.0];
  var lon = [-180.0, 180.0];
  var bit = 0;
  var ch = 0;
  precision = precision || 12;

  var geohash = "";
  while (geohash.length < precision) {
    var mid;
    if (isEven) {
      mid = (lon[0] + lon[1]) / 2;
      if (longitude > mid) {
        ch |= BITS[bit];
        lon[0] = mid;
      } else {
        lon[1] = mid;
      }
    } else {
      mid = (lat[0] + lat[1]) / 2;
      if (latitude > mid) {
        ch |= BITS[bit];
        lat[0] = mid;
      } else {
        lat[1] = mid;
      }
    }

    isEven = !isEven;
    if (bit < 4) {
      bit++;
    } else {
      geohash += BASE32[ch];
      bit = 0;
      ch = 0;
    }
  }
  return geohash;
}; 

You will need to make two small modification at the top of the file:

  • Set the IP of your webserver
  • Set your Mapbox access token

Once you have done this, open the page in your browser and a map should appear. NOTE: nothing will happen at this moment 🙂

Configure Logstash

Now we are all set for the last part: configuring Logstash to also forward (some) logs to our Node-application.
On your T-Pot server, we need to get the Logstash configuration as described on the T-Pot Wiki:

docker exec -it logstash ash
cd /etc/logstash/conf.d/
cp logstash.conf /data/elk/logstash.conf
exit

Open the Logstash configuration and add the following lines to the output section, after the Elasticsearch output:

if [type] == "ConPot" and [dest_port] and [event_type] == "NEW_CONNECTION" and [src_ip] != "${MY_INTIP}" {
   http {
     url => "http://${HTTP_LOGIP}"
     http_method => "post"
     mapping => {
         "type" => "%{type}"
         "protocol" => "Elastic"
         "source" => "%{src_ip}"
         "dest_port" => "%{dest_port}"
         "geoip" => "%{geoip}"
     }
   }
}
if [type] == "Ciscoasa" and [src_ip] != "${MY_INTIP}" {
   http {
     url => "http://${HTTP_LOGIP}"
     http_method => "post"
     mapping => {
          "type" => "%{type}"
         "protocol" => "Ciscoasa"
         "source" => "%{src_ip}"
         "geoip" => "%{geoip}"
     }
   }
}
if [type] == "Mailoney" and [dest_port] and [src_ip] != "${MY_INTIP}" {
   http {
     url => "http://${HTTP_LOGIP}"
     http_method => "post"
     mapping => {
          "type" => "%{type}"
         "protocol" => "Mail"
         "source" => "%{src_ip}"
         "dest_port" => "%{dest_port}"
         "geoip" => "%{geoip}"
     }
   }
}
if [type] == "ElasticPot" and [dest_port] and [src_ip] != "${MY_INTIP}" {
   http {
     url => "http://${HTTP_LOGIP}"
     http_method => "post"
     mapping => {
          "type" => "%{type}"
         "protocol" => "Elastic"
         "source" => "%{src_ip}"
         "dest_port" => "%{dest_port}"
         "geoip" => "%{geoip}"
     }
   }
}
if [type] == "Adbhoney" and [dest_port] and [src_ip] != "${MY_INTIP}" {
   http {
     url => "http://${HTTP_LOGIP}"
     http_method => "post"
     mapping => {
          "type" => "%{type}"
         "protocol" => "ADB"
         "source" => "%{src_ip}"
         "dest_port" => "%{dest_port}"
         "geoip" => "%{geoip}"
     }
   }
}
if [type] == "Dionaea" and [dest_port] and [src_ip] != "${MY_INTIP}" {
   http {
     url => "http://${HTTP_LOGIP}"
     http_method => "post"
     mapping => {
          "type" => "%{type}"
         "protocol" => "%{[connection][transport]}"
         "service" => "%{[connection][protocol]}"
         "source" => "%{src_ip}"
         "dest_port" => "%{dest_port}"
         "geoip" => "%{geoip}"
     }
   }
}
if [type] == "Fatt" and [protocol] != "ssh" and [src_ip] != "${MY_INTIP}" {
   http {
     url => "http://${HTTP_LOGIP}"
     http_method => "post"
     mapping => {
          "type" => "%{type}"
         "protocol" => "%{protocol}"
         "source" => "%{src_ip}"
         "dest_port" => "%{dest_port}"
         "geoip" => "%{geoip}"
     }
   }
}
if [type] == "Cowrie" and [dest_port] and [protocol] and [src_ip] != "${MY_INTIP}" {
   http {
     url => "http://${HTTP_LOGIP}"
     http_method => "post"
     mapping => {
          "type" => "%{type}"
         "protocol" => "%{protocol}"
         "source" => "%{src_ip}"
         "dest_port" => "%{dest_port}"
         "geoip" => "%{geoip}"
     }
   }
}
if [type] == "HoneyTrap" and [dest_port] and [src_ip] != "${MY_INTIP}" {
   http {
     url => "http://${HTTP_LOGIP}"
     http_method => "post"
     mapping => {
          "type" => "%{type}"
         "protocol" => "%{[attack_connection][protocol]}"
         "source" => "%{src_ip}"
         "dest_port" => "%{dest_port}"
         "geoip" => "%{geoip}"
     }
   }
}

We need to add a new variable to the docker environment:

vi /opt/tpot/etc/compose/elk_environment

Then add the following line to the file, to exclude the T-Pot server in the Logstash messages:

HTTP_LOGIP=<YOUR_TPOTSERVER_IP>:8080

Now add a new docker volume for the Logstash service:

vi /opt/tpot/etc/tpot.yml

Go to the Logstash service section and add the following line:

- /data/elk/logstash.conf:/etc/logstash/conf.d/logstash.conf

Now we are all set and it’s time to restart your T-port service:

systemctl start tpot

That’s It!

Now take a look at your map. If there are attacks on your server, they should appear on the map and in the listing below.

You can trigger an event by, for example, opening a regular SSH session to your T-Pot server:

ssh <TPOT_SERVER>

Simply close the connection once it is established, and your location should appear on the map.

Daily figures

Almost immediately when you start running a honeypot you will see attacks. Within one day, I saw over 200.000 attacks, mostly on common ports like HTTP(S), SSH and SMTP. You can use this data to make your environments more safe, or just use them for some fun projects.

Some notes
As this was a quick project in limited time, there are definitely some optimisations or better coding that can take place 🙂 The Javascript will give a few errors after some time, probably due to points being removed from the map while a call to update the same point happens at the same time.
In addition, some points on the map will suddenly run on steroids, animating at higher frames than they did initially. The Node.js application was made quick and dirty but is suitable for this demo.

Technical Careers at Leaseweb

We are searching for the next generation of engineers and developers to help us build the infrastructure to automate our global hosting services! If you are interested in finding out more, check out our Careers at Leaseweb.

Share

Set up Private DNS-over-TLS/HTTPS

Domain Name System (DNS) is a crucial part of Internet infrastructure. It is responsible for translating a human-readable, memorizable domain (like leaseweb.com) into a numeric IP address (such as 89.255.251.130).

In order to translate a domain into an IP address, your device sends a DNS request to a special DNS server called a resolver (which is most likely managed by your Internet provider). The DNS requests are sent in plain text so anyone who has access to your traffic stream can see which domains you visit.

There are two recent Internet standards that have been designed to solve the DNS privacy issue:

  • DNS over TLS (DoT):
  • DNS over HTTPS (DoH)

Both of them provide secure and encrypted connections to a DNS server.

DoT/DoH feature compatibility matrix:

Firefox Chrome Android 9+ iOS 14+
DoT
DoH

iOS 14 will be released later this year.

In this article, we will setup a private DoH and DoT recursor using pihole in a docker container, and dnsdist as a DNS frontend with Letsencrypt SSL certificates. As a bonus, our DNS server will block tracking and malware while resolving domains for us.

Installation

In this example we use Ubuntu 20.04 with docker and docker-compose installed, but you can choose your favorite distro (you might need to adapt a bit).

You may also need to disable systemd-resolved because it occupies port 53 of the server:

# Check which DNS resolvers your server is using:
systemd-resolve --status
# look for "DNS servers" field in output

# Stop systemd-resolved
systemctl stop systemd-resolved

# Then mask it to prevent from further starting
systemctl mask systemd-resolved

# Delete the symlink systemd-resolved used to manage
rm /etc/resolv.conf

# Create /etc/resolv.conf as a regular file with nameservers you've been using:
cat <<EOF > /etc/resolv.conf
nameserver <ip of the first DNS resolver>
nameserver <ip of the second DNS resolver>
EOF

Install dnsdist and certbot (for letsencrypt certificates):

# Install dnsdist repo
echo "deb [arch=amd64] http://repo.powerdns.com/ubuntu focal-dnsdist-15 main" > /etc/apt/sources.list.d/pdns.list
cat <<EOF > /etc/apt/preferences.d/dnsdist
Package: dnsdist*
Pin: origin repo.powerdns.com
Pin-Priority: 600
EOF
curl https://repo.powerdns.com/FD380FBB-pub.asc | apt-key add -

apt update
apt install dnsdist certbot

Pihole

Now we create our docker-compose project:

mkdir ~/pihole
touch ~/pihole/docker-compose.yml

The contents of docker-compose.yml file:

version: '3'
services:
  pihole:
    container_name: pihole
    image: 'pihole/pihole:latest'
    ports:
    # The DNS server will listen on localhost only, the ports 5300 tcp/udp.
    # So the queries from the Internet won't be able to reach pihole directly.
    # The admin web interface, however, will be reachable from the Internet.
      - '127.0.1.53:5300:53/tcp'
      - '127.0.1.53:5300:53/udp'
      - '8081:80/tcp'
    environment:
      TZ: Europe/Amsterdam
      VIRTUAL_HOST: dns.example.com # domain name we'll use for our DNS server
      WEBPASSWORD: super_secret # Pihole admin password
    volumes:
      - './etc-pihole/:/etc/pihole/'
      - './etc-dnsmasq.d/:/etc/dnsmasq.d/'
    restart: unless-stopped

Start the container:

docker-compose up -d

After the container is fully started (it may take several minutes) check that it is able to resolve domain names:

dig +short @127.0.1.53 -p5300 one.one.one.one
# Excpected output
# 1.0.0.1
# 1.1.1.1

Letsencrypt Configuration

Issue the certificate for our dns.example.com domain:

certbot certonly

Follow the instructions on the screen (i.e. select the proper authentication method suitable for you, and fill the domain name).

After the certificate is issued it can be found by the following paths:

  • /etc/letsencrypt/live/dns.example.com/fullchain.pem – certificate chain
  • /etc/letsencrypt/live/dns.example.com/privkey.pem – private key

By default only the root user can read certificates and keys. Dnsdist, however, is running as user and group _dnsdist, so permissions need to be adjusted:

chgrp _dnsdist /etc/letsencrypt/live/dns.example.com/{fullchain.pem,privkey.pem}
chmod g+r /etc/letsencrypt/live/dns.example.com/{fullchain.pem,privkey.pem}

# We should also make archive and live directories readable.
# That will not expose the keys since the private key isn't world-readable
chmod 755 /etc/letsencrypt/{live,archive}

The certificates are periodically renewed by Certbot, so dnsdist should be restarted after that happens since it is not able to detect the new certificate. In order to do so, we put a so-called deploy script into /etc/letsencrypt/renewal-hooks/deploy directory:

mkdir -p /etc/letsencrypt/renewal-hooks/deploy
cat <<EOF > /etc/letsencrypt/renewal-hooks/deploy/restart-dnsdist.sh
#!/bin/sh
systemctl restart dnsdist
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/restart-dnsdist.sh

Dnsdist Configuration

Create dnsdist configuration file /etc/dnsdist/dnsdist.conf with the following content:

addACL('0.0.0.0/0')

-- path for certs and listen address for DoT ipv4,
-- by default listens on port 853.
-- Set X(int) for tcp fast open queue size.
addTLSLocal("0.0.0.0", "/etc/letsencrypt/live/dns.example.com/fullchain.pem", "/etc/letsencrypt/live/dns.example.com/privkey.pem", { doTCP=true, reusePort=true, tcpFastOpenSize=64 })

-- path for certs and listen address for DoH ipv4,
-- by default listens on port 443.
-- Set X(int) for tcp fast open queue size.
-- 
-- In this example we listen directly on port 443. However, since the DoH queries are simple HTTPS requests, the server can be hidden behind Nginx or Haproxy.
addDOHLocal("0.0.0.0", "/etc/letsencrypt/live/dns.example.com/fullchain.pem", "/etc/letsencrypt/live/dns.example.com/privkey.pem", "/dns-query", { doTCP=true, reusePort=true, tcpFastOpenSize=64 })

-- set X(int) number of queries to be allowed per second from a IP
addAction(MaxQPSIPRule(50), DropAction())

--  drop ANY queries sent over udp
addAction(AndRule({QTypeRule(DNSQType.ANY), TCPRule(false)}), DropAction())

-- set X number of entries to be in dnsdist cache by default
-- memory will be preallocated based on the X number
pc = newPacketCache(10000, {maxTTL=86400})
getPool(""):setCache(pc)

-- server policy to choose the downstream servers for recursion
setServerPolicy(leastOutstanding)

-- Here we define our backend, the pihole dns server
newServer({address="127.0.1.53:5300", name="127.0.1.53:5300"})

setMaxTCPConnectionsPerClient(1000)    -- set X(int) for number of tcp connections from a single client. Useful for rate limiting the concurrent connections.
setMaxTCPQueriesPerConnection(100)    -- set X(int) , similiar to addAction(MaxQPSIPRule(X), DropAction())

Checking if DoH and DoT Works

Check if DoH works using curl with doh-url flag:

curl --doh-url https://dns.example.com/dns-query https://leaseweb.com/

Check if DoT works using kdig program from the knot-dnsutils package:

apt install knot-dnsutils

kdig -d @dns.example.com +tls-ca leaseweb.com

Setting up Private DNS on Android

Currently only Android 9+ natively supports encrypted DNS queries by using DNS-over-TLS technology.

In order to use it go to: Settings -> Connections -> More connection settings -> Private DNS -> Private DNS provider hostname -> dns.example.com

Conclusion

In this article we’ve set up our own DNS resolving server with the following features:

  • Automatic TLS certificates using Letsencrypt.
  • Supports both modern encrypted protocols: DNS over TLS, and DNS over HTTPS.
  • Implements rate-limit of incoming queries to prevent abuse.
  • Automatically updated blacklist of malware, ad, and tracking domains.
  • Easily upgradeable by simply pulling a new version of Docker image.
Share

Simple web application firewall using .htaccess

Apache provides a simple web application firewall by a allowing for a “.htaccess” file with certain rules in it. This is a file you put in your document root and may restrict or allow access from certain specific IP addresses. NB: These commands may also be put directly in the virtual host configuration file in “/etc/apache2/sites-available/”.

Use Case #1: Test environment

Sometimes you may want to lock down a site and only grant access from a limited set of IP addresses. The following example (for Apache 2.2) only allows access from the IP address “127.0.0.1” and blocks any other request:

Order Allow,Deny
Deny from all
Allow from 127.0.0.1

In Apache 2.4 the syntax has slightly changed:

Require all denied
Require ip 127.0.0.1

You can find your IP address on: whatismyipaddress.com

Use Case #2: Application level firewall

If you run a production server and somebody is abusing your system with a lot of requests then you may want to block a specific IP address. The following example (for Apache 2.2) only blocks access from the IP address “172.28.255.2” and allows any other request:

Order deny,allow
Allow from all
Deny from 172.28.255.2

In Apache 2.4 the syntax has slightly changed:

Require all granted
Require not ip 172.28.255.2

If you want to block an entire range you may also specify CIDR notation:

Require all granted
Require not ip 10.0.0.0/8
Require not ip 172.16.0.0/12
Require not ip 192.168.0.0/16

NB: Not only IPv4, but also IPv6 addresses may be used.

Share

Static code analysis for PHP templates

Templating is cool. Everybody is using Twig today. Other popular choices are: Smarty, Mustache and Latte. You may also want to read what Fabien Potencier has written about PHP templates languages. It makes sense.

Still I can think of two reasons why we don’t want a templating language and we rather use PHP itself for templating. First reason: PHP templating is easier to learn than a PHP templating language.  Second reason: it executes faster.

PHP templating languages improve security

I tried to understand what the primary reason is that people are using a templating language. It seems to be ease of use, while keeping the application secure. The following example shows how easily you can write unsafe code:

Hello <?php echo $POST['name']; ?>!

It would only be safe to print a POST variable when using:

<?php echo htmlspecialchars($POST['name'],ENT_QUOTES,'UTF-8'); ?>

A templating language typically allows you to write something like:

Hello {{ name }}!

I agree that security is improved by using a templating language. The templating language escapes the output strings in order to prevent XSS vulnerabilities. But still I wonder: Can’t we get the same security benefits when we use native PHP for templating?

Helper function

As you have seen the PHP way of escaping is rather long. Fortunately, you can easily define a function that allows an alternative syntax, for instance:

Hello <?php e($POST['name']); ?>!

Yup, that is the “e” for “echo” :-). Now we can report all native (unescaped) echo function calls as being potentially unsafe. This can be achieved by doing static code analysis. While analyzing the code the analyzer could complain like this:

PHP Warning:  In "template.php" you should not use "echo" on line 1. Error raised  in analyzer.php on line 11

This could be limited to debug mode as static code analysis actually takes some time and may harm the performance of your application.

Static code analysis in PHP

I worked out the idea of secure PHP templating using static code analysis. In development (debug) mode it should warn the programmer when he uses a potentially non-safe construct.

The following analyzer script shows how this works:

<?php
$tokens    = array('T_ECHO', 'T_PRINT', 'T_EXIT', 'T_STRING', 'T_EVAL', 'T_OPEN_TAG_WITH_ECHO');
$functions = array('echo', 'print', 'die', 'exit', 'var_dump', 'eval', '<?=');
$filename  = 'template.php';

$all_tokens = token_get_all(file_get_contents($filename));
foreach ($all_tokens as $token) {
  if (is_array($token)) {
    if (in_array(token_name($token[0]),$tokens)) {
      if (in_array($token[1],$functions)) {
        trigger_error('In "'.$filename.'" you should not use "'.htmlentities($token[1]).'" on line '.$token[2].'. Error raised ', E_USER_WARNING);
      }
    }
  }
}

It will analyze the “template.php” file and report potentially insecure or erroneous language constructs.

This form of templating and static code analysis is fully implemented in the MindaPHP framework that you can find on my Github account. You can find the source code of the PHP static code analyzer class here.

Share

Linux commands “astu” and “astsu” in Mr. Robot

mr_robot

People told me that the hacking in “Mr Robot” was pretty accurate. Mr Robot is a TV series about a hacker named “Elliot”. I had to see it, but until now I was lacking the time. Last Sunday was a prefect lazy day and I took the time to finally watch it. I must admit it was pretty amazing to see the inside of a data-center and all the geeky Linux command line screens in a such a popular TV series.

Linux commands “astu” and “astsu”

When Elliot (the main character) is hacking he uses two Linux commands frequently: “astu” and “astsu”. The commands play a critical role in the series. I did not know what they did, so I wondered:

Did anyone figure out what the “astsu” command is supposed to be? Did he just type random characters or what? The other commands I noticed were all real.

On which some other user on the Cyberpunk and Science Fiction board replied:

It seems to be used like sudo (or ssh) would so I guess the idea was that the company that he works for has its own way to allow safe privilege escalation and this is the tool they install astsu = AllSafe Toolkit Super User (allsafe security being the company name).

You should read the Mr. Robot Episode 1 Analysis for more detail on the actual commands used during the hacking.

Things I liked

Some things were really spot on in the series and I liked them a lot:

  1. The correctness, detail and accuracy of the hacking that goes on.
  2. Elliot has some social challenges and thus feels like an outsider.
  3. Elliot is unhappy and this is his strength, as he has nothing to lose.

But not everything was good, there was also some stuff that bothered me in the series.

Things that bothered me

Here is a list of the most annoying things in the series:

  1. Elliot uses a smart-phone and he never switches SIM or phone.
  2. Elliot’s schizophrenia is making his conspiracy thinking less genuine.
  3. Computers and downers do not match. Caffeine on the other hand…

I feel the makers of Mr. Robot should have thought these things over better. Nevertheless they made an enjoyable TV series. Recommended!

 

Share