Friday, May 5, 2017

BACnet - Bro - Spicy Part 4

So, in BACnet - Bro -Spicy Part 3, I went through building Docker images and starting containers running bacnet, bro, and spicy, and php and apache. These two containers share a volume on the host VM and use the host VM's network to access outside the container itself.

This last post will describe the HMI and it's php script that takes the output from the bro log and uses it to determine which animated gif to display.  The one with the ctf flag is displayed for those IPs in the log which means they successfully crafted and sent a packet with the correct BACnet write-property command.

The Plan:

  1. Set up some virtual machines in my VMware environment to experiment. 
    1. Install the BACnet-stack on two: server and client. (see BACnet - Bro - Spicy Part 1)
    2. Install Docker and the rsmmr-hilti docker image on the bacnet server machine. (see BACnet - Bro - Spicy Part 2)
  2. Write a Bro script that uses the events created by the BACnet parser to log the source IP address from packets with the correct BACnet command. (see BACnet - Bro - Spicy Part 2)
  3. Install the php-apache docker image on the bacnet server machine and run it with the same shared volume as the rsmmr-hilti container where the log file is saved. (see BACnet - Bro - Spicy Part 3)
  4. Write a PHP webpage/script that shows the flag only to the IPs in the log file and a different page to all other IPs


4) Write a PHP webpage/script that shows the flag only to the IPs in the log file and a different page to all other IPs


  1. The index.php script is very basic and simply displays the normal.gif file unless the visiting IP address is in the bro log file in the shared volume of the Docker container. If the visiting IP is on the list, the alert.gif file is displayed, along with the flag as text over the top of the image.

