Lab #5: Firewalls and proxies

Due: Saturday, Oct 29 11:59PM

Introduction

In this lab, you will be simulating building your own “cyber range” inside of Virginia Cyber Range. You will set up a forward proxy using mitmproxy and a firewall using nftables. In addition, you will be using the journalctl utility to review logs and see what packets are being dropped.

How does Virginia Cyber Range work?

Virginia Cyber Range tries to provide a closed network for students to practice their cybersecurity skills while ensuring that they can’t (accidentally or intentionally) attack external websites. To this end, VCR does two things:

  • It blocks all traffic in or out of the network, except for students’ requests to DNS and HTTP(S) services. This ensures that student environments can’t be attacked from outside of the network (which is useful considering that these environments are often inherently vulnerable). It also prevents students from attacking hosts outside the network with VCR’s machines.

  • It logs all DNS and HTTP(S) traffic that students make to servers outside of the network. This provides an additional check to ensure that among the types of traffic that students are allowed to send outside the network, they aren’t attacking external services.

The server that you are given in this lab simulates a mini-Cyber Range, with a few hundred users that are each making various requests from the lab environment to external servers. You will need to implement a firewall and a forward proxy to block and log disallowed traffic, as well as intercept HTTP requests.

Lab structure

What to submit

You should submit a short PDF report with the following information:

Problem 1: submit your mitmproxy addon, inject_script.py, along with proof (e.g. in the form of screenshots) that it works correctly on an HTTPS site.

Problem 2: submit your firewall configuration, as well as your answers to the questions about the dropped packets and HTTP requests that are asked at the end of the problem.

Grading

This assignment is worth 9 points. Each of the following pieces is worth 3 points:

  • The combined mitmproxy addon and proof that it worked
  • Your nftables configuration
  • Your answers to the questoins at the end of Problem 2.

Problem 1: Set up the forward proxy

In the first half of this assignment, we will set up mitmproxy to act as a forward proxy for traffic coming from your virtual machine.

What is mitmproxy?

mitmproxy is a flexible HTTP(S) proxy written in Python. Its primary usage is for pentesting and network audits, but it’s flexible enough to have a wide variety of uses.

For this assignment, we will be using mitmproxy as a forward proxy; a proxy that’s deployed close to the client side that (for our purposes) can be used to audit traffic going in or out of a network.

Create a user to run the proxy

Before we start using mitmproxy, we’ll want to create a dedicated user to run the proxy. While this isn’t generally strictly necessary for us to use mitmproxy, it’ll be convenient for us to have a dedicated proxy user in the second half of the assignment.

We’ll name our new user mitmproxy. You should run the following command:

sudo useradd -m mitmproxy

(sudo allows you to run a command as root, which is required on Linux in order for you to create a new user.)

Now you can start running the proxy. We will use sudo once again to execute the mitmproxy command as the new user:

sudo -u mitmproxy mitmproxy --showhost \
  --mode upstream:http://proxy.cyberservices.internal

This will start the terminal interface to mitmproxy.

Tangent: http://proxy.cyberservices.internal is Virginia Cyber Range’s internal Squid proxy, which they use to make sure that nobody is doing anything bad with their VMs. All HTTP traffic outside of the VCR network has to be routed through this proxy.

If we weren’t running inside a network that already had its own forward proxy, we would probably run mitmproxy with --mode transparent instead.

Right now the UI looks a little bare. To see how mitmproxy works, open up a new tab in your terminal (e.g. by clicking File > New Tab) and run the following commands:

export http_proxy=http://localhost:8080
curl http://www.example.org
curl http://www.cs.virginia.edu

Now if you return to your original tab you should see some HTTP requests appear in it, such as the following:

The >> on the side indicates which HTTP request intercepted by the proxy you’re currently looking at; you can use your arrow keys to move on to a different request. If you hit Enter, you should see a new panel show up with details about the HTTP request you selected, e.g.:

Try making some requests to other http:// URLs that go through the proxy and make sure you understand how the mitmproxy interface works. You can check out the mitmproxy documentation for a tutorial.

Installing the mitmproxy TLS certificate

