Skip to content

Commit

Permalink
cgi-io: implement exec action
Browse files Browse the repository at this point in the history
Implement a new "cgi-exec" applet which allows to invoke remote commands
and stream their stdandard output back to the client via HTTP. This is
needed in cases where large amounts of data or binary encoded contents
such as tar archives need to be transferred, which are unsuitable to be
transported via ubus directly.

The exec call is guarded by the same ACL semantics as rpcd's file plugin,
means in order to be able to execute a command remotely, the ubus session
identified by the given session ID must have read access to the "exec"
function of the "cgi-io" scope and an explicit "exec" permission rule for
the invoked command in the "file" scope.

In order to initiate a transfer, a POST request in x-www-form-urlencoded
format must be sent to the applet, with one field "sessionid" holding
the login session and another field "command" specifiying the commandline
to invoke.

Further optional fields are "filename" which - if present - will cause
the download applet to set a Content-Dispostition header and "mimetype"
which allows to let the applet respond with a specific type instead of
the default "application/octet-stream".

Below is an example for the required ACL rules to grant exec access to
both the "date" and "iptables" commands. The "date" rule specifies the
base name of the executable and thus allows invocation with arbitrary
parameters while the latter "iptables" rule merely allows one specific
set of arguments which must appear exactly in the given order.

    ubus call session grant '{
        "ubus_rpc_session": "...",
        "scope": "cgi-io",
        "objects": [
            [ "exec", "read" ]
        ]
    }'

    ubus call session grant '{
        "ubus_rpc_session": "...",
        "scope": "file",
        "objects": [
            [ "/bin/date", "exec" ],
            [ "/usr/sbin/iptables -n -v -L", "exec" ]
        ]
   }'

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
  • Loading branch information
jow- authored and blogic committed Dec 13, 2019
1 parent 1dfec06 commit bd04a5d
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 1 deletion.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
include $(TOPDIR)/rules.mk

PKG_NAME:=cgi-io
PKG_RELEASE:=14
PKG_RELEASE:=15

PKG_LICENSE:=GPL-2.0-or-later