<?php
$IPAddrsFile = "/tmp/bacnetCTF.log";
$IPAddrs = file($IPAddrsFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$normal = "images/normal.gif";
$alert = "images/alert.gif";
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>BACnet Monitoring</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    </head>
    <style>       
        h2 {
            position: fixed;
            left: 450px;
            top: 350px;
            color: orange;
        }
        body {
            background-color: #FFFFFF;
            background-repeat: no-repeat;
            background-size: 900px;
            <?php
                $visitor = (string)$_SERVER['REMOTE_ADDR'];
                    if (in_array($visitor, $IPAddrs)) {
                        echo "background-image: url(\"" . $alert . "\");\n";
                        echo "</style>\n";
                        echo "<h2>flag{PatYourselfOnTheBacNet}.</h2>";
                    } else {
                        echo "background-image: url(\"" . $normal . "\");\n";
                        echo "}\n</style>\n";
                    }
            ?>
    </body>
</html>

The normal.gif looks like this:


The alert.gif looks like this and the php script would write the flag text over the black area in the bottom right corner:


BACnet - Bro - Spicy Part 3

So, in BACnet - Bro -Spicy Part 2, I went through setting up a VM with Docker and the rsmmr-hilti docker image that had Bro and Spicy installed. I then described the modifications to the bacnet.spicy and bacnet.evt files that would allow my bacnetctf.bro script to run and detect when a specific bacnet command packet was seen.

The Plan:

  1. Set up some virtual machines in my VMware environment to experiment. 
    1. Install the BACnet-stack on two: server and client. (see BACnet - Bro - Spicy Part 1)
    2. Install Docker and the rsmmr-hilti docker image on the bacnet server machine. (see BACnet - Bro - Spicy Part 2)
  2. Write a Bro script that uses the events created by the BACnet parser to log the source IP address from packets with the correct BACnet command.
  3. Install the php-apache docker image on the bacnet server machine and run it with the same shared volume as the rsmmr-hilti container where the log file is saved.
  4. Write a PHP webpage/script that shows the flag only to the IPs in the log file and a different page to all other IPs

3) Install the php-apache docker image on the third machine and run it with the same shared volume as the rsmmr-hilti container where the log file is saved.

Now that we have the spicy, evt, and bro files working in the rsmmr-hilti docker container, we need to build our own docker image that will incorporate our new files every time we run a new container. Then we can set up our web server using the php-apache docker image and server our the simulated Human Machine Interface (HMI) that will indicate when a successful write-property packet is detected.  
  1. In order to build a new image, I needed to create a DockerFile that defined how to get from the base image of rsmmr/hilti to my bacnet-hilti image.  This is basically a file that has its own syntax where you can do things like copy a local file into the container as it starts up, or run specific commands. This is the DockerFile for by bacnet-hilti image:
  2. FROM rsmmr/hilti
    MAINTAINER jenngates2004@gmail.com 
    COPY ./node.cfg /usr/local/bro/etc/node.cfg
    COPY ./bacnet.evt /opt/hilti/bro/spicy/bacnet.evt
    COPY ./bacnet.spicy /opt/hilti/bro/spicy/bacnet.spicy
    COPY ./properties.bro  /opt/hilti/bro/tests/spicy/bacnet/properties.bro
    COPY ./myBACnet.bro /opt/hilti/bro/tests/spicy/bacnet/myBACnet.bro
    COPY ./bacnetctf.bro /opt/hilti/bro/tests/spicy/bacnet/bacnetctf.bro
    COPY ./*.pcap /opt/hilti/bro/tests/Traces/bacnet/
    COPY ./CriticalRoom55-1.cap    /opt/hilti/bro/tests/Traces/bacnet/CriticalRoom55-1.cap
    RUN mkdir /opt/bacnet-stack-0.8.3/
    COPY ./bacnet-stack /opt/bacnet-stack-0.8.3/
    RUN apt-get update
    RUN cd /opt/bacnet-stack-0.8.3 &&  make clean all
    RUN mkdir /root/brologs

  3. Next I use this file to create the bacnet-hilti Docker image.
    1. Made a new dir 
      1. mkdir ~/bacnet-hilti
      2. cd bacnet-hilti/
    2. Copied .bro/.evt/.spicy/DockerFile files to it
    3. Build the new image with the files inserted
      1. NOTE: The space and then period at the end of the command are important!
      2.  sudo docker build -t bacnet-hilti .
  4. Now I run a container named bacnet-hilti1 in interactive mode (-ti) using the host VM's networking (--network=host) maps port 47808 on the host VM to forward packets to port 47808 in the bacnet-hilti1 container (-p 47808:47808) since bacnet listens on that port inside my container. I add a volume (--volume) to the container that maps a directory on the host VM (/home/bacnetchall/sharedvolume) to the /root/brologs directory in the container and allows read/write access (:rw) and tells the container it is sharing that volume with other containers (, z). The following command should be entered as one line.
    1. sudo docker run --network=host --name bacnet-hilti1 -ti -p 47808:47808  --volume=/home/bacnetchall/sharedvolume:/root/brologs:rw,z bacnet-hilti
  5. This starts the container and puts me at the root command prompt inside the container. Since I'm just going to run a specific Bro command so that it runs my bro script, I cd into the proper folder for the log file to be written. This is the folder the web server of the HMI will be monitoring.
    1. cd brologs   
    2. I start Bro listening on interface ens160 (the Docker image doesn't have the interfaces named eth0, eth1, etc). I tell Bro to use the bacnet.evt file which the new bacnet-hilti image made sure the running container has my modified version. I run the traffic through my bacnetctf.bro script, which, again, the container has the new version.  The -C tells it to ignore checksums and the & runs it in the background
      1. bro -i ens160 bacnet.evt /opt/hilti/bro/tests/spicy/bacnet/bacnetctf.bro -C & 
        1. NOTE!! wait for "listening on ens160". It can take a long time.
      2. Once bro is running and listening for traffic to parse with the bacnetctf.bro script, I start the bacnet server installed in Step 1 of this blog series. It has the device ID of 422 and runs in the background.
        1. /opt/bacnet-stack-0.8.3/demo/server/bacserv 422 &
      3. To break out of the running container and back to the host VM without stopping bro, bacnet, or the container:
        1. Ctrl-p Ctrl-q 
  6. From the same host VM I'm also going to run a Docker container that will run a web server with the HMI webpage. I'll talk more about the webpage and php script in the next blog post but here I'm just going to get the server piece working.  Again, there is a Docker container that does 98% of what I want already called php:7.0-apache, I just need to build it to copy the webpage files into the container's /var/www/html/ directory when it runs.
    1. I make a new dir called php-apache and cd into it
      1. mkdir php-apache
      2. cd php-apache
    2. I create a new file here named DockerFile and add the following lines inside it:
      1. FROM php:7.0-apache
      2. COPY ./bacnet_monitor/ /var/www/html/
    3. Here I make another new dir for the website files called bacnet_monitor.
      1. mkdir bacnet_monitor
    4. I copy over the website files into the bacnet_monitor directory
    5. I build a new Docker image file called php-apache
      1. sudo docker build -t php-apache .
      2. NOTE: remember the space and period at the end
    6. Now I run a new container of the new php-apache image file using the host VMs networking (--network=host) and name it php-apache1. It maps the host's port 80 with the container's port 80 and adds the same volume as the bacnet-hilti container, but with read-only access (:ro). This container doesn't require me to run any commands in it interactively, so we run it detached (-d).
      1. sudo docker run --network=host --name php-apache1 -d -p 80:80 --volume=/home/bacnetchall/sharedvolume/:/tmp/:ro,z php-apache


Part 4 will finish up with the PHP script for the HMI and wrap up this series.

BACnet - Bro - Spicy Part 2

So in BACnet - Bro - Spicy Part 1 I explained that I've been working with the BACnet protocol developing a CTF challenge.  I'm using Bro with a Spicy parser and a logging script to detect when a specific BACnet command is send in a packet. When triggered, the sending IP Address is recorded in the log file. That file is then used to restrict access to a webpage that contains the flag. Probably not the best plan, but it got me working on something and thinking through a problem.


The Plan:

  1. Set up some virtual machines in my VMware environment to experiment. 
    1. Install the BACnet-stack on two: server and client. (see BACnet - Bro - Spicy Part 1)
    2. Install Docker and the rsmmr-hilti docker image on the bacnet server machine.
  2. Write a Bro script that uses the events created by the BACnet parser to log the source IP address from packets with the correct BACnet command.
  3. Install the php-apache docker image on the bacnet server machine and run it with the same shared volume as the rsmmr-hilti container where the log file is saved.
  4. Write a PHP webpage/script that shows the flag only to the IPs in the log file and a different page to all other IPs.

1.2) Install Docker and the rsmmr-hilti docker image on a third machine.

Docker was very easy and I was up and running very quickly. The rsmmr-hilti image already has Bro and Spicy installed and working. It also has BACnet parser Spicy scripts included and some test bro scripts to get an idea of how they work.


  1. I started with a fresh install of Ubuntu 16.04 LTS and OpenSSH server installed and really just followed the steps at the Docker Community Edition for Ubuntu site. 
    • sudo apt-get -y install apt-transport-https ca-certificates curl
    • curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
    • sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
    • sudo apt-get update
    • sudo apt-get -y install docker-ce
    • sudo docker run hello-world
  2. Next I installed the rsmmr-hilti image from https://hub.docker.com/r/rsmmr/hilti/ 
      • mkdir ~/hilti-docker
      • cd hilti-docker
      • sudo docker pull rsmmr/hilti
    1. The hilti site has instructions to run the docker image and test bro and spicy.
      • Start the container in interactive mode with a pseudo-TTY
        • sudo docker run -i -t "rsmmr/hilti"  
      • Verify the BRO_PLUGIN_PATH is /opt/hilti/build/bro/
        • env
      • Use the spicy-driver tool to test executing a spicy parser script:
        • spicy-driver hello-world.spicy
      • Run bro with the verbose plugin check option. This returns info about what the provides.
        • bro -NN Bro::Hilti
      • Tests running the script with bro and the spicy plugin 
        • bro hello-world.spicy
      • Runs bro at the command line reading in the trace pcap file, using the .evt file that defines events and the the .bro file that describes the response to those events. In this case, it writes to standard out information about detected SSH banners.
        • bro -r ssh-single-conn.trace ./ssh-banner.bro ssh.evt
    2. From here, I started looking at what was available in the BACnet scripts.  I verified the bacnet.evt plugin was installed and working.
      • bro --NN bacnet.evt
      • grep for bacnet and you should see:
    Bro::Hilti - Dynamically compiled HILTI/Spicy functionality (*.spicy, *.evt, *.hlt) (dynamic, version 0.1)
    [Analyzer] spicy_BACnet (ANALYZER_SPICY_BACNET, enabled)
    [Event] bacnet_message
    [Event] bacnet_bvlc_result
    1. The bacnet .bro test script files are located in:
      • /opt/hilti/bro/tests/spicy/bacnet
    2. The bacnet trace files are located in:
      •  /opt/hilti/bro/Traces/bacnet
    3. The docker image comes with some pcap files capturing specific bacnet traffic and bro scripts to detect bacnet commands.  For example, the who-has.pcap captures who-has traffic and the apdu_who.bro script will detect it.  This is the command to test a .bro script is that uses a definition in the bacnet.evt plugin:
      • bro -r /opt/hilti/bro/tests/Traces/bacnet/who-has.pcap bacnet.evt /opt/hilti/bro/tests/spicy/bacnet/apdu_who.bro


    2) Write a Bro script that uses the events created using the BACnet parser to log the source IP address from packets with the correct BACnet command.

    Spicy (formerly BinPAC++) is a language used to write a protocol parser for Bro. Spicy files have the extension .spicy and they define how to parse a packet.  Bro uses .evt files to determine which parser is used to parse each kind of traffic and which events to generate based on that parsed traffic.  In my case, the bacnet.spicy and bacnet.evt files already existed within the rsmmr/hilti docker image, so I only needed to modify them to create an event based on the traffic I was watching for; specifically the write-property command packet.

    bacnet.spicy
    In the bacnet.spicy file, I needed to keep track of the segments. All segments of a segmented request must have the same invokeID. The invokeID should be incremented for each new BACnet-Confirmed-Request-PDU. So I modified the /opt/hilti/bro/spicy/bacnet.spicy file as follows:

    [...]
    type PDU_confirmedRequest = unit(head: bytes, len: uint16) {
      var len: uint16 = 4;
      var segmentation_status: bool = False;

      [...]

      switch ( self.confirmed_service_choice ) {
        BACnetConfirmedServiceChoice::confirmedEventNotification -> confirmedEventNotification: ConfirmedEventNotification_Request( len - self.len );
        BACnetConfirmedServiceChoice::readProperty -> readProperty: ReadProperty_Request( len - self.len, self.invokeID );
        BACnetConfirmedServiceChoice::writeProperty -> writeProperty: WriteProperty_Request( len - self.len, self.invokeID );

      [...]

      switch ( self.confirmed_service_choice ) {
        BACnetConfirmedServiceChoice::confirmedEventNotification -> confirmedEventNotification: ConfirmedEventNotification_Request( len - self.len );
        BACnetConfirmedServiceChoice::readProperty -> readProperty: ReadProperty_Request( len - self.len, self.invokeID );
        BACnetConfirmedServiceChoice::writeProperty -> writeProperty: WriteProperty_Request( len - self.len, self.invokeID );

    [...]

    type WriteProperty_Request = unit(len: uint16, invokeID: uint8) {
      # context-specific tags 0 - 4

      first_tag: BACnetTag;

    [...]

    I also needed to work around an issue with the property value I was looking for being part of an array, but the parser not fully working yet with this array. What I changed worked for my very scoped purpose, but really isn't the solution to the array issue overall.  However, I again modified the /opt/hilti/bro/spicy/bacnet.spicy file as follows:

    [...]

    type WriteProperty_Request = unit(len: uint16, invokeID: uint8) {
      # context-specific tags 0 - 4
      first_tag: BACnetTag;
      objectIdentifier: BACnetContextMessage(BACnetType::BACnetObjectIdentifier, 0, self.first_tag);
      propertyIdentifier_tag: BACnetTag; # tag 1
      propertyIdentifier: bytes &length=self.propertyIdentifier_tag.lvt &convert=BACnetPropertyIdentifier($$.to_uint(Spicy::ByteOrder::Network));
      propertyArrayIndex: BACnetContextMessage(BACnetType::UnsignedInteger, 2, self.propertyIdentifier_tag);
      propertyValue_tag: BACnetTag if ( self.propertyArrayIndex.tag.tag != 3 ); # check if previous tag was optional. This tag = 3
      #propertyValue: BACnetArray_Partial(3);
      propertyValue: BACnetMessage;

    [...]


    bacnet.evt
    I then needed to modify the bacnet.evt file to generate an event for the write property I was watching for. I had to add some logic to create different events for write property packets that were writing analog (realval) values and those writing digital (enumerated) values. Then I used those to generate and event when the property being written was an enumerated value of type 4 (Binary Output), instance 3, property identifier 85 (present_value). This was the packet I would be looking for. 

    I added the following lines to the end of  /opt/hilti/bro/spicy/bacnet.evt:

    [...]

    on BACnet::WriteProperty_Request if (self.propertyValue.tag.tpe == BACnetType::Real)
      -> event bacnet_writeproperty_request($conn, cast<uint64>(invokeID), self.objectIdentifier.oid.tpe, cast<uint64>(self.objectIdentifier.oid.data.instanceNumber), self.propertyIdentifier, self.propertyValue.realval);

    on BACnet::WriteProperty_Request if ((self.objectIdentifier.oid.tpe == 4) && (self.propertyValue.tag.tpe == BACnetType::Enumerated) && (self.objectIdentifier.oid.data.instanceNumber == 3) && (self.propertyIdentifier == 85))
      -> event bacnetctf_writeproperty_request($conn, cast<uint64>(invokeID), self.objectIdentifier.oid.tpe, cast<uint64>(self.objectIdentifier.oid.data.instanceNumber), self.propertyIdentifier, self.propertyValue.unsigned);


    bacnetctf.bro
    A Bro script uses events that are generated to trigger some actions. I wrote a bro script that would pull the IP address from the selected packet that triggered the event my bacnet.evt file additions created. It wrote that IP to a file which would be used later on. Here is the script called bacnetctf.bro:

    #Module to parse bacnet ctf writes to binary output 3 on device 422

    #load processes the __load__.bro scripts in the directories loaded 
    #which basically includes libraries
    @load base/protocols/conn

    #create namespace 
    module bacnetCTF;

    export {
    #Create an ID for our new stream. 
    redef enum Log::ID += { LOG };

    #Define the record type that will contain the data to log.
    type Info: record {
    packet_src: addr &log;
    };
    }

    event bro_init()  &priority=5
    {
    #Create the stream. this adds a default filter automatically
    Log::create_stream(bacnetCTF::LOG, [$columns=Info, $path="bacnetCTF"]);
    }

    #add a new field to the connection record so that data is accessible in variety of event handlers
    redef record connection += {
    bacnetctf: Info &optional;
    };

    event bacnetctf_writeproperty_request(c: connection, invokeID: count, tpe: BACnet::BACnetObjectType, instance: count, property: BACnet::BACnetPropertyIdentifier, propval: string)
       {

       #Don't count packets from ssh box (simulate coming from different subnet to force packet crafting)
     local src_ip: addr;

     if (c$id$orig_h == 192.168.141.111)
     {
           src_ip = 192.168.1.1;
     } else
     {
           src_ip = c$id$orig_h;
     }

      #Log format
      local rec: bacnetCTF::Info = [$packet_src=src_ip];

      c$bacnetctf = rec;

      Log::write(bacnetCTF::LOG, rec);

    }



    Part 3 will pick up from here with building the docker image with these new files.