Right now, we can’t proxy HTTPS requests through mitmproxy, because it’s not able to read the encrypted sessions between a random client program and an HTTPS server. To fix this issue, we’ll need to install the root TLS certificate generated by mitmproxy; this allows it to establish separate one TLS session from the client to the proxy and another TLS session from the proxy to the server.

With mitmproxy running in one tab, run the following commands:

sudo mkdir -p /usr/share/ca-certificates/extra/
sudo curl http://mitm.it/cert/pem \
  --proxy http://localhost:8080 \
  -o /usr/share/ca-certificates/extra/mitmproxy.crt
sudo dpkg-reconfigure ca-certificates
sudo update-ca-certificates

The first command creates a directory to store the new certificate. The second command downloads the certificate to that directory.

The third and fourth commands install the mitmproxy certificate system-wide, so that unless a client program has its own root TLS certificates that it uses, mitmproxy will be able to intercept and read its HTTPS traffic. When you run dpkg-reconfigure you should get a dialogue like the one shown below, asking which CA certificates you want to install. You should see the mitmproxy.crt certificate in that list. It will initially be deactivated; press Space to activate the certificate, then press Tab and select Ok.

Terminal dialogue showing the new certificate to be installed.

Once the certificates have been updated, run the following commands to test whether you installed them correctly:

export https_proxy=http://localhost:8080
wget --delete-after -l 1 -r \
  https://cs3710.kernelmethod.org/

If you did, you should see the following (or similar) in your mitmproxy session:

Observe that all of these requests are HTTPS, not HTTP, so we’re correctly intercepting the TLS session between the client and the server.

Stealthily injecting JavaScript with mitmproxy

mitmproxy supports Python-based addons that can be used to log and modify requests that pass through the proxy. Create a new addon (for this example we’ll call it inject_script.py), starting from the following template:

"""
Inject some JavaScript into the webpage returned in an HTTP response.
"""

from mitmproxy import ctx, http

def response(flow: http.HTTPFlow) -> None:
    if (response := flow.response) is None or response.content is None:
        return

    # Only inject JavaScript into pages that return status code 200
    if response.status_code != 200:
        return

    # The `response` variable holds a `Response` object, with various fields
    # that you would find in an HTTP response:
    #     https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response
    #
    # If the request contains HTML, you should modify the response.content
    # variable so that it contains your custom JavaScript code.

    # TODO: your code here!

Modify this code so that if the response contains HTML, you inject some JavaScript of your choosing into the page. To run mitmproxy with your addon, run the following:

# Set permissions on inject_script.py so that the mitmproxy user
# can read the file
chmod a+r inject_script.py

sudo -u mitmproxy mitmproxy -s inject_script.py --showhost \
    --mode upstream:http://proxy.cyberservices.internal

For this problem you should submit your mitmproxy addon. In addition, you should submit proof that your addon and mitmproxy setup works by visiting an HTTPS site (in your browser or with curl) and demonstrating that your JavaScript was injected into the page.

Hints

Debugging mitmproxy addons

You can import ctx in an mitmproxy addon and use it to generate log messages. For instance, here is an extremely simply mitmproxy that simply generates a log whenever you receive a response to an HTTP request:

from mitmproxy import ctx, http

def response(flow: http.HTTPFlow) -> None:
  if (response := flow.response) is None or response.content is None:
    return

  ctx.log.info(f"{response = }")

You can view the event log in mitmproxy by pressing E from the main window; press q to go back. This can be helpful for debugging addons.

In addition, you don’t need to restart mitmproxy every time you make a modification to your addon. mitmproxy will automatically reload your addon for you as soon as you save your Python file.

Browser configuration for the proxy

If you wish to demonstrate that mitmproxy works in your browser, you will need to go to Firefox’s preferences (either by clicking on the “hamburger” button in the top right followed by settings, or by entering about:preferences in the URL bar). Go to Network Settings where you should see some options to configure proxy access.

Click Manual Proxy Configuration, and set the following options:

  • HTTP Proxy: localhost
  • Port: 8080
  • You should also click the “Also use this proxy for HTTPS” button.

