GoSecure / pyrdp

RDP monster-in-the-middle (mitm) and library for Python with the ability to watch connections live or after the fact
https://www.gosecure.net/blog/2020/10/20/announcing-pyrdp-1/
GNU General Public License v3.0
1.55k stars 250 forks source link

Layer 2 transparent proxying #204

Closed obilodeau closed 4 years ago

obilodeau commented 4 years ago

We toyed with the idea of doing L2 transparent proxying instead of L3. This would be more flexible if you need to drop a device on a network segment with DHCP.

It took us more time than I'm willing to admit but here are the rough steps.

  +--------+           +------+             +--------+
  | CLIENT | <-- 1 --> | MITM | <--- 2 ---> | SERVER |
  +--------+           +------+             +--------+
 10.13.37.10         10.13.37.118*          10.13.37.111

*: Assigning an IP to the MITM is necessary for operation

Netplan bridge configuration:

network:
  version: 2
  renderer: networkd
  ethernets:
    enp0s3:
      dhcp4: no
    enp0s8:
      dhcp4: no
  bridges:
    br0:
      dhcp4: yes
      # or
      #addresses:
        #- 10.13.37.11/24
      interfaces:
        - enp0s3
        - enp0s8

In my case, enp0s3 is the interface on the MITM system with is on the segment shown as (1) above. enp0s8 is the interface on the MITM system on the segment shown as (2) above.

setup.sh::

#!/bin/bash -x

# The IP of the RDP server which will receive proxied connections.
SERVER_IP=10.13.37.111

# The mark number to use in iptables (should be fine as-is)
MARK=1

# The routing table ID for custom rules (should be fine as-is)
TABLE_ID=100

# Create a custom routing table for pyrdp traffic
echo "$TABLE_ID    pyrdp" >> /etc/iproute2/rt_tables

# Route RDP traffic intended for the target server to local PyRDP (1)
iptables -t nat \
    -A PREROUTING \
    -d $SERVER_IP \
    -p tcp -m tcp --dport 3389 \
    -j REDIRECT --to-port 3389

# Mark RDP traffic intended for clients (2)
iptables -t mangle -A PREROUTING \
    -s $SERVER_IP \
    -m tcp -p tcp --sport 3389 \
    -j MARK --set-mark $MARK

# Set route lookup to the pyrdp table for marked packets.
ip rule add fwmark $MARK lookup $TABLE_ID

# Add a custom route that redirects traffic intended for the outside world to loopback
# So that server-client traffic passes through PyRDP
# This table will only ever be used by RDP so it should not be problematic
ip route add local default dev lo table $TABLE_ID

# If you want the interception to happen at the Layer 2
# Make sure that both your interfaces are bridged together

# WARNING: if you have other important firewall rules, make sure to test the impact of this
modprobe br_netfilter
echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables

# Target DROP in brouting means out of L2 go route in L3
ebtables -t broute -A BROUTING -i enp0s8 -p ipv4 --ip-src $SERVER_IP --ip-proto tcp --ip-source-port 3389 -j redirect --redirect-target DROP
# recent linux like debian buster needs to use ebtables-legacy as nft ebtables doesn't support the broute table yet
#ebtables-legacy -t broute -A BROUTING -i enp0s8 -p ipv4 --ip-src $SERVER_IP --ip-proto tcp --ip-source-port 3389 -j redirect --redirect-target DROP

# Disable return path filtering
echo 0 > /proc/sys/net/ipv4/conf/default/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/enp0s3/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/enp0s8/rp_filter

cleanup.sh (Warning: it is a bit aggressive on firewall rule deletion, adapt to your needs):

#!/bin/bash -x
# Use same value as in setup script
MARK=1
TABLE_ID=100

# Remove custom routing table for pyrdp traffic
sed -i '/pyrdp/d' /etc/iproute2/rt_tables

# Remove RDP traffic redirection
iptables -t nat \
    -F PREROUTING

# Remove all marking
iptables -t mangle -F PREROUTING

# Remove route lookup for marked packets
ip rule del fwmark $MARK lookup $TABLE_ID

# Remove custom route that redirects outside traffic to loopback
ip route del local default dev lo table $TABLE_ID

# Remove L2 to L3 packet passing
ebtables -t broute -F BROUTING

# Reseting kernel defaults
# bridging shouldn't hit iptables rules
echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptables
# re-activating return path filtering
echo 1 > /proc/sys/net/ipv4/conf/default/rp_filter
echo 1 > /proc/sys/net/ipv4/conf/all/rp_filter
echo 1 > /proc/sys/net/ipv4/conf/enp0s3/rp_filter
echo 1 > /proc/sys/net/ipv4/conf/enp0s8/rp_filter

Invoking pyrdp (as root!):

pyrdp-mitm.py --transparent 10.13.37.111

I'll merge this with the upstream docs eventually but not now. I wanted to keep track of it so here it is.

*: This is a limitation that we should be able to remove but I couldn't figure it out in a timely fashion.

obilodeau commented 4 years ago

Ok, we spent more time on this one trying to remove the interface needs an IP where the targets are limitation. The issue is still there but we understand why and the technique has improved in the process. There's also an alternative without an IP present but it requires prior knowledge of the target network (or on the fly configuration) which is not ideal.

Lastly, we might be able to do a fully transparent proxy where the targets don't need to be specified in advance and w/o relying on our bettercap caplet and from a single process not one-per target.

Tproxy implementation

Note that the setup is the same as in this issue's message. Same IPs and layout.

Netplan

Didn't really change, just tweaks

network:
  version: 2
  renderer: networkd
  ethernets:
    enp0s3:
      dhcp4: no
    enp0s8:
      dhcp4: no
    # management interface
    enp0s9:
      dhcp4: yes
  bridges:
    br0:
      interfaces:
        - enp0s3
        - enp0s8
      # if you are confident that your deployment is not going 
      # to cause a loop you should leave these enabled, otherwise comment them
      parameters:
        stp: false
        forward-delay: 0

IP_TRANSPARENT on the client side

@alxbl added IP_TRANSPARENT support to the client-side socket. Here's a dirty PoC patch. Don't tell him that I posted this code :wink:. He really rushed out this patch for me so don't worry implementation will be cleaner but I wanted to document the whole working thing and this piece is pretty important.

This allows the incoming socket to receive connections not destined to a local address and enables in-kernel interactions with TPROXY iptables rules.

diff --git a/bin/pyrdp-mitm.py b/bin/pyrdp-mitm.py
index 4273295..40d2c62 100755
--- a/bin/pyrdp-mitm.py
+++ b/bin/pyrdp-mitm.py
@@ -18,11 +18,37 @@ from pyrdp.core.mitm import MITMServerFactory
 from pyrdp.mitm import MITMConfig, DEFAULTS
 from pyrdp.mitm.cli import showConfiguration, configure
 from pyrdp.logging import LOGGER_NAMES
+import socket

 def main():
     config = configure()
-    reactor.listenTCP(config.listenPort, MITMServerFactory(config))
+
+    # HACKY TEST
+    ok = False
+    if config.transparent:
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        s.setblocking(0)
+        try:
+            if not s.getsockopt(socket.SOL_IP, socket.IP_TRANSPARENT):
+                s.setsockopt(socket.SOL_IP, socket.IP_TRANSPARENT, 1)
+                ok = True
+        except Exception:
+            pass
+
+        s.bind(('0.0.0.0', config.listenPort))
+        out = s.listen()
+        print(out)
+        print('BOUND TRANSPARENTLY')
+
+        reactor.adoptStreamPort(s.fileno(), socket.AF_INET, MITMServerFactory(config))
+        s.close()
+
+    if not ok:
+        print('FAILED TO BIND TRANSPARENTLY.')
+        reactor.listenTCP(config.listenPort, MITMServerFactory(config))
+
     logger = logging.getLogger(LOGGER_NAMES.PYRDP)

     logger.info("MITM Server listening on port %(port)d", {"port": config.listenPort})

setup.sh:

#!/bin/bash -x

# The IP of the RDP server which will receive proxied connections.
SERVER_IP=10.13.37.111

# The mark number to use in iptables (should be fine as-is)
MARK=1

# The routing table ID for custom rules (should be fine as-is)
TABLE_ID=100

# Create a custom routing table for pyrdp traffic
echo "$TABLE_ID    pyrdp" >> /etc/iproute2/rt_tables

# this can redirect to localhost now (1)
iptables -t mangle -I PREROUTING -p tcp -d 10.13.37.111 --dport 3389 \
  -j TPROXY --tproxy-mark 0x1/0x1 --on-port 3389 --on-ip 127.0.0.1

# Mark RDP traffic intended for clients (2)
iptables -t mangle -A PREROUTING \
    -s $SERVER_IP \
    -m tcp -p tcp --sport 3389 \
    -j MARK --set-mark $MARK

# Set route lookup to the pyrdp table for marked packets.
ip rule add fwmark $MARK lookup $TABLE_ID

# Add a custom route that redirects traffic intended for the outside world to loopback
# So that server-client traffic passes through PyRDP
# This table will only ever be used by RDP so it should not be problematic
ip route add local default dev lo table $TABLE_ID

# If you want the interception to happen at the Layer 2
# Make sure that both your interfaces are bridged together

# WARNING: if you have other important firewall rules, make sure to test the impact of this
modprobe br_netfilter
echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables

# Target DROP in brouting means out of L2 go route in L3
ebtables -t broute -A BROUTING -i enp0s3 -p ipv4 --ip-dst $SERVER_IP --ip-proto tcp --ip-dport 3389 -j redirect --redirect-target DROP
ebtables -t broute -A BROUTING -i enp0s8 -p ipv4 --ip-src $SERVER_IP --ip-proto tcp --ip-source-port 3389 -j redirect --redirect-target DROP
# recent linux like debian buster
#ebtables-legacy -t broute -A BROUTING -i enp0s8 -p ipv4 --ip-src $SERVER_IP --ip-proto tcp --ip-source-port 3389 -j redirect --redirect-target DROP

# Disable return path filtering
echo 0 > /proc/sys/net/ipv4/conf/default/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/enp0s3/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/enp0s8/rp_filter

# make work w/o a local IP on br0
# WARNING: unclear if required on not
echo 1 > /proc/sys/net/ipv4/conf/br0/route_localnet
echo 1 > /proc/sys/net/ipv4/conf/enp0s3/route_localnet

# marking socket packets
# WARNING: unclear if required or not but probably good for performance so it will likely stay
iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT
iptables -t mangle -I PREROUTING -p tcp -m socket -j DIVERT

cleanup.sh: left as an exercise to the reader

Option 1: Add an interface in networks of interests for ARP resolution

In our lab setup, doing something like below is enough to make everything work because when the machine needs the L2 address to reply to a poisoned answer, it has the means to look it up by itself. The alternative is ARP pinning described in option 2.

ip addr add 10.13.37.11/24 dev br0

Option 2: ARP Pinning

ARP pin gateways or machines that need to talk to each other. The thought process is: if I'm impersonating 10.13.37.111 and the gateway is 10.13.37.10 then I need to be able to communicate with both on layer 2 on br0 if I don't have his MAC I need to look it up. If I don't have an IP in that segment then I can't.

arp -i br0 -s 10.13.37.10 08:00:27:59:05:fe

This requires a route as well:

ip route add 10.13.37.0/24 dev br0

Future work: Remote out-of-band deployments without a console

I want to be able to have a default route on br0 for the stuff we are intercepting and absolutely avoid using it for management traffic. I also want the separate management interface of the interception device to have a default route for Internet access. This will require policy routing which I haven't done yet.

References

obilodeau commented 4 years ago

TProxy implementation with ARP pinned gateway and out of band management

I managed to completely isolate the bridge from the rest of the host using network namespaces. The beauty of the network namespace implementation is that there is only one process who has the ability to interact with the bridge and it is pyrdp. This is guaranteed by the kernel-level namespace isolation.

The MITM machine has:

Additional information required for the attack that is used in the instructions:

Host

Setup the host with its management interface and default route as you would like to use. Don't touch the interception interfaces.

MITM Network Namespace

# creates the namespace
ip netns add mitm
# gives the interfaces to the namespace, they will disappear from the host and stop working so make sure you have proper access before doing so
ip link set dev enp0s3 netns mitm
ip link set dev enp0s8 netns mitm

# get a bash shell that is inside the namespace, configure the rest from there
ip netns exec mitm /bin/bash

# enable the loopback in the namespace (off by default, required for routing)
ip link set dev lo up
# bridge setup
ip link add name br0 type bridge
ip link set br0 up
ip link set enp0s3 up
ip link set enp0s8 up
ip link set enp0s3 master br0
ip link set enp0s8 master br0
# forward delay to 0
brctl setfd br0 0

Still inside the namespace, run setup.sh from the previous comment as-is (pasted again here for usability):

#!/bin/bash -x

# The IP of the RDP server which will receive proxied connections.
SERVER_IP=10.13.37.111

# The mark number to use in iptables (should be fine as-is)
MARK=1

# The routing table ID for custom rules (should be fine as-is)
TABLE_ID=100

# Create a custom routing table for pyrdp traffic
echo "$TABLE_ID    pyrdp" >> /etc/iproute2/rt_tables

# this can redirect to localhost now (1)
iptables -t mangle -I PREROUTING -p tcp -d 10.13.37.111 --dport 3389 \
  -j TPROXY --tproxy-mark 0x1/0x1 --on-port 3389 --on-ip 127.0.0.1

# Mark RDP traffic intended for clients (2)
iptables -t mangle -A PREROUTING \
    -s $SERVER_IP \
    -m tcp -p tcp --sport 3389 \
    -j MARK --set-mark $MARK

# Set route lookup to the pyrdp table for marked packets.
ip rule add fwmark $MARK lookup $TABLE_ID

# Add a custom route that redirects traffic intended for the outside world to loopback
# So that server-client traffic passes through PyRDP
# This table will only ever be used by RDP so it should not be problematic
ip route add local default dev lo table $TABLE_ID

# If you want the interception to happen at the Layer 2
# Make sure that both your interfaces are bridged together

# WARNING: if you have other important firewall rules, make sure to test the impact of this
modprobe br_netfilter
echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables

# Target DROP in brouting means out of L2 go route in L3
ebtables -t broute -A BROUTING -i enp0s3 -p ipv4 --ip-dst $SERVER_IP --ip-proto tcp --ip-dport 3389 -j redirect --redirect-target DROP
ebtables -t broute -A BROUTING -i enp0s8 -p ipv4 --ip-src $SERVER_IP --ip-proto tcp --ip-source-port 3389 -j redirect --redirect-target DROP
# recent linux like debian buster
#ebtables-legacy -t broute -A BROUTING -i enp0s8 -p ipv4 --ip-src $SERVER_IP --ip-proto tcp --ip-source-port 3389 -j redirect --redirect-target DROP

# Disable return path filtering
echo 0 > /proc/sys/net/ipv4/conf/default/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/enp0s3/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/enp0s8/rp_filter

# make work w/o a local IP on br0
# WARNING: unclear if required on not
echo 1 > /proc/sys/net/ipv4/conf/br0/route_localnet
echo 1 > /proc/sys/net/ipv4/conf/enp0s3/route_localnet

# marking socket packets
# WARNING: unclear if required or not but probably good for performance so it will likely stay
iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT
iptables -t mangle -I PREROUTING -p tcp -m socket -j DIVERT

Still inside the namespace, run this:

ip route add 10.13.37.0/24 dev br0
ip route add default via 10.13.37.10 dev br0
arp -i br0 -s 10.13.37.10 08:00:27:59:05:fe

Lastly, still inside the namespace, start pyrdp with:

pyrdp-mitm.py --transparent 10.13.37.111

You will see that the network namespace completely isolates access to the network interfaces, routing tables, iptables/ebtables, etc. From the global namespace you don't see the bridge and from the mitm namespace you don't see the management interface.

References

alxbl commented 4 years ago

Did not know about net namespaces, this is perfect.