Nebula - Level11 - Content Injection
About
The /home/flag11/flag11 binary processes standard input and executes a
shell command.
There are two ways of completing this level, you may wish to do both :-)
To do this level, log in as the level11 account with the password
level11. Files for this level can be found in /home/flag11.
Source code
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
/*
* Return a random, non predictable file, and return the file descriptor for it.
*/
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;
}
void process(char *buffer, int length)
{
unsigned int key;
int i;
key = length & 0xff;
for(i = 0; i < length; i++) {
buffer[i] ^= key;
key -= buffer[i];
}
system(buffer);
}
#define CL "Content-Length: "
int main(int argc, char **argv)
{
char line[256];
char buf[1024];
char *mem;
int length;
int fd;
char *path;
if(fgets(line, sizeof(line), stdin) == NULL) {
errx(1, "reading from stdin");
}
if(strncmp(line, CL, strlen(CL)) != 0) {
errx(1, "invalid header");
}
length = atoi(line + strlen(CL));
if(length < sizeof(buf)) {
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
process(buf, length);
} else {
int blue = length;
int pink;
fd = getrand(&path);
while(blue > 0) {
printf("blue = %d, length = %d, ", blue, length);
pink = fread(buf, 1, sizeof(buf), stdin);
printf("pink = %d\n", pink);
if(pink <= 0) {
err(1, "fread fail(blue = %d, length = %d)", blue, length);
}
write(fd, buf, pink);
blue -= pink;
}
mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(mem == MAP_FAILED) {
err(1, "mmap");
}
process(mem, length);
}
}
Solution
The first path occurs when the content length == 1 as seen by the statement
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
The 1 specifies that 1 element should be read into memory (see man fread). If there is not 1
element to read, then the program reports an error and exits. The process() function call then
proceeds to mangle the user input into a predictable different character. For instance, D always
becomes E. The function then passes this new character to system() via a c-style buffer, which
attempts to start a process using the executable path given to it. Recall that c-style buffers
terminate with a NULL or \0 character.
To exploit this behavior, create a symbolic link to /bin/getflag (ln -s /bin/getflag E) that
the program executes with properly formatted input. The lines
#define CL "Content-Length: "
if(fgets(line, sizeof(line), stdin) == NULL) {
errx(1, "reading from stdin");
}
if(strncmp(line, CL, strlen(CL)) != 0) {
errx(1, "invalid header");
}
check that there is input in stdin. If there is, the program next checks that the header matches
CL, or “Content-Length: “.
To craft the payload, echo -ne "Content-Length: 1\nD'" outputs the
correct sequence for the link file E. To hope for a random, null-terminated buffer to occur (it
happens), use while true; do echo -ne "Content-Length: 1\nD'" | /home/flag11/flag11 2>>output; done. The
2>>output portion redirects stderr to output, thereby allowing only the successful system
calls to appear in the terminal. If sh: E: command not found appears in the output file,
modify the PATH variable via PATH=$PATH:/home/level11.
Note that like previous solutions, this requires the disk to be mounted WITHOUT the nosuid option
specified.