Now all of your traffic should go through the proxy, which you can verify by going to http://neverssl.com. Note that Mozilla has its own TLS certificates that it uses, so whenever you go to an HTTPS site you’ll get a warning from the browser. You should be able to click past those warnings and go on to the website to see your HTTPS traffic go through mitmproxy.

Additional hints

  • If you’re finding that the HTTP requests you’re making with curl or wget aren’t working, ensure that you’re export‘ing the http_proxy and https_proxy environment variables as shown above. Alternatively, curl takes a --proxy flag, for example:
curl --proxy http://localhost:8080 http://www.example.org
  • You may wish to look at the mitmproxy addon examples for some inspiration for what you should put in your mitmproxy addon.

  • In order to make sure that you only modify responses containing HTML, you can check the Content-Type header in the HTTP response, which you can retrieve with response.headers.get(b"Content-Type").

  • mitmproxy will automatically reload your addon every time you make a change to it, so you don’t need to repeatedly stop and restart mitmproxy while you’re writing your implementation of inject_script.py.

Problem 2: Set up the firewall

For the second part of this assignment, you will be setting up a host-based firewall for the server using nftables.

Firewalls

We want to achieve two goals:

  • Block all inbound and outbound traffic from the machine except for DNS and HTTP(S) traffic, and log dropped traffic.
  • Route all HTTP(S) traffic through mitmproxy so that we can log it and review any potentially malicious requests that users are making.

We can achieve both of these goals with nftables.

Configure nftables

To get you started, there’s a base nftables configuration file, which you can either find on the server in /usr/local/share/cs3710/base.nft. You can also download it from this link.

There are already some basic tables and chains defined in this base configuration. Note that this configuration already starts off with the following rules, which redirect HTTP(S) traffic to the proxy:

define NO_REDIRECT = { mitmproxy }

table inet nat {
    chain prerouting {
        type nat hook output priority -100; policy accept;
        skuid != $NO_REDIRECT tcp dport {80, 443} goto proxy
    }

    chain proxy {
        counter comment "Number of packets rerouted to the proxy: "
        limit rate 15/minute log prefix "PROXY_LOG: " flags all
        ip protocol tcp counter redirect to :8080
    }
}

So the main thing you need to do for this problem is implement the traffic blocking. Here are the various constraints you will want to make sure your firewall conforms to:

  • Loopback: All inbound and outbound traffic on the loopback interface (i.e. from the host back to itself) should be permitted.

  • Proxy traffic: All traffic to and from the mitmproxy port (TCP port 8080) should be permitted. In addition, you’ll want to allow all traffic from the mitmproxy user.

  • DNS: All outbound DNS queries and responses to those queries should be permitted (in the real world we’d want to audit DNS queries too, but we won’t concern ourselves with that for this lab).

  • Established connections: you should allow traffic for all “related” and “established” TCP connections. This is traffic coming from connections that the machine itself established, so that if a packet passes all of the rules in the output chain correctly, the machine can get a response to that packet.

  • Logging: dropped packets should be logged to the system journal.

  • Default drop: all packets that do not meet any of the prior conditions for acceptance should be dropped by default.

You should look at the hints at the end of this assignment for tips on how to implement your firewall.

Review dropped packets

Once you’ve set up your firewall, you should answer the following questions:

  • How many packets are being dropped per minute?
  • Query the system logs with journalctl to give an example of a dropped packet.
  • Why are those packets being dropped?

Review HTTP requests

With the firewall up, you should be able to see HTTP requests passing through the proxy. Answer the following questions:

  • What domains are users making HTTP requests to?
  • What is the least common domain that users are connecting to? What data are they sending to that domain?

Hints

  • The mitmproxy server listens on port 8080 by default.
  • VCR is already cut off from the rest of the internet, so when your rules are completely finished you may not see any traffic getting dropped by your inbound rules.

General hints

mitmproxy commands

Navigation: navigation through mitmproxy is fairly straightforward. In the main panel, you can shift between looking at different HTTP requests by pressing the up and down arrow keys; the >> on the left will indicate which HTTP request you currently have selected. You can press Enter to look at a specific HTTP request, at which point you can press the left and right arrow keys for more details. To go back (or to quit mitmproxy), press q.