Expand Down Expand Up @@ -40,6 +40,7 @@ define Package/cgi-io/install
$(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-upload
$(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-download
$(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-backup
$(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-exec
endef

$(eval $(call BuildPackage,cgi-io))
233 changes: 233 additions & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,237 @@ main_backup(int argc, char **argv)
}
}


static const char *
lookup_executable(const char *cmd)
{
size_t plen = 0, clen = strlen(cmd) + 1;
static char path[PATH_MAX];
char *search, *p;
struct stat s;

if (!stat(cmd, &s) && S_ISREG(s.st_mode))
return cmd;

search = getenv("PATH");

if (!search)
search = "/bin:/usr/bin:/sbin:/usr/sbin";

p = search;

do {
if (*p != ':' && *p != '\0')
continue;

plen = p - search;

if ((plen + clen) >= sizeof(path))
continue;

strncpy(path, search, plen);
sprintf(path + plen, "/%s", cmd);

if (!stat(path, &s) && S_ISREG(s.st_mode))
return path;

search = p + 1;
} while (*p++);

return NULL;
}

static char **
parse_command(const char *cmdline)
{
const char *p = cmdline, *s;
char **argv = NULL, *out;
size_t arglen = 0;
int argnum = 0;
bool esc;

while (isspace(*cmdline))
cmdline++;

for (p = cmdline, s = p, esc = false; p; p++) {
if (esc) {
esc = false;
}
else if (*p == '\\' && p[1] != 0) {
esc = true;
}
else if (isspace(*p) || *p == 0) {
if (p > s) {
argnum += 1;
arglen += sizeof(char *) + (p - s) + 1;
}

s = p + 1;
}

if (*p == 0)
break;
}

if (arglen == 0)
return NULL;

argv = calloc(1, arglen + sizeof(char *));

if (!argv)
return NULL;

out = (char *)argv + sizeof(char *) * (argnum + 1);
argv[0] = out;

for (p = cmdline, s = p, esc = false, argnum = 0; p; p++) {
if (esc) {
esc = false;
*out++ = *p;
}
else if (*p == '\\' && p[1] != 0) {
esc = true;
}
else if (isspace(*p) || *p == 0) {
if (p > s) {
*out++ = ' ';
argv[++argnum] = out;
}

s = p + 1;
}
else {
*out++ = *p;
}

if (*p == 0)
break;
}

argv[argnum] = NULL;
out[-1] = 0;

return argv;
}

static int
main_exec(int argc, char **argv)
{
char *fields[] = { "sessionid", NULL, "command", NULL, "filename", NULL, "mimetype", NULL };
int i, devnull, status, fds[2];
bool allowed = false;
ssize_t len = 0;
const char *exe;
char *p, **args;
pid_t pid;

postdecode(fields, 4);

if (!fields[1] || !session_access(fields[1], "cgi-io", "exec", "read"))
return failure(403, 0, "Exec permission denied");

for (p = fields[5]; p && *p; p++)
if (!isalnum(*p) && !strchr(" ()<>@,;:[]?.=%-", *p))
return failure(400, 0, "Invalid characters in filename");

for (p = fields[7]; p && *p; p++)
if (!isalnum(*p) && !strchr(" .;=/-", *p))
return failure(400, 0, "Invalid characters in mimetype");

args = fields[3] ? parse_command(fields[3]) : NULL;

if (!args)
return failure(400, 0, "Invalid command parameter");

/* First check if we find an ACL match for the whole cmdline ... */
allowed = session_access(fields[1], "file", args[0], "exec");

/* Now split the command vector... */
for (i = 1; args[i]; i++)
args[i][-1] = 0;

/* Find executable... */
exe = lookup_executable(args[0]);

if (!exe) {
free(args);
return failure(404, 0, "Executable not found");
}

/* If there was no ACL match, check for a match on the executable */
if (!allowed && !session_access(fields[1], "file", exe, "exec")) {
free(args);
return failure(403, 0, "Access to command denied by ACL");
}

if (pipe(fds)) {
free(args);
return failure(500, errno, "Failed to spawn pipe");
}

switch ((pid = fork()))
{
case -1:
free(args);
close(fds[0]);
close(fds[1]);
return failure(500, errno, "Failed to fork process");

case 0:
devnull = open("/dev/null", O_RDWR);

if (devnull > -1) {
dup2(devnull, 0);
dup2(devnull, 2);
close(devnull);
}
else {
close(0);
close(2);
}

dup2(fds[1], 1);
close(fds[0]);
close(fds[1]);

if (chdir("/") < 0) {
free(args);
return failure(500, errno, "Failed chdir('/')");
}

if (execv(exe, args) < 0) {
free(args);
return failure(500, errno, "Failed execv(...)");
}

return -1;

default:
printf("Status: 200 OK\r\n");
printf("Content-Type: %s\r\n",
fields[7] ? fields[7] : "application/octet-stream");

if (fields[5])
printf("Content-Disposition: attachment; filename=\"%s\"\r\n",
fields[5]);

printf("\r\n");
fflush(stdout);

do {
len = splice(fds[0], NULL, 1, NULL, READ_BLOCK, SPLICE_F_MORE);
} while (len > 0);

waitpid(pid, &status, 0);

close(fds[0]);
close(fds[1]);
free(args);

return 0;
}
}

int main(int argc, char **argv)
{
if (strstr(argv[0], "cgi-upload"))
Expand All @@ -812,6 +1043,8 @@ int main(int argc, char **argv)
return main_download(argc, argv);
else if (strstr(argv[0], "cgi-backup"))
return main_backup(argc, argv);
else if (strstr(argv[0], "cgi-exec"))
return main_exec(argc, argv);

return -1;
}

0 comments on commit bd04a5d

Please sign in to comment.