Reputation: 3440
I am trying to make a suid application that will only execute ruby scripts located in a restricted folder. I have tried to do this using realpath(3) but it is only returning the first segment of the path. Below is my code...
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#define SUEXEC_STR_LEN 2048
#define RUBY_APP "/usr/bin/ruby"
#define DIRECTORY_SEPARATOR "/"
static void safepath(const char *path_in, char * path_out, int outlen) {
realpath(path_in, path_out);
}
int main ( int argc, char *argv[] )
{
char cmd[SUEXEC_STR_LEN];
char path_out[SUEXEC_STR_LEN];
char path_in[SUEXEC_STR_LEN];
char *cp = &cmd[0];
strncpy(cp, RUBY_APP, SUEXEC_STR_LEN - 1);
strncpy(path_in, DIRECTORY_SEPARATOR, SUEXEC_STR_LEN - 1);
strncat(path_in,argv[1],SUEXEC_STR_LEN - 1);
safepath(path_in,path_out,SUEXEC_STR_LEN - 1);
printf("path_in=%s path_out=%s\n",path_in,path_out);
setuid( 0 );
// system( cmd );
return 0;
}
This is an example of the result I'm getting
root@server01:/root/src# ./a.out foo/bar/../test
path_in=/foo/bar/../test path_out=/foo
This is the result I want
root@server01:/root/src# ./a.out foo/bar/../test
path_in=/foo/bar/../test path_out=/foo/test
Upvotes: 3
Views: 2732
Reputation: 3440
This is the code which I used as a solution to the problem. It may have some bugs remaining in it, and it isn't checking the outlen argument to avoid segfaults and other uglyness but it seems to get the job done.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <linux/limits.h>
#define SUEXEC_STR_LEN 2048
#define RUBY_APP "/usr/bin/ruby"
#define DIRECTORY_SEPARATOR "/"
#define RUBY_EXT ".rb"
#define SERVICES_BASE_PATH "/path/to/ruby/services"
static inline int isDirSeparator(const char c) { return (c == '/' || c == '\\'); }
static void safepath(const char *path_in, char * path_out, int outlen)
{
char *dirs[PATH_MAX];
int depth = 0;
char *dstptr = path_out;
const char *srcptr = path_in;
*dstptr++ = DIRECTORY_SEPARATOR[0];
dirs[0] = dstptr;
dirs[1] = NULL;
depth++;
while (1) {
if ((srcptr[0] == '.') && isDirSeparator(srcptr[1])) {
srcptr += 2;
} else if (srcptr[0] == '.' && srcptr[1] == '.' && isDirSeparator(srcptr[2])) {
if (depth > 1) {
dirs[depth] = NULL;
depth--;
dstptr = dirs[depth-1];
} else {
dstptr = dirs[0];
}
srcptr += 3;
} else if (srcptr[0] == '.' && srcptr[1] == '.' && srcptr[2] == 0) {
if (depth == 1) {
srcptr += 2;
} else {
depth--;
dstptr = dirs[depth-1];
srcptr += 2;
}
} else {
while (!isDirSeparator(srcptr[0]) && srcptr[0]) {
*dstptr++ = *srcptr++;
}
if (srcptr[0] == 0) {
if (dstptr != dirs[0] && isDirSeparator(dstptr[-1])) {
dstptr[-1] = 0;
}
dstptr[0] = 0;
return;
} else if (isDirSeparator(srcptr[0])) {
if (dstptr == dirs[0]) {
srcptr++;
} else {
*dstptr++ = *srcptr++;
dirs[depth] = dstptr;
depth++;
}
while (isDirSeparator(srcptr[0]) && srcptr[0]) {
srcptr++;
}
} else {
path_out[0] = 0;
return;
}
}
}
}
int main ( int argc, char *argv[] )
{
int ret;
char cmd[SUEXEC_STR_LEN];
char path_out[SUEXEC_STR_LEN];
char path_in[SUEXEC_STR_LEN];
char *cp = &cmd[0];
if (argc < 2) {
fprintf(stderr,"usage: %s <service>\n",argv[0]);
return 1;
}
strncpy(cp, RUBY_APP, SUEXEC_STR_LEN - 1);
strncpy(path_in, DIRECTORY_SEPARATOR, SUEXEC_STR_LEN - 1);
strncat(path_in,argv[1],SUEXEC_STR_LEN - 1);
safepath(path_in,path_out,SUEXEC_STR_LEN - 1);
//printf("path_in=%s path_out=%s\n",path_in,path_out);
strncat(cmd," ",SUEXEC_STR_LEN - (1+sizeof(RUBY_EXT)));
strncat(cmd,SERVICES_BASE_PATH,SUEXEC_STR_LEN - (1+sizeof(RUBY_EXT)));
strncat(cmd,path_out,SUEXEC_STR_LEN - (1+sizeof(RUBY_EXT)));
strncat(cmd,RUBY_EXT,SUEXEC_STR_LEN - 1);
setuid( 0 );
ret = system( cmd );
if (ret == -1) {
return ret;
}
ret = WEXITSTATUS(ret);
return ret;
}
Upvotes: 0
Reputation: 6401
So, here's a working sketch of how you might go about it in C on Linux. This is a quick hack that I do not represent as being exemplary code, efficient, etc. It (ab)uses PATH_MAX
, uses “bad” string functions, and may leak memory, eat your cat, and have corner cases that segfault, etc. When it breaks, you get to keep both parts.
The basic idea is to go through the given path, breaking it up into “words” using “/” as the delimiter. Then, go through the list, pushing the “words” onto a stack, but ignoring if empty or “.”, and popping if “..”, then serializing the stack by starting at the bottom and accumulating a string with slashes in between.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <linux/limits.h>
typedef struct stack_s {
char *data[PATH_MAX];
int top;
} stack_s;
void stack_push(stack_s *s, char *c) {
s->data[s->top++] = c;
}
char *stack_pop(stack_s *s) {
if( s->top <= 0 ) {
return NULL;
}
s->top--;
return s->data[s->top];
}
// DANGER! DANGER! Returns malloc()ed pointer that you must free()
char *stack_serialize(stack_s *s) {
int i;
char *buf;
int len=1;
for(i=0; i<s->top; i++) {
len += strlen(s->data[i]);
len++; // For a slash
}
buf = malloc(len);
*buf = '\0';
for(i=0; i<s->top-1; i++) {
strcat(buf, s->data[i]);
strcat(buf, "/");
}
strcat(buf, s->data[i]);
return buf;
}
// DANGER! DANGER! Returns malloc()ed pointer that you must free()
char *semicanonicalize(char *src) {
char *word[PATH_MAX] = {NULL};
int w=0;
int n_words;
char *buf;
int len;
char *p, *q;
stack_s dir_stack = {{NULL},0};
// Make a copy of the input string:
len = strlen(src);
buf = strdup(src);
// Replace slashes with NULs and record the start of each "word"
q = buf+len;
word[0]=buf;
for(p=buf,w=0; p<q; p++) {
if(*p=='/') {
*p = '\0';
word[++w] = p+1;
}
}
n_words=w+1;
// We push w[0] unconditionally to preserve slashes and dots at the
// start of the source path:
stack_push(&dir_stack, word[0]);
for(w=1; w<n_words; w++) {
len = strlen(word[w]);
if( len == 0 ) {
// Must've hit a double slash
continue;
}
if( *word[w] == '.' ) {
if( len == 1 ) {
// Must've hit a dot
continue;
}
if( len == 2 && *(word[w]+1)=='.' ) {
// Must've hit a '..'
(void)stack_pop(&dir_stack);
continue;
}
}
// If we get to here, the current "word" isn't "", ".", or "..", so
// we push it on the stack:
stack_push(&dir_stack, word[w]);
}
p = stack_serialize(&dir_stack);
free(buf);
return p;
}
int main(void)
{
char *in[] = { "/home/emmet/../foo//./bar/quux/../.",
"../home/emmet/../foo//./bar/quux/../.",
"./home/emmet/../foo//./bar/quux/../.",
"home/emmet/../foo//./bar/quux/../."
};
char *out;
for(int i=0; i<4; i++) {
out = semicanonicalize(in[i]);
printf("%s \t->\t %s\n", in[i], out);
free(out);
}
return 0;
}
Upvotes: 1
Reputation: 23058
You should check for realpath()
's return value. As described in its man page,
RETURN VALUE
If there is no error, realpath() returns a pointer to the resolved_path.Otherwise it returns a NULL pointer, and the contents of the array resolved_path are undefined. The global variable errno is set to indicate the error.
Also in ERRORS section of its man page,
ENOENT The named file does not exist.
Thus, if there is indeed no /foo/test
in your file system, realpath()
should return NULL
and the output is undefined.
Upvotes: 2