Commands: Inside of mitmproxy, you can run a few different commands that you may find helpful. You can run

mitmproxy --commands

to list the full set of available commands. To run one of these commands, press : followed by the command inside of mitmproxy, e.g. :view.clear.

  • view.clear: clear the HTTP requests that mitmproxy has received so far.

nftables commands

This section contains various commands you may find useful for working with nftables. All of these commands need to be run as root, so you should use sudo ... before the command (e.g. sudo nft list ruleset).

  • nft list ruleset: list the current nftables firewall rules.
  • nft -f firewall.nft: read the nftables script firewall.nft, and apply all of the rules that it contains.
  • nft flush ruleset: delete all nftables rules.
  • nft list counters: list all named counters in the tables, and the number of packets that have gone through them.

journalctl commands

You can view the system log with journalctl. For these commands you will also need to use sudo:

  • journalctl -xe: View the system journal, starting from the end. This can be useful for viewing log messages generated by nftables.
  • journalctl -xe -g PATTERN: search for a pattern (in this example, PATTERN) in the system logs.
    • You can also do journalctl -xe | grep PATTERN

References

mitmproxy:

nftables:

Writing firewall rules

This section contains some tips for writing your firewall rules in Problem 2.

First steps

When you start writing your rules, the first thing you should do is set the policies of the input and output chains to accept all packets by default. Then you should add a rule to the bottom of each chain counting and logging the dropped packets, e.g.

chain input {
  ...

  counter log prefix "input.drop " flags all
}

This will allow you to meet the logging requirement, but it will also show you which packets would be dropped once you finish writing all of your rules. This will help you figure out where there might still be gaps in your rules.

You can see the number of dropped packets with nft list ruleset, and you can see the log messages emitted by nftables using journalctl, e.g. sudo journalctl -xe -g input.drop in the example above.

Debugging firewall rules

You can insert logging statements and counters in other places to make it easier to see which of your firewall rules are being matched, and by what packets. Here’s an example:

chain output {
  ...

  ip protocol icmp counter goto output_accept
  tcp dport 80 counter goto output_accept
  skuid student counter goto output_accept

  ...
}

chain output_accept {
  counter comment "Accepted outbound packets: "
  log prefix "output.accept " flags all accept
}

In this configuration, we created a new chain, output_accept, that counts the number of packets it sees, emits a log message (which you can query with journalctl -xe -g output.accept), and then accepts the packet. In the original output chain, all ICMP (ping) packets, packets going to port 80, and packets from user student automatically go to this chain before being accepted.

If you find that there are too many log messages being generated, you can rate-limit them with something like

limit rate 10/minute log prefix "output.accept " flags all accept

Measuring dropped packets

Named counters (such as the input_drop_counter and output_drop_counter counters given in the base configuration) can be listed with the following command:

$ sudo nft list counters
table inet firewall {
        counter input_drop_counter {
                comment "Number of dropped input packets: "
                packets 14 bytes 928
        }
        counter output_drop_counter {
                comment "Number of dropped output packets: "
                packets 14 bytes 720
        }
}

You may find this helpful while taking your measurements. You can reset these counters with

$ sudo nft reset counters

The number of packets matched by non-named counters can be seen when you run nft list ruleset.

Getting locked out of VCR

Host-based firewalls have footguns lying all over the place. In the real world, it is extremely easy to write a firewall rule that blocks you from accessing the machine that you’re working on, thereby locking yourself out (a mistake that your own professor has made more times than he’d care to admit).

The input and output chains of the firewall table that you get in the base nftables configuration each include a rule to allow all traffic to ports 22 and 3389 in Virginia Cyber Range. You should not delete these rules, and preferably you should try to keep them close to the top of each chain. These rules are required for VCR to give you browser access to your VM.

Should you either choose not to heed that warning, or you find a different way to cut yourself off from VM access, it’s still possible to recover the machine without a full box reset. The VM will freeze, and VCR will tell you that it’s no longer able to access the machine. To fix this problem, you can just reboot your VM from the web interface; when it comes back online, the firewall rules should all be flushed and you should be able to access your machine again.