Lab 07: TCP Sessoion Hijacking

Table of contents
  1. Introduction
    1. Packet filtering
    2. Logistics
    3. Learning objectives
    4. Getting the configuration
    5. Generating your .env file
    6. Network topology
  2. Step 1: Map the running services
  3. Step 2: Adding firewall rules
    1. View current rules
    2. nft tables
    3. nft chains
      1. Chain types
      2. Chain hooks
      3. Chain policy
      4. Chain priority
      5. Adding a new chain
      6. Flushing and modifying chains
      7. Adjust the chain
  4. Step 3: nftables scripting
  5. Adding rules
    1. Counters
    2. Flushing a chain
    3. Deleting a rule
  6. Exploring actions
    1. Dropping packets
    2. Rejecting packets
  7. Your first firewall
    1. Test other TCP ports
    2. Test other protocols
    3. Submission

Introduction

This lab serves as an introduction to using nftables to set up a stateless firewall that can control which traffic can enter into a subnet while blocking other kinds of traffic. We will mostly focus on TCP traffic in this lab, but the same concepts generalize to other protocols and applications. We will focus first on packet filtering and then add on more applications in later concept labs.

Packet filtering

A packet filter is a piece of software that hooks into your network layer and reads packet headers to determine whether such packets are allowed in the system or not. At its most basic level, a packet filter makes one of two decisions:

  1. ACCEPT a packet, which means that packet can go through to the application or to be forward on another interface.

  2. DROP a packet, which means that packet will be dropped from the system, and thus will not be allowed to move into your network stack.

Those two actions form the basis of a packet filter, however, much more sophisticated applications can be created, such as performing Network Address Translation (NAT) and more involved firewall applications.

Packet filtering allows you to gain control over the network, deciding which packets can go through and which ones will be dropped. It also allows for visibility over your network, so you can track what traffic is coming in and where it destined. All of this will serve the purpose of enhancing the network’s security. You can think of it as a bouncer that sits at your front door and decides who is allowed to come through and knock and who will be turned away directly.

Logistics

We will continue with the same set of tools from Lab01, these are namely:

  1. Wireshark to visually see packets and protocols.
    • Install this on your local machine, so you can see things visually.
  2. If you are comfortable with command line, you can also use tshark to observe the same packets and protocols, directly on the server machine.

  3. scp or rsync will prove to be useful to obtain packet captures from the server and download them on your local machine. They should be available by default on your Linux distribution that you are running.

Learning objectives

After completing this lab, you should be able to:

  • Define how a firewall works in the context of a Linux box.

  • Experiment with different filtering rules using nftables.

  • Add nftables rules to restrict access to your private network for certain individuals or applications.

Getting the configuration

You can find the starter setup for this lab under the 10_Lab10 directory of the csse341-labs repository. If you have set up your private repository correctly, you can fetch the latest version of the labs using the following sequence of commands:

  1. Synchronize with the labs remote using git fetch upstream.

  2. Pull the latest changes from the main branch of the class repository:

    $ git pull upstream main
    
  3. Push the starter setup to your repository so you can start modifying it:

    $ git add 10_Lab10
    $ git push origin main
    

Generating your .env file

Before we spin up our containers, there are some configuration variables that we must generate. To do so, please run the gen_env_file.sh script from the lab repository directory as follows:

$ ./gen_env_file.sh

If run correctly, you will find the following new files:

  1. .env (hidden file - use ls -al to see it) contains your UID and GID variables.

  2. connect_*.sh a utility script to connect to each container in this lab.

  3. run_*.sh is another utility script that allows you to run commands on a container without logging into it.

Network topology

In this lab, our network topology is simple. We have a client container connected to a firewall container. On the other end sits a server that is providing certain services to your client container. The firewall is acting both as a router and a packet filter for your two subnetworks.

Here’s a simple representation of this topology (Shamelessly generated using AI):

