add some boilerplate scripts
[hband-tools.git] / ssh-groupcommand / sh.ssh.c
blobef1d3844dffb2f76ac08d27b94a097d87325ba6a
2 #include <malloc.h>
3 #include <stdlib.h>
4 #include <unistd.h>
5 #include <err.h>
6 #include <errno.h>
7 #include <string.h>
8 #include <sys/types.h>
9 #include <sys/stat.h>
10 #include <pwd.h>
11 #include <grp.h>
12 #include <stdio.h>
13 #include <linux/limits.h>
15 #include "libmallocab.h"
16 #include "libstrtokdup.h"
18 #define FREE(p) {if(p!=NULL){free(p);p=NULL;}}while(0)
19 typedef int boolean;
20 #define FALSE 0
21 #define TRUE !FALSE
23 #define MSG_NOSHELL "Not allowed to run interactive shell."
24 #define MSG_NOCMDLN "Not allowed to run requested command line: %s"
25 #define MSG_NOWHATE "Not allowed to run whatever you want."
28 #define CONFFILE "/etc/ssh/AllowGroupCommands"
29 #define PARENTSFILE "/etc/ssh/AllowGroupStrictParents"
33 #ifdef DEBUG
34 #define PRINTDEBUG(...) {warnx(__VA_ARGS__);}while(0)
35 #else
36 #define PRINTDEBUG(...) {}
37 #endif
39 boolean EQ(const char* a, const char* b)
41 if(a==NULL || b==NULL || strcmp(a,b) != 0) return FALSE;
42 return TRUE;
45 /* Trims the given string's last char if matches. */
46 boolean trimtrail(char* s, char t)
48 if(s[strlen(s)-1] == t) {
49 s[strlen(s)-1] = '\0';
50 return TRUE;
52 return FALSE;
55 boolean gid_in_array(gid_t gid, gid_t* array, unsigned int size)
57 unsigned int i;
58 for(i = 0; i < size; i++)
60 if(array[i] == gid) return TRUE;
62 return FALSE;
65 boolean match(const char* str_a, const char* str_b)
67 if(EQ(str_a, str_b)) return TRUE;
68 if(EQ(str_a, "*")) return TRUE;
69 return FALSE;
72 /* This macro calls parse_option() and continues next iteration in the loop if succeeds. */
73 #define PARSE_OPT_BOOL(x,y,z) if(parse_option_bool(x,y,z)){continue;}
74 boolean parse_option_bool(const char* configline, const char* option, boolean* variable)
76 char* token1;
77 boolean found = FALSE;
78 token1 = strtokdup(configline, 1);
79 if(EQ(token1, option))
81 char* token2;
82 found = TRUE;
83 token2 = strtokdup(configline, 2);
84 if(EQ(token2, "off"))
86 PRINTDEBUG("unset option %s", option+1);
87 *variable = FALSE;
89 else if(EQ(token2, "on"))
91 *variable = TRUE;
92 PRINTDEBUG("set option %s", option+1);
94 FREE(token2);
96 FREE(token1);
97 return found;
100 char* get_real_comm(char* argv0, char** real_argv0)
102 char* cp;
103 char* real_comm;
105 cp = strrchr(argv0, '/');
106 if(cp == NULL)
108 /* Take our basename. */
109 *real_argv0 = argv0;
111 else
113 /* Point to our basename. */
114 *real_argv0 = cp+1;
116 /* Strip everything after the last dot, if there is one. */
117 cp = *real_argv0;
118 if(strrchr(cp, '.') != NULL)
120 *real_argv0 = strndupab(cp, strrchr(cp, '.') - cp);
122 /* If our name starts with a dash, let the command do not. */
123 if((*real_argv0)[0] == '-')
125 real_comm = (*real_argv0)+1;
127 else
129 real_comm = *real_argv0;
131 return real_comm;
135 int main(int argc, char** argv, char** envp)
137 char* real_comm;
138 char** real_argv;
139 struct passwd * pwent;
140 struct group * grent;
141 gid_t* group_ids;
142 uid_t myuid;
143 int n_groups, n;
144 FILE* fh;
145 #define LNBUFLEN 4096
146 char lnbuf[LNBUFLEN];
147 char* fc_string = NULL;
148 char* sh_string = NULL;
149 char* cmdline = NULL;
150 boolean report_dotsshrc = FALSE;
151 boolean strip_quotes = FALSE;
152 pid_t parentpid;
153 char pathbuf[PATH_MAX];
154 char parentpath[PATH_MAX];
155 ssize_t pathlen;
156 char* cmd;
157 struct stat stat_buf;
159 /* Set how glibc responds when various kinds of programming errors are detected. */
160 /* bit 0: print error message */
161 /* bit 1: call abort(3) */
162 /* bit 2: error message be short */
163 mallopt(M_CHECK_ACTION, 7);
166 if(getenv("SHELL_DIRECTLY_BY_SSHD") != NULL)
168 unsetenv("SHELL_DIRECTLY_BY_SSHD");
169 goto controlled_mode;
172 /* Check parent process owner. */
173 myuid = getuid();
174 parentpid = getppid();
175 PRINTDEBUG("Parent PID: %d", parentpid);
176 sprintf(pathbuf, "/proc/%d", parentpid);
177 if(stat(pathbuf, &stat_buf)==0)
179 PRINTDEBUG("Parent UID: %u", stat_buf.st_uid);
180 if(stat_buf.st_uid != myuid)
182 /* Parent process run not by us */
183 goto controlled_mode;
186 else
188 /* Parent process run by root */
189 goto controlled_mode;
192 /* Check parent process command name. */
193 sprintf(pathbuf, "/proc/%d/exe", parentpid);
194 pathlen = readlink(pathbuf, parentpath, PATH_MAX-1);
195 if(pathlen <= -1)
197 /* Can not access parent process. */
198 PRINTDEBUG("Can not read parent exe path.");
200 else
202 /* readlink() does not append a null byte to buf. */
203 parentpath[pathlen] = '\0';
204 fh = fopen(PARENTSFILE, "r");
205 if(fh == NULL)
207 cmd = strrchr(parentpath, '/');
208 if(cmd == NULL)
210 /* Can not find parent command's basename. */
212 else
214 cmd++;
215 PRINTDEBUG("Parent command: %s", cmd);
216 if(EQ(cmd, "sshd") || EQ(cmd, "dropbear"))
218 /* We are called by an SSHd. */
220 else
222 /* We are not called by SSHd. Bypass access control. */
223 PRINTDEBUG("Permissive mode.");
224 real_comm = get_real_comm(argv[0], &argv[0]);
225 execvpe(real_comm, argv, envp);
226 warn("%s", argv[0]);
227 return 127;
231 else
233 /* Read the list of basename/path of possible parent processes. */
234 // TODO
238 controlled_mode:
239 PRINTDEBUG("Controlled mode.");
242 /* Gather group IDs the current user (process) is member of. */
243 n_groups = getgroups(0, NULL) + 1;
244 group_ids = mallocab(sizeof(gid_t*) * n_groups);
245 if(getgroups(n_groups - 1, group_ids) == -1)
247 errx(errno, "Can not get groups for uid %d.", myuid);
249 group_ids[n_groups - 1] = getegid();
250 PRINTDEBUG("Groups found: %d", n_groups);
253 fh = fopen(CONFFILE, "r");
254 if(fh == NULL)
256 warn("%s", CONFFILE);
258 else
260 while(TRUE)
262 /* Read a line. */
263 fgets(lnbuf, LNBUFLEN, fh);
264 if(feof(fh)) break;
265 trimtrail(lnbuf, '\n');
266 trimtrail(lnbuf, '\r');
268 /* Take the 1st word (ie. group name). */
269 fc_string = strtokdup(lnbuf, 1);
270 if(fc_string != NULL && fc_string[0] != '#')
272 PARSE_OPT_BOOL(lnbuf, "!report-dotsshrc", &report_dotsshrc);
273 PARSE_OPT_BOOL(lnbuf, "!strip-quotes", &strip_quotes);
275 grent = getgrnam(fc_string);
276 FREE(fc_string);
277 if(grent != NULL && gid_in_array(grent->gr_gid, group_ids, n_groups))
279 boolean allowed = TRUE;
280 boolean wildcarded = FALSE;
281 boolean invoke_shell;
282 int forced_opts_num = -1;
284 PRINTDEBUG("Reading entry: %s", lnbuf);
286 /* Current user is a member of this group. */
287 /* Check for forced shell options. (optional) */
288 for(n = 2; (fc_string = strtokdup(lnbuf, n)) != NULL; n++)
290 if(EQ(fc_string, "---"))
292 forced_opts_num = n - 2;
293 FREE(fc_string);
294 break;
296 FREE(fc_string);
298 if(forced_opts_num == -1) {
299 invoke_shell = FALSE;
300 } else {
301 invoke_shell = TRUE;
304 /* Assuming argv[1] is "-c". */
305 if(argc > 2) {
306 cmdline = argv[2];
307 } else {
308 cmdline = "";
310 /* Check if he wants to run an allowed command. */
311 for(n = 3 + forced_opts_num; (fc_string = strtokdup(lnbuf, n)) != NULL && (sh_string = strtokdup(cmdline, n - forced_opts_num - 2)) != NULL; n++)
313 if(EQ(fc_string, "**")) {
314 wildcarded = TRUE;
315 } else if(!match(fc_string, sh_string)) {
316 allowed = FALSE;
318 FREE(fc_string);
319 FREE(sh_string);
320 if(wildcarded || !allowed) break;
322 if(allowed && !wildcarded)
324 if((fc_string = strtokdup(lnbuf, n)) != NULL)
326 if(!EQ(fc_string, "**"))
328 /* More arguments were defined in config file than in command line. */
329 allowed = FALSE;
331 FREE(fc_string);
333 else if((sh_string = strtokdup(cmdline, n - forced_opts_num - 2)) != NULL)
335 /* More arguments were given in shell command than in config file. */
336 allowed = FALSE;
337 FREE(sh_string);
341 if(allowed)
343 fclose(fh);
344 real_argv = (char**) mallocab((argc+forced_opts_num+1) * sizeof(char*));
346 if(invoke_shell)
348 PRINTDEBUG("Invoking shell.");
350 /* Compute command name and 0th argument. */
351 real_comm = get_real_comm(argv[0], &real_argv[0]);
353 /* Append forced options to shell command. */
354 for(n = 1; n <= forced_opts_num; n++)
356 real_argv[n] = strtokdup(lnbuf, n+1);
358 /* Append commandline to shell command. */
359 if(argc > 1)
361 real_argv[n] = "-c";
362 real_argv[n+1] = cmdline;
363 real_argv[n+2] = NULL;
365 else
367 real_argv[n] = NULL;
369 /* Forget arguments after: bash -c "..." */
371 else
373 PRINTDEBUG("Not invoking shell.");
375 /* Add each words in shell command line as separated arguments. */
376 for(n = 0; (sh_string = strtokdup(cmdline, n+1)) != NULL; n++)
378 if(strip_quotes && strlen(sh_string)>1 && (sh_string[0]=='\'' && sh_string[strlen(sh_string)-1]=='\'') || (sh_string[0]=='"' && sh_string[strlen(sh_string)-1]=='"'))
380 /* Dummy quote-stripper. */
381 /* Strip single/double quotes, but no real interpolation. */
382 /* Leave 0th char untouched and refer it as quoting char. */
383 unsigned int shift = 0;
384 unsigned int pos;
385 for(pos = 1; sh_string[pos+shift+1] != '\0'; pos++)
387 PRINTDEBUG("unquote [%s]", sh_string);
388 PRINTDEBUG(" %*s%s%*s%s", pos, "", shift==0?"↕":"↓", shift==0?0:shift-1, "", shift==0?"":"↑");
389 if(sh_string[pos+shift] == '\\' && (sh_string[pos+shift+1] == sh_string[0] || sh_string[pos+shift+1] == '\\'))
391 shift++;
393 sh_string[pos] = sh_string[pos+shift];
394 PRINTDEBUG("unquote [%s]", sh_string);
396 sh_string[pos] = '\0';
397 PRINTDEBUG("unquoted [%s]", sh_string+1);
398 /* Unquoted string starts at char pos 1. */
399 real_argv[n] = (char*)sh_string+1;
401 else
403 real_argv[n] = sh_string;
406 real_argv[n] = NULL;
407 real_comm = real_argv[0];
410 PRINTDEBUG("[-]=%s", real_comm);
411 for(n=0; real_argv[n]!=NULL; n++)
412 PRINTDEBUG("[%d]=%s", n, real_argv[n]);
414 /* Finally call the desidered command. */
415 execvpe(real_comm, real_argv, envp);
416 warn("%s", real_argv[0]);
417 return 127;
422 fclose(fh);
425 if(report_dotsshrc==FALSE && argc > 2 && (sh_string = strtokdup(argv[2] /* assuming argv[1] is "-c" */, 2 /* assuming 1st token is "/bin/sh" */)) != NULL && EQ(sh_string, ".ssh/rc"))
427 /* Don't report that ".ssh/rc" is denied to run. */
429 else
431 /* Report that shell command is denied to run. */
432 if(cmdline == NULL)
434 if(argc > 2)
436 if(EQ(argv[1], "-c"))
437 warnx(MSG_NOCMDLN, argv[2]);
438 else
439 warnx(MSG_NOWHATE);
441 else
442 warnx(MSG_NOSHELL);
444 else
446 if(strlen(cmdline))
447 warnx(MSG_NOCMDLN, cmdline);
448 else
449 warnx(MSG_NOSHELL);
452 FREE(sh_string);
453 return 1;