After my post yesterday about using PROXY protocol for red teams to enable trustless TLS redirectors without losing client information, I wanted to write a short follow up post to talk about how great nginx is. I used a lesser known feature of nginx in that blog post called stream, which originates from ngx_stream_core_module. This enables nginx to act as a simple packet redirector and it can do all sorts of fun stuff.

Redirectors

TCP Redirection with Nginx

By making use of the stream block, we can do arbitrary TCP redirection.

A set of example configurations:

/etc/nginx/stream_redirector.conf on 192.168.0.3.

stream {
	server {
		listen 80;
		proxy_pass 192.168.0.2:80;
	}
}

/etc/nginx/stream_redirector.conf on 192.168.0.2.

stream {
	server {
		listen 80;
		proxy_pass 127.0.0.1:8080;
	}
}

In this set of example configurations, we can do seamless HTTP redirection without directly exposing our app’s IP address. Just include /etc/nginx/stream_redirector.conf in your /etc/nginx/nginx.conf file, outside of the HTTP block.

UDP Redirection with Nginx

By making use of stream we can do arbitrary UDP redirection. I used a simple UDP client and server to proof of concept this example.

udp_redirect_example

A set of example configurations

/etc/nginx/stream_redirector.conf on 192.168.0.3.

stream {
	server {
		listen 53 udp;
		proxy_pass 192.168.0.2:5353;
	}
}

/etc/nginx/stream_redirector.conf on 192.168.0.2.

stream {
	server {
		listen 5353 udp;
		proxy_pass 127.0.0.1:53;
	}
}

This can be combined with the PROXY protocol to get information about the initial client, however it doesn’t seem to work the same way as it does with TCP redirection streams. You can see in this example that it actually just adds the header at both nginx hops and nginx doesn’t strip it away before passing it along to the udp redirection.

udp_redirect_with_proxy

Zero Trust TLS Redirectors

Please see my using PROXY protocol for red teams blog post for information on this topic.

Restricting Direct Access

You should note that in our previous examples, 192.168.0.2 is listening to 0.0.0.0 and there are no restrictions on the source IP address. This means that anyone who visits 192.168.0.2 directly will see the same content as 192.168.0.3 and would be able to link the two addresses together. You can handle this at a different layer, having your firewall only permit inbound connections from 192.168.0.3, or you can have nginx do it by making use of ngx_stream_geo_module. In this case maybe we want to have a separate http server that we redirect unexpected visitors to which can do http redirects.

Alternate /etc/nginx/stream_redirector.conf on 192.168.0.2.

stream {
	upstream proxy_traffic {
		server 127.0.0.1:8080;
	}
	upstream direct_traffic {
		server 127.0.0.1:8081;
	}
	map $remote_addr $backend {
		192.168.0.2 "proxy_traffic";
		default "direct_traffic";
	}
	server {
		listen 80;
		proxy_pass $backend;
	}
}

Where 127.0.0.1:8081 might look something like this:

/etc/nginx/sites-enabled/direct_traffic.conf

server {
	listen 127.0.0.1:8081;
	return 302 http://example.com
}

And 127.0.0.1:8080 might look something like this:

/etc/nginx/sites-enabled/proxy_traffic.conf

server {
	listen 127.0.0.1:8080;
	location / {
		proxy_pass 127.0.0.1:8000;
	}
}

proxy_access_only

Conditional Filtering

IP Address (HTTP)

Defenders have a lot of tools at their disposal, and some of those tools use cloud providers to do things like fetch payloads or snapshot the page when it’s visited, or categorize the content of a link for web safety. One way to help get around this is to block off cloud IP addresses from accessing your applications. In most situations your targets probably won’t be connecting to you through only cloud IP addresses, especially in a phishing scenario. Blocking unwanted traffic in nginx is easy. I even wrote a tool, sephiroth, to make it even easier to generate cloud block lists. But I’ll give a quick rundown of how it’s done here.

To block large amounts of ip addresses, we’re going to make use of the geo block. This is provided via ngx_http_geo_module, which is a default module when installing nginx on Ubuntu 18.04.4.

/etc/nginx/conf.d/bad_ips.conf

geo $block_ip {
	default 0;
	# proofpoint addresses borrowed from https://gist.github.com/curi0usJack/971385e8334e189d93a6cb4671238b10
    148.163.148.0/22 1;
    148.163.156.0/23 1;
    208.84.65.0/24 1;
    208.84.66.0/24 1;
    208.86.202.0/24 1;
    208.86.203.0/24 1;
    67.231.144.0/24 1;
    67.231.145.0/24 1;
    67.231.146.0/24 1;
    67.231.147.0/24 1;
    67.231.148.0/24 1;
	67.231.149.0/24 1;
	67.231.151.0/24 1;
	67.231.158.0/24 1;
}

By putting our bad_ips.conf file in /etc/nginx/conf.d/, it will be loaded before our site configs which enable us to access the $block_ip variable from within our site configurations. Then we can redirect or show a benign page or whatever else we want.

/etc/nginx/sites-enabled/our.site.conf

server {
	listen 80;
	if ($block_ip) {
		return 302 http://example.com;
	}
	location / {
		proxy_pass 127.0.0.1:1337;
	}
}

If using this with TLS, ensure that you are doing it on your trusted infrastructure where you can terminate the TLS connection.

IP Address (Stream)

We can combine some things we’ve learned so far in order to block large ranges of IP addresses at our stream redirector layers. We’re going to make use of geo, upstream, and map blocks. Unfortunately, stream blocks are not capable of using if statements, which is why we use upstream and map instead. It’s a little more restrictive, but we can make it work.