+-------------------+          +-------------------+          +-------------------+
|      client       |          |     firewall      |          |      server       |
|  IP: 10.10.0.4    |          | eth0: 10.10.0.10  |          |  IP: 10.10.1.5    |
|  Interface: eth0  +----------+ eth1: 10.10.1.10  +----------+  Interface: eth0  |
+-------------------+          +-------------------+          +-------------------+

Step 1: Map the running services

To start off, make sure that the client container can reach the server container and vice versa. Simply ping the server from the client to make sure that the network is up and running.

Our first step in protecting our network is understanding what services are running on our network. Use the nmap tool to uncover all services running on the server container, that is the one we’d like to protect.

Hint: server is only running TCP services, so it should be quick and easy to find out what services are running there. There should be three of them.

After running your nmap scan, answer the following questions:

  1. List out the services running on the server container.
  2. For each service running there, list a command that you can use to test if that service is running and reachable.

Step 2: Adding firewall rules

If you have done some form of networking before, then you must have heard of iptables as a tool to use the Linux kernel’s features to perform packet tracing and filtering. While you could use iptables to perform everything we need in this lab, we will be using the newer nftables to do so.

nftables leverages newer features in the Linux kernel (starting kernel >= 3.13) to provide similar functionalities to iptables, albeit with a better (arguably) syntax and rule definition, reducing code duplication, and enhancing performance.

All your firewall rules must happen on the firewall container. Do not add or change any rules on the client or server containers.

Please note that if you login as root and you edit or create any files, then you might lose access to it outside of the container. To prevent that from happening, edit your files from the virtual machine (not the container). But if you happen to forget, you can always change the owner of that file back to netsec using chown netsec file_path.

View current rules

On the firewall container, let’s examine the current rules there. You can do so using:

sudo nft list ruleset

On my setup, that showed something like the following:

# Warning: table ip nat is managed by iptables-nft, do not touch!
table ip nat {
        chain DOCKER_OUTPUT {
                ip daddr 127.0.0.11 tcp dport 53 counter packets 0 bytes 0 dnat to 127.0.0.11:45487
                ip daddr 127.0.0.11 udp dport 53 counter packets 0 bytes 0 dnat to 127.0.0.11:42559
        }

        chain OUTPUT {
                type nat hook output priority dstnat; policy accept;
                ip daddr 127.0.0.11 counter packets 0 bytes 0 jump DOCKER_OUTPUT
        }

        chain DOCKER_POSTROUTING {
                ip saddr 127.0.0.11 tcp sport 45487 counter packets 0 bytes 0 snat to :53
                ip saddr 127.0.0.11 udp sport 42559 counter packets 0 bytes 0 snat to :53
        }

        chain POSTROUTING {
                type nat hook postrouting priority srcnat; policy accept;
                ip daddr 127.0.0.11 counter packets 0 bytes 0 jump DOCKER_POSTROUTING
        }
}

You will notice that docker by default injected a bunch of rules to make sure that your traffic does not leak out of the internal network and cause mayhem.

Please do not mess with any of the docker rules as breaking those can lead to sever connectivity issues and can also impact other users on our server.

nft tables

The first abstraction when it comes to nftables is that of a table. A table is a top level container of rulesets that holds chains, rules, maps, and state objects. A packet can pass through several tables before arriving to the target application or forwarded on the network.

We must associate a table must with exactly one family. In nftables, we currently have several families:

  1. ip: Filters IPv4 traffic.

  2. ip6: Filters IPv6 traffic.

  3. inet: Filters both IPv4 and IPv6 traffic.

  4. arp: Filters ARP packets.

  5. bridge: Filter traffic traversing bridged networks.

  6. netdev: This one is a bit different, it enables us to see and filter traffic right out of the Network Interface Card (NIC), i.e., as raw as possible.

In this lab, we will only concern ourselves with the ip family.

Let’s first create a table for this lab, we’ll call it netsec_tbl. To do so, you can use:

sudo nft add table ip netsec_tbl

The first argument after sudo nft add table is the family to which the table will belong (which is ip in our case), followed by the name you’d like to give to that table.

Now, run sudo nft list ruleset to see that your table has been successfully created. It will show up as empty for now. To only see our table, you can also use sudo nft list table netsec_tbl.

