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)
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"
34 #define PRINTDEBUG(...) {warnx(__VA_ARGS__);}while(0)
36 #define PRINTDEBUG(...) {}
39 boolean
EQ(const char* a
, const char* b
)
41 if(a
==NULL
|| b
==NULL
|| strcmp(a
,b
) != 0) return FALSE
;
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';
55 boolean
gid_in_array(gid_t gid
, gid_t
* array
, unsigned int size
)
58 for(i
= 0; i
< size
; i
++)
60 if(array
[i
] == gid
) return TRUE
;
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
;
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
)
77 boolean found
= FALSE
;
78 token1
= strtokdup(configline
, 1);
79 if(EQ(token1
, option
))
83 token2
= strtokdup(configline
, 2);
86 PRINTDEBUG("unset option %s", option
+1);
89 else if(EQ(token2
, "on"))
92 PRINTDEBUG("set option %s", option
+1);
100 char* get_real_comm(char* argv0
, char** real_argv0
)
105 cp
= strrchr(argv0
, '/');
108 /* Take our basename. */
113 /* Point to our basename. */
116 /* Strip everything after the last dot, if there is one. */
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;
129 real_comm
= *real_argv0
;
135 int main(int argc
, char** argv
, char** envp
)
139 struct passwd
* pwent
;
140 struct group
* grent
;
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
;
153 char pathbuf
[PATH_MAX
];
154 char parentpath
[PATH_MAX
];
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. */
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
;
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);
197 /* Can not access parent process. */
198 PRINTDEBUG("Can not read parent exe path.");
202 /* readlink() does not append a null byte to buf. */
203 parentpath
[pathlen
] = '\0';
204 fh
= fopen(PARENTSFILE
, "r");
207 cmd
= strrchr(parentpath
, '/');
210 /* Can not find parent command's basename. */
215 PRINTDEBUG("Parent command: %s", cmd
);
216 if(EQ(cmd
, "sshd") || EQ(cmd
, "dropbear"))
218 /* We are called by an SSHd. */
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
);
233 /* Read the list of basename/path of possible parent processes. */
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");
256 warn("%s", CONFFILE
);
263 fgets(lnbuf
, LNBUFLEN
, fh
);
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
);
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;
298 if(forced_opts_num
== -1) {
299 invoke_shell
= FALSE
;
304 /* Assuming argv[1] is "-c". */
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
, "**")) {
315 } else if(!match(fc_string
, 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. */
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. */
344 real_argv
= (char**) mallocab((argc
+forced_opts_num
+1) * sizeof(char*));
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. */
362 real_argv
[n
+1] = cmdline
;
363 real_argv
[n
+2] = NULL
;
369 /* Forget arguments after: bash -c "..." */
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;
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] == '\\'))
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;
403 real_argv
[n
] = sh_string
;
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]);
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. */
431 /* Report that shell command is denied to run. */
436 if(EQ(argv
[1], "-c"))
437 warnx(MSG_NOCMDLN
, argv
[2]);
447 warnx(MSG_NOCMDLN
, cmdline
);