We’re going to use the same /etc/nginx/conf.d/bad_ips.conf from the previous section, but we’re going to have to include it directly in our stream blocks in order to use it, since /etc/nginx/conf.d/* is only included inside the http block by default.

/etc/nginx/stream_redirector.conf on 192.168.0.3

stream {
	upstream good_traffic {
		server 192.168.0.2:80;
	}
	upstream bad_traffic {
		server 127.0.0.1:8080;
	}
	include /etc/nginx/conf.d/bad_ips.conf;
	map $bad_ip $backend {
		1 "bad_traffic";
	    default "good_traffic";
	}
	server {
		listen 80;
		proxy_pass $backend;
	}
}

/etc/nginx/sites-enabled/bad_traffic_handler.conf on 192.168.0.3

server {
	listen 127.0.0.1:8080;
	return 302 http://example.com;
}

And now any of our designated bad ip addresses will get redirected to 127.0.0.1:8080 which will in turn redirect them to http://example.com instead of sending it upstream to our actual application server. This enables redirectors to do some basic filtering instead of purely requiring redirecting it back to the application server unconditionally. If you apply this at the first redirector layer, then your first layer of defense can automatically block known unwanted IP addresses.

While this technically works with TLS streams as well, you’ll encounter the problem where the upstream server for the bad traffic (127.0.0.1:8080 in this case) doesn’t have a valid key pair for the domain being requested, and it will throw an error. This probably isn’t desired. If you’re trying to redirect TLS traffic, it’s generally going to be more reliable to do it at the TLS termination point rather than trying to do it at some arbitrary midpoint.

User Agents

User agent filtering is another fairly common thing used in red team infrastructures. If it’s a mobile user agent, for instance, you might want to only try to capture credentials via a login page. If it’s a desktop user agent, you may want to deliver a different payload based on which OS it is. If it’s an unknown user agent, maybe you don’t want to support it at all.

Inside of a site configuration, you can access the $http_user_agent variable and do some matching on it. ~ will do a case sensitive regular expression match. ~* will do a case insensitve regular expression match. = will do a direct match, and != naturally does the opposite of =.

/etc/nginx/sites-enabled/our-site.conf

server {
	listen 80;
	if ($http_user_agent ~* (curl|wget|python)) {
		return 302 http://example.com
	}
	location / {
		proxy_pass 127.0.0.1:8080;
	}
}

In this example, we do a case insensitive match against the user agent to look for curl, wget, and python and if any of these are detected then we redirect them to example.com instead of our backend server at 127.0.0.1:8080.

Another example might be to do an exact match for a known user agent. Maybe your malware talks over http and you want to use user agent filtering to reduce the unwanted traffic to your C2 server.

/etc/nginx/sites-enabled/our-site.conf

server {
	listen 80;
	location / {
		if ($http_user_agent = "Slackbot-LinkExpander 1.1 (+https://api.slack.com/robots)") {
			proxy_pass 127.0.0.1:8080;
		}
	}
	return 302 http://example.com;
}

You’ll notice that in this case our default behavior is to return a redirect. This is generally a safer design practice (explicit allow) because if something goes wrong then the default behavior won’t expose your C2 server. But it can, and should, be used in combination with deny rules.

Cookies

Perhaps you’re using custom cookies to key your malware. Good for you. Your blue team must be pretty good if it’s come to this. Go buy them a beer, or their other drink of choice. But anyways, nginx also allows us to do filtering based on cookies via the $cookie_COOKIENAME variable format. We can start with something simple:

/etc/nginx/sites-enabled/our.site.conf

server {
	listen 80;
	location / {
		if ($cookie_ismalware ~ "SECRETPREFIX-(.*)") {
			proxy_pass 127.0.0.1:8080;
		}
	}
	return 302 http://example.com;
}

So here we check if the cookie ismwalware exists and if it matches our secret prefix, conveniently called SECRETPREFIX for the sake of this demo. If it has the prefix then we trust it’s one of ours and we invite it into our C2 server.

Stripping Upstream Headers

Sometimes you’re using an application that sets an X-Powered-By: or X-Server header, or some other unwanted header that reveals details about what you’re using (cough gophish cough). Unfortunately it’s a little harder to remove headers in nginx than it is in apache, but it’s still pretty doable. The capability doesn’t ship out of the box, but Ubuntu does provide a package for it. Simply run sudo apt install libnginx-mod-http-headers-more-filter. Whew, that’s a mouthful. Once you’re done, it’ll automatically be added as an enabled module on nginx and you can start to use it. More information on this module can be found on github.

Let’s say we have a server up stream that sets X-Powered-By: Caffeine and Basslines and we don’t want to expose that to our endpoint traffic. So we’re going to proxy_pass our traffic to 127.0.0.1:1337, it will handle the request and pass it back to nginx. nginx will then look for the X-Powered-By header and remove it if found.

/etc/nginx/sites-enabled/our.site.conf

server {
	listen 80;
	location / {
		proxy_pass 127.0.0.1:1337;
		more_clear_headers X-Powered-By;
	}
}

Some Notes

This section contains some notes that are about other things in the post but don’t really fit in a whole section on their own.

A note on proxy_pass

I use the proxy_pass directive a bunch of times throughout this post. In the stream blocks, it works in the context of server. However, when we use http blocks, such as any sites-enabeld/* files, proxy_pass only works in the context of location blocks. If you want to do conditionals to proxy_pass to something, it has to be inside a location block.

A note on the if directive

Nginx has a wiki page about the if directive being evil. It’s probably worth a look if you’re experiencing unexpected behaviors.

Questions? Recommendations? Attacks?

Holler at me on twitter if you have some ideas to add, or something I said doesn’t work for you, or you’ve got some dank memes.

Hack The Planet