Here are some other useful commands when it comes to tables (recall to always use sudo if you are not logged in as root):

  • To delete a table: sudo nft delete table ip netsec_tbl.
  • To remove all the rules in a table: sudo nft flush table ip netsec_tbl.
  • To see just the tables (without the chains and rules): sudo nft list tables.

nft chains

Each table will have one or more chains. Chains are sets of rules that are apply to your packets at a specific location, specific by a hook into the kernel’s network stack. Unlike iptables, nftables does not have predefined chains, you will add chains to your tables at the corresponding hooks.

To add a chain to your table, here’s the generic syntax. Everything between <> is an argument, while everything between [] is optional.

nft 'add chain [<family>] <table_name> <chain_name> { type <type> hook <hook>
priority <value> ; [policy <policy>] ; [comment "text comment"] ;}'

where:

  • <type> is the type of your chain (see below).

  • <hook> is the name of the hook where we weill insert your chain.

  • <priority> is an integer representing the priority of your chain (relevant if you have multiple chains).

  • <policy> is the default policy to apply to packets on this chain after evaluating all rules.

  • text comment is simply a comment you can put it to document your chain.

Please note that the single quotes around the add chain command are mandatory unless you want to escape all your semicolons and quotes. So, unless you use the signal quotes, you’ll have to write \; for semicolons and \" for quotes.

Chain types

A chain can have one of three types. In this lab, we will only focus on the filter type. We will discuss other types as we need them.

Chain hooks

We can place each chain at a possible hook in the kernel. Here are the possible values:

  1. ingress: used only for netdev family. Basically this hooks right after your NIC driver.

  2. prerouting: This hook sees all incoming traffic before we make any routing decision. For example, this sees traffic destined to the machine itself and packets forwarded to other devices on the network.

  3. input: This hook sees packets that are addressed to the local system (for example, if someone pings the firewall, that packet will show up on the input hook).

  4. forward: This hook sees packets that have been routed and are not destined to the local system.

  5. output: This hook sees packets that are originating from the local system and are leaving it.

  6. postrouting: This hook sees all packets that are leaving the system, regardless of whether they are origination from the local system or forwarded from somewhere else.

In this lab, we are mostly interested in the prerouting, input, output, and forward hooks.

Chain policy

The chain’s base policy defines what happens when a packet reaches the end of a ruleset and all the rules have decided to pass it through. Currently, there are only two possible options, which are accept to keep the packet alive and moving on and drop to drop the packet and discard it.

By default, the policy is accept unless otherwise specified.

Chain priority

We use the chain priority to order the chains, i.e., which chains get applied first. Chains with a lower priority (i.e., most negative) apply before chains with positive priority value, etc.

To see a list of hooks and default priorities, you can consult the nftables wiki page.

Adding a new chain

Based on that, let’s go ahead and add our first chain. We’ll call this one netsec_in to catch packets coming into the firewall that are destined for the firewall itself.

Before starting, try to reach the firewall container from the client container through a simple ping. Make sure that both containers are able to reach each other.

Next, add this chain, use:

sudo nft 'add chain ip netsec_tbl netsec_in { type filter hook input priority 0 ;
policy drop ; comment "my first chain" ; }'

To make sure your chain shows up in the table, you can use sudo nft list table netsec_tbl. Mine looks like the following:

table ip netsec_tbl {
        chain netsec_in {
                comment "my first chain"
                type filter hook input priority filter; policy drop;
        }
}

On your question sheet, answer the following question:

  1. What do you expect the impact of the chain we have added to be?
  2. Verify your answer by running a simple command from the client or the server.
  3. How would you change the chain to make sure that this behavior does not take place?

Flushing and modifying chains

As we have seen, our netsec_in chain is not desirable, so we’d like to remove it and replace it with a chain that produces more of what we want.

To do so, first you’ll need to flush the chain to remove any rules in it using:

sudo nft flush chain netsec_tbl netsec_in

Then you can delete the chain using:

sudo nft delete chain ip netsec_tbl netsec_in

Verify that your chain has disappeared using sudo nft list table netsec_tbl.

Adjust the chain

Before moving on, recreate the netsec_in chain with the correct parameters so that you can play with the packets that are coming into the firewall. Verify that your chain does not lead to the behavior we observed earlier on.

Please note that nftables tables, chains, and rules we define here are not persistent, i.e., they will disappear if you restart your container.

Step 3: nftables scripting

Before starting this step, please delete all chains and tables from Steps 1 and 2.

Previously, we had created tables and chains by hand, one by one. That is not scalable! We can do a bit better using nftables scripts, so let’s go ahead and do that here to get started. To get started, under the /volumes/ directory, create a file called netsec_tbl.nft and add the following in it:

#!/usr/sbin/nft -f

# create the table netsec
#   note that we don't need to use nft here, simple start the command.
add table ip netsec_tbl

# create our first chain for incoming traffic
add chain netsec_tbl netsec_in { type filter hook input priority 0 ; policy accept ; comment "our first netsec chain" ; }

Next, make this script executable using chmod u+x netsec_tbl.nft and then execute it using ./netsec_tbl.nft. To verify that the script executed correctly, check the content of the table using (note that I reduced the indent size for clarity):

$ sudo ./netsec_tbl.nft
table ip netsec_tbl {
  chain netsec_in {
      comment "our first netsec chain"
      type filter hook input priority filter; policy accept;
  }
}

Adding rules

Now that we have our first chain, we can start adding rules to it. Let’s start with our first rule. Create another script, I called it netsec_rules.nft, as follows:

#!/usr/sbin/nft -f

# include the file that creates the table and chain, so that we can mess around
# with the rules here
include "./netsec_tbl.nft"

# define some variables, adjust these for your ip addresses
define client_ip = 10.10.0.4
define server_ip = 10.10.1.4

# add our first rule
add rule netsec_tbl netsec_in ip saddr $client_ip counter

As you notice in the above script, we can include the previous file to create the table using include "./netsec_tbl.nft", this way you don’t have to run a different script to get everything together. You can also define variables, such as the client_ip and the server_ip. Finally, we add our first rule:

add rule netsec_tbl netsec_in ip saddr $client_ip counter

Let’s break this one down, add rule is simply the nft command we’d like to execute. Here’s the breakdown of the other parameters:

  • netsec_tbl is the table in which our rule is to be added.
  • netsec_in is the chain in that table where to add our rule into.
  • ip specifies that we are looking to math IPv4 packets.
  • saddr $client_ip specifies that we are interested in IPv4 packets whose source address match the client’s IP address.
  • counter is the action we’d like to take on matching packet. We will explore what this means next.

Please make sure to adjust the client_ip and the server_ip variables to contain the IP address of the client and the server specific to your environment.

Counters

After creating the rules script, execute it using:

sudo ./netsec_rules.nft

Verify that it run correctly using:

sudo nft list table netsec_tbl

My output looks something the following:

table ip netsec_tbl {
  chain netsec_in {
    comment "our first netsec chain"
    type filter hook input priority filter; policy accept;
    ip saddr 10.10.0.4 counter packets 0 bytes 0
  }
}

Now, from the client container, try to ping the firewall using ping -c3 firewall and then check again the content of the table.

Based on your observations above, answer the following questions:

  1. What do you think the counter rule is doing?

Next, from the client, try to reach the server using ping -c3 server and then check the content of the table again.

  1. Does the table change after the client pings the server? What in the nftables table and chain impact this outcome?
  2. If you were to change the table or chain to apply the counter rule to the client to server traffic, what would your script look like? Make sure to write such a script and test it before submission.

Flushing a chain

To remove all rules from a chain, you can flush that chain and drop it back to its default policy specified when you created the chain. To do so, you can use:

sudo nft flush chain netsec_tbl netsec_in

Recall that you can also delete that chain using nft delete chain netsec_tbl netsec_in, if needed.

Deleting a rule

