3

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
Ralph Ritoch
  • 3,260
  • 27
  • 37
  • 1
    I think the problem here is that `realpath()` expects the path to actually exist, since it also advertises that it resolves symbolic links, which it couldn't possibly do if it merely manipulated strings. – Emmet Mar 13 '14 at 14:38
  • @Emmet, you are right. So what should I be using instead? – Ralph Ritoch Mar 13 '14 at 14:42
  • http://stackoverflow.com/questions/11034002/how-to-get-absolute-path-of-file-or-directory-that-does-not-exist Perform string manipulation by yourself. – timrau Mar 13 '14 at 14:49
  • 1
    @RalphRitoch: check out my answer below. It doesn't take much longer to code a working sketch than it would to explain it. – Emmet Mar 13 '14 at 16:57

3 Answers3

2

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.

Community
  • 1
  • 1
timrau
  • 22,578
  • 4
  • 51
  • 64
  • This is helpful, but only reveals that realpath() can't be used to resolves files which don't exist. It doesn't solve the problem of converting a relative path to an absolute path when the file doesn't exist. – Ralph Ritoch Mar 13 '14 at 14:41
  • @RalphRitoch _"only execute ruby scripts located in a restricted folder"_ — it sounds like if the file doesn't exist the point is moot so I think `realpath()` would have worked, unless you have many sub-directories and as a result the path is more than `PATH_MAX` characters (1024 in current versions if I'm not mistaken). If `realpath()` did not return the correct full filename, then you may have had another issue. The problem with parsing a path manually is that you totally ignore possible softlinks. – Alexis Wilke Mar 21 '22 at 16:20
1

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;
}
Emmet
  • 6,192
  • 26
  • 39
  • I like this logical approach to the problem. The hack I'm using now is virtually unreadable. I'll post it for archival purposes though. – Ralph Ritoch Mar 13 '14 at 20:39
0

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;
}
Ralph Ritoch
  • 3,260
  • 27
  • 37