Netfilter - Part 2

CTF : Mars@Hack 2026  ·  Catégorie : Réseau / Système  ·  Difficulté : Easy

« Et voilà, mes règles iptables sont béton, plus personne ne peut pénétrer ce réseau. Je suis tranquille. »

l'admin, convaincu d'avoir pensé à tout

Après avoir obtenu un accès bind shell via le port knocking de la Partie 1, me voilà connecté en tant qu'user1 sur la machine cible. Les règles iptables IPv4 étaient effectivement solides. Mais l'admin a oublié quelque chose de fondamental : IPv6 existe.

Ninja sneaking past
IPv6 : le couloir de service que tout le monde oublie de fermer.

1 - Situation initiale

On reprend depuis le bind shell obtenu en Partie 1. Première réflexe : chercher une élévation de privilèges locale. On passe en revue les classiques.

$ sudo -l
Sorry, user user1 may not run sudo on this host.

$ find / -perm -4000 -type f 2>/dev/null
/usr/bin/mount
/usr/bin/umount
/usr/bin/su
/usr/bin/newgrp
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/passwd

$ cat /etc/crontab
# /etc/crontab: system-wide crontab
# (rien d'intéressant)

SUID banals, pas de cron exploitable, capabilities nulles. La voie locale est un cul-de-sac. Le challenge s'appelle Netfilter, pas "PrivEsc" : la solution est ailleurs. On pivote vers l'énumération réseau.

2 - Découverte de la topologie réseau

On liste les interfaces réseau de la machine compromise :

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
    inet 127.0.0.1/8 scope host lo
    inet6 ::1/128 scope host

2: eth0@if283: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    inet 172.30.100.30/24 scope global eth0
    inet6 fe80::1/64 scope link

285: eth1@if284: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    inet 192.168.6.2/24 scope global eth1
    inet6 fd00:cafe::2/64 scope global
    inet6 fe80::cce2:fdff:fe8b:fe08/64 scope link

La machine est multihomed : deux interfaces, deux réseaux distincts. eth0 est le réseau que l'on connaissait, côté challenge. eth1 est une interface interne avec un sous-réseau 192.168.6.0/24 et une adresse IPv6 en fd00:cafe::2/64.

Le préfixe fd00::/8 est une plage Unique Local Address (ULA), l'équivalent IPv6 du RFC 1918 (192.168.x.x, 10.x.x.x). Une adresse statique, configurée à la main par l'admin, sur un réseau interne. Intéressant.

On consulte la table de voisinage NDP (l'équivalent IPv6 de l'ARP) pour savoir si d'autres hôtes communiquent sur ce segment :

$ ip neigh
192.168.5.1    dev eth0  lladdr 2a:2b:c5:c1:41:02  REACHABLE
fd00:cafe::dead:14a1 dev eth1  lladdr 4a:56:15:9e:51:4a  STALE
fe80::4856:15ff:fe9e:514a dev eth1  lladdr 4a:56:15:9e:51:4a  STALE

Un second hôte répond sur eth1, à l'adresse fd00:cafe::dead:14a1. L'adresse IPv6 choisie par l'admin (dead:14a1) est volontairement mémorable, ce qui confirme qu'elle est intentionnelle et fait partie du challenge.

3 - Le talon d'Achille : ip6tables non configuré

Avant d'attaquer le second hôte, on vérifie l'état du filtrage IPv6 sur la machine compromise. La Partie 1 nous avait montré un filtrage IPv4 très sophistiqué via iptables. Qu'en est-il d'ip6tables ?

$ ip6tables -L -n 2>/dev/null || ip6tables-save 2>/dev/null
# Generated by ip6tables-save v1.8.9
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT

Voilà le problème. Trois règles, toutes en ACCEPT. Aucun filtrage IPv6 d'aucune sorte. Pendant que l'admin passait des heures à sculpter ses 15 chaînes iptables avec TTL magique et SYN+URG, il n'a pas écrit une seule règle ip6tables.

En IPv4 : forteresse imprenable. En IPv6 : porte grande ouverte. C'est la leçon centrale de ce challenge : iptables et ip6tables sont deux daemons totalement indépendants. Une règle IPv4 ne s'applique jamais à du trafic IPv6, et inversement.

4 - Reconnaissance sur la cible IPv6

On sonde les ports ouverts sur fd00:cafe::dead:14a1. Pas de Nmap disponible sur le conteneur, on fait avec les outils natifs :

$ for port in 22 80 443 8080 3000 5000; do
    nc -z -w 1 -6 fd00:cafe::dead:14a1 $port 2>/dev/null && echo "port $port ouvert"
  done
port 80 ouvert

Un serveur HTTP sur le port 80. On vérifie en direct avec curl. La notation IPv6 dans une URL requiert des crochets autour de l'adresse (RFC 2732) :

$ curl -s http://[fd00:cafe::dead:14a1]/
<!DOCTYPE html>
<html>
  <head><title>Netfilter</title></head>
  <body>
    <h1>CTF Mars@Hack 2026</h1>
    <p>Encore un qui a oublié de filtrer IPv6 ... :-/</p>
    <p>Flag: <strong>fl@g{D0Nn0tF0rg3tT0Fi1t3rIPv6}</strong></p>
  </body>
</html>

Le serveur lui-même résume le challenge. « Encore un qui a oublié de filtrer IPv6 ». Succinct.

Access Granted
Un curl. Littéralement un seul curl.

Ce qu'on retient

  • iptables et ip6tables sont indépendants. Configurer l'un ne protège pas l'autre. Un pare-feu IPv4 parfait avec ip6tables vide revient à barricader la porte principale en laissant la fenêtre ouverte.
  • ip neigh (NDP) remplace ARP en IPv6. C'est le premier réflexe pour cartographier les hôtes actifs sur un segment interne en IPv6.
  • Les adresses ULA (fd00::/8) sont routables en interne. Elles ne sortent pas sur internet mais permettent des pivots entre conteneurs ou VMs sur le même hôte physique.
  • La notation URL IPv6 exige des crochets : http://[adresse_ipv6]/ est la forme correcte (RFC 2732). Sans les crochets, curl interprète les deux-points comme séparateur de port.

Flag

fl@g{D0Nn0tF0rg3tT0Fi1t3rIPv6}

Fin de la série Netfilter. La Partie 1 reste la plus technique des deux : 15 chaînes iptables, un TTL magique à 42, et un token bucket à vider. La Partie 2 enseigne une leçon plus simple, mais souvent oubliée en production.