If you’d like to just delete a rule, then you will first have to obtain that rule’s number from the table. You can do so by adding the -a flag to the nft command as follows:

sudo nft -a list table netsec_tbl

On my container, here’s my output:

table ip netsec_tbl { # handle 2
  chain netsec_in { # handle 6
    comment "our first netsec chain"
    type filter hook input priority filter; policy accept;
    ip saddr 10.10.0.4 counter packets 0 bytes 0 # handle 7
  }
}

You will notice that the rule that we added earlier now has comment next to it showing its handle number # handle 7 above. You can use this number to index that specific rule that you wish to manipulate.

To delete that rule, you can use:

sudo nft delete rule netsec_tbl netsec_in handle 7

That rule will disappear from your table after that.

Exploring actions

Let’s now turn to making our rules take actions on the matching packets. An obvious action would be accept packet, another would be to drop it, a third would be to reject that packet. We will explore the difference between drop and reject in the following section.

Dropping packets

Let’s start first by dropping all packets that are coming from the client container and destined to the firewall itself. We will be doing our work in the netsec_tbl table and its corresponding netsec_in chain.

Let’s add our first rule as follows (feel free to put this in a script if you’d like):

sudo nft add rule netsec_tbl netsec_in ip saddr 10.10.0.4 icmp type echo-request counter drop

Note that in this case our rule perform two actions: counter and drop, which means that we would like to count all ICMP echo request packets from the client and then drop them.

Now, verify that the rule is working by trying to ping the firewall from the client container. If your ping is not successful, then you should be good.

Rejecting packets

Now let’s modify the rule we added in the previous exercise. To do so, we must obtain the rule’s handle. To do so, use the -a flag just like we did before and record the rule’s handle.

To replace that rule, you can then use:

sudo nft replace rule netsec_tbl netsec_in handle <handle_num> ip saddr 10.10.0.4 icmp type echo-request counter reject

where <handle_num> is the handle number of your rule.

Now, test the ping from the client again and watch the difference.

  1. Based on this experiment, what is the main difference between the drop and the reject actions?
  2. Consider now the following nft script:

    #!/usr/sbin/nft -f
    
    add table ip netsec_tbl
    
    add chain netsec_tbl netsec_out { type filter hook output priority 0; }
    
    1. Describe the impact of the following rule on the container:
    add rule netsec_tbl netsec_out icmp type echo-request drop
    
    1. Describe the impact of the following rule on the container:
    add rule netsec_tbl netsec_out icmp type echo-reply drop
    

Your first firewall

Now, we would like to implement our first firewall. The goal of your firewall is to prevent all traffic from the client to the server, expect for the following TCP ports:

  • port 22 for ssh,
  • port 23 for telnet,
  • port 80 for http.

All other traffic generated by anyone and destined to the server should be dropped. We are also interested in keeping track of the number of packets that are able to reach the server, i.e., we do want to count those packets that have a TCP destination port being one of the acceptable three.

To test your firewall, make sure that all three of the services are running:

  • To test ssh, simply try ssh server from the client (you don’t need to login, just make sure you can connect).
  • To test telnet, simply try telnet server from the client.
  • To test http, simply try wget server from the client. If wget is not installed on the container, you can install it using apt update && apt install -y wget.

All of your firewall rules must be on the firewall container and not the server itself. We do not want our server to even see any of this traffic.

To test other types of traffic, try to ping the server from the client, you should not be able to reach the server in any way. Also try to set up a netcat server on the server container and attempt to connect to it from the client, it should also not work. Here are some examples:

Test other TCP ports

Create a netcat server on the server container using nc -n -v -l 1234 and then try to connect to it from the client’s side using nc server 1234. If your firewall works correctly, it should not make it through.

Test other protocols

Make sure that ping is not working and only TCP services are reachable. To test out another protocol, set up a UDP netcat server on the server using nc -n -v -u -l 1234 and then connect to it from the client using nc -u server 1234, it should also not be able to connect.

Submission

Please submit your worksheet and your final script to the appropriate drop boxes on Gradescope.