1 /*
2  * SPDX-License-Identifier: ISC
3  *
4  * Copyright (c) 2008, 2010-2018, 2020-2021 Todd C. Miller <Todd.Miller@sudo.ws>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 
19 /*
20  * This is an open source non-commercial project. Dear PVS-Studio, please check it.
21  * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
22  */
23 
24 #include <config.h>
25 
26 #include <sys/stat.h>
27 #include <errno.h>
28 #include <fcntl.h>
29 #include <limits.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <signal.h>
34 #include <time.h>
35 #include <unistd.h>
36 #ifdef HAVE_STDBOOL_H
37 # include <stdbool.h>
38 #else
39 # include "compat/stdbool.h"
40 #endif /* HAVE_STDBOOL_H */
41 
42 #include "sudo.h"
43 #include "sudo_exec.h"
44 #include "sudo_edit.h"
45 
46 sudo_dso_public int main(int argc, char *argv[], char *envp[]);
47 
48 static int sesh_sudoedit(int argc, char *argv[]);
49 
50 /*
51  * Exit codes defined in sudo_exec.h:
52  *  SESH_SUCCESS (0)         ... successful operation
53  *  SESH_ERR_FAILURE (1)     ... unspecified error
54  *  SESH_ERR_INVALID (30)    ... invalid -e arg value
55  *  SESH_ERR_BAD_PATHS (31)  ... odd number of paths
56  *  SESH_ERR_NO_FILES (32)   ... copy error, no files copied
57  *  SESH_ERR_SOME_FILES (33) ... copy error, no files copied
58  */
59 int
main(int argc,char * argv[],char * envp[])60 main(int argc, char *argv[], char *envp[])
61 {
62     int ret;
63     debug_decl(main, SUDO_DEBUG_MAIN);
64 
65     initprogname(argc > 0 ? argv[0] : "sesh");
66 
67     setlocale(LC_ALL, "");
68     bindtextdomain(PACKAGE_NAME, LOCALEDIR);
69     textdomain(PACKAGE_NAME);
70 
71     if (argc < 2)
72 	sudo_fatalx("%s", U_("requires at least one argument"));
73 
74     /* Read sudo.conf and initialize the debug subsystem. */
75     if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1)
76 	exit(EXIT_FAILURE);
77     sudo_debug_register(getprogname(), NULL, NULL,
78 	sudo_conf_debug_files(getprogname()), -1);
79 
80     if (strcmp(argv[1], "-e") == 0) {
81 	ret = sesh_sudoedit(argc, argv);
82     } else {
83 	bool login_shell;
84 	char *cp, *cmnd;
85 	int flags = 0;
86 	int fd = -1;
87 
88 	/* If the first char of argv[0] is '-', we are running a login shell. */
89 	login_shell = argv[0][0] == '-';
90 
91 	/* If argv[0] ends in -noexec, pass the flag to sudo_execve() */
92 	if ((cp = strrchr(argv[0], '-')) != NULL && cp != argv[0]) {
93 	    if (strcmp(cp, "-noexec") == 0)
94 		SET(flags, CD_NOEXEC);
95 	}
96 
97 	/* If argv[1] is --execfd=%d, extract the fd to exec with. */
98 	if (strncmp(argv[1], "--execfd=", 9) == 0) {
99 	    const char *errstr;
100 
101 	    cp = argv[1] + 9;
102 	    fd = sudo_strtonum(cp, 0, INT_MAX, &errstr);
103 	    if (errstr != NULL)
104 		sudo_fatalx(U_("invalid file descriptor number: %s"), cp);
105 	    argv++;
106 	    argc--;
107 	}
108 
109 	/* Shift argv and make a copy of the command to execute. */
110 	argv++;
111 	argc--;
112 	if ((cmnd = strdup(argv[0])) == NULL)
113 	    sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
114 
115 	/* If invoked as a login shell, modify argv[0] accordingly. */
116 	if (login_shell) {
117 	    if ((cp = strrchr(argv[0], '/')) == NULL)
118 		sudo_fatal(U_("unable to run %s as a login shell"), argv[0]);
119 	    *cp = '-';
120 	    argv[0] = cp;
121 	}
122 	sudo_execve(fd, cmnd, argv, envp, -1, flags);
123 	sudo_warn(U_("unable to execute %s"), cmnd);
124 	ret = SESH_ERR_FAILURE;
125     }
126     sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, ret);
127     _exit(ret);
128 }
129 
130 /*
131  * Destructively parse a string in the format:
132  *  uid:gid:groups,...
133  *
134  * On success, fills in ud and returns true, else false.
135  */
136 static bool
parse_user(char * userstr,struct sudo_cred * cred)137 parse_user(char *userstr, struct sudo_cred *cred)
138 {
139     char *cp, *ep;
140     const char *errstr;
141     debug_decl(parse_user, SUDO_DEBUG_EDIT);
142 
143     /* UID */
144     cp = userstr;
145     if ((ep = strchr(cp, ':')) == NULL) {
146 	sudo_warnx(U_("%s: %s"), cp, U_("invalid value"));
147 	debug_return_bool(false);
148     }
149     *ep++ = '\0';
150     cred->uid = cred->euid = sudo_strtoid(cp, &errstr);
151     if (errstr != NULL) {
152 	sudo_warnx(U_("%s: %s"), cp, errstr);
153 	debug_return_bool(false);
154     }
155 
156     /* GID */
157     cp = ep;
158     if ((ep = strchr(cp, ':')) == NULL) {
159 	sudo_warnx(U_("%s: %s"), cp, U_("invalid value"));
160 	debug_return_bool(false);
161     }
162     *ep++ = '\0';
163     cred->gid = cred->egid = sudo_strtoid(cp, &errstr);
164     if (errstr != NULL) {
165 	sudo_warnx(U_("%s: %s"), cp, errstr);
166 	debug_return_bool(false);
167     }
168 
169     /* group vector */
170     cp = ep;
171     cred->ngroups = sudo_parse_gids(cp, NULL, &cred->groups);
172     if (cred->ngroups == -1)
173 	debug_return_bool(false);
174 
175     debug_return_bool(true);
176 }
177 
178 static int
sesh_edit_create_tfiles(int edit_flags,struct sudo_cred * user_cred,struct sudo_cred * run_cred,int argc,char * argv[])179 sesh_edit_create_tfiles(int edit_flags, struct sudo_cred *user_cred,
180     struct sudo_cred *run_cred, int argc, char *argv[])
181 {
182     int i, fd_src = -1, fd_dst = -1;
183     struct timespec times[2];
184     struct stat sb;
185     debug_decl(sesh_edit_create_tfiles, SUDO_DEBUG_EDIT);
186 
187     for (i = 0; i < argc - 1; i += 2) {
188 	char *path_src = argv[i];
189 	const char *path_dst = argv[i + 1];
190 
191 	/*
192 	 * Try to open the source file for reading.
193 	 * If it doesn't exist, we'll create an empty destination file.
194 	 */
195 	fd_src = sudo_edit_open(path_src, O_RDONLY,
196 	    S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, edit_flags, user_cred, run_cred);
197 	if (fd_src == -1) {
198 	    if (errno != ENOENT) {
199 		if (errno == ELOOP) {
200 		    sudo_warnx(U_("%s: editing symbolic links is not "
201 			"permitted"), path_src);
202 		} else if (errno == EISDIR) {
203 		    sudo_warnx(U_("%s: editing files in a writable directory "
204 			"is not permitted"), path_src);
205 		} else {
206 		    sudo_warn("%s", path_src);
207 		}
208 		goto cleanup;
209 	    }
210 	    /* New file, verify parent dir exists and is not writable. */
211 	    if (!sudo_edit_parent_valid(path_src, edit_flags, user_cred, run_cred))
212 		goto cleanup;
213 	}
214 	if (fd_src == -1) {
215 	    /* New file. */
216 	    memset(&sb, 0, sizeof(sb));
217 	} else if (fstat(fd_src, &sb) == -1 || !S_ISREG(sb.st_mode)) {
218 	    sudo_warnx(U_("%s: not a regular file"), path_src);
219 	    goto cleanup;
220 	}
221 
222 	/*
223 	 * Create temporary file using O_EXCL to ensure that temporary
224 	 * files are created by us and that we do not open any symlinks.
225 	 */
226 	fd_dst = open(path_dst, O_WRONLY|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR);
227 	if (fd_dst == -1) {
228 	    sudo_warn("%s", path_dst);
229 	    goto cleanup;
230 	}
231 
232 	if (fd_src != -1) {
233 	    if (sudo_copy_file(path_src, fd_src, -1, path_dst, fd_dst, -1) == -1)
234 		goto cleanup;
235 	    close(fd_src);
236 	}
237 
238 	/* Make mtime on temp file match src (sb filled in above). */
239 	mtim_get(&sb, times[0]);
240 	times[1].tv_sec = times[0].tv_sec;
241 	times[1].tv_nsec = times[0].tv_nsec;
242 	if (futimens(fd_dst, times) == -1) {
243 	    if (utimensat(AT_FDCWD, path_dst, times, 0) == -1)
244 		sudo_warn("%s", path_dst);
245 	}
246 	close(fd_dst);
247 	fd_dst = -1;
248     }
249     debug_return_int(SESH_SUCCESS);
250 
251 cleanup:
252     /* Remove temporary files. */
253     for (i = 0; i < argc - 1; i += 2)
254 	unlink(argv[i + 1]);
255     if (fd_src != -1)
256 	close(fd_src);
257     if (fd_dst != -1)
258 	close(fd_dst);
259     debug_return_int(SESH_ERR_NO_FILES);
260 }
261 
262 static int
sesh_edit_copy_tfiles(int edit_flags,struct sudo_cred * user_cred,struct sudo_cred * run_cred,int argc,char * argv[])263 sesh_edit_copy_tfiles(int edit_flags, struct sudo_cred *user_cred,
264     struct sudo_cred *run_cred, int argc, char *argv[])
265 {
266     int i, ret = SESH_SUCCESS;
267     int fd_src = -1, fd_dst = -1;
268     debug_decl(sesh_edit_copy_tfiles, SUDO_DEBUG_EDIT);
269 
270     for (i = 0; i < argc - 1; i += 2) {
271 	const char *path_src = argv[i];
272 	char *path_dst = argv[i + 1];
273 	off_t len_src, len_dst;
274 	struct stat sb;
275 
276 	/* Open temporary file for reading. */
277 	if (fd_src != -1)
278 	    close(fd_src);
279 	fd_src = open(path_src, O_RDONLY|O_NONBLOCK|O_NOFOLLOW);
280 	if (fd_src == -1) {
281 	    sudo_warn("%s", path_src);
282 	    ret = SESH_ERR_SOME_FILES;
283 	    continue;
284 	}
285 	/* Make sure the temporary file is safe and has the proper owner. */
286 	if (!sudo_check_temp_file(fd_src, path_src, run_cred->uid, &sb)) {
287 	    sudo_warnx(U_("contents of edit session left in %s"), path_src);
288 	    ret = SESH_ERR_SOME_FILES;
289 	    continue;
290 	}
291 	(void) fcntl(fd_src, F_SETFL, fcntl(fd_src, F_GETFL, 0) & ~O_NONBLOCK);
292 
293 	/* Create destination file. */
294 	if (fd_dst != -1)
295 	    close(fd_dst);
296 	fd_dst = sudo_edit_open(path_dst, O_WRONLY|O_CREAT,
297 	    S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, edit_flags, user_cred, run_cred);
298 	if (fd_dst == -1) {
299 	    if (errno == ELOOP) {
300 		sudo_warnx(U_("%s: editing symbolic links is not "
301 		    "permitted"), path_dst);
302 	    } else if (errno == EISDIR) {
303 		sudo_warnx(U_("%s: editing files in a writable directory "
304 		    "is not permitted"), path_dst);
305 	    } else {
306 		sudo_warn("%s", path_dst);
307 	    }
308 	    sudo_warnx(U_("contents of edit session left in %s"), path_src);
309 	    ret = SESH_ERR_SOME_FILES;
310 	    continue;
311 	}
312 
313 	/* sudo_check_temp_file() filled in sb for us. */
314 	len_src = sb.st_size;
315 	if (fstat(fd_dst, &sb) != 0) {
316 	    sudo_warn("%s", path_dst);
317 	    sudo_warnx(U_("contents of edit session left in %s"), path_src);
318 	    ret = SESH_ERR_SOME_FILES;
319 	    continue;
320 	}
321 	len_dst = sb.st_size;
322 
323 	if (sudo_copy_file(path_src, fd_src, len_src, path_dst, fd_dst,
324 		len_dst) == -1) {
325 	    sudo_warnx(U_("contents of edit session left in %s"), path_src);
326 	    ret = SESH_ERR_SOME_FILES;
327 	    continue;
328 	}
329 	unlink(path_src);
330     }
331     if (fd_src != -1)
332 	close(fd_src);
333     if (fd_dst != -1)
334 	close(fd_dst);
335 
336     debug_return_int(ret);
337 }
338 
339 static int
sesh_sudoedit(int argc,char * argv[])340 sesh_sudoedit(int argc, char *argv[])
341 {
342     int edit_flags, post, ret;
343     struct sudo_cred user_cred, run_cred;
344     debug_decl(sesh_sudoedit, SUDO_DEBUG_EDIT);
345 
346     memset(&user_cred, 0, sizeof(user_cred));
347     memset(&run_cred, 0, sizeof(run_cred));
348     edit_flags = CD_SUDOEDIT_FOLLOW;
349 
350     /* Check for -h flag (don't follow links). */
351     if (argc > 2 && strcmp(argv[2], "-h") == 0) {
352 	argv++;
353 	argc--;
354 	CLR(edit_flags, CD_SUDOEDIT_FOLLOW); // -V753
355     }
356 
357     /* Check for -w flag (disallow directories writable by the user). */
358     if (argc > 2 && strcmp(argv[2], "-w") == 0) {
359 	SET(edit_flags, CD_SUDOEDIT_CHECKDIR);
360 
361 	/* Parse uid:gid:gid1,gid2,... */
362 	if (argv[3] == NULL || !parse_user(argv[3], &user_cred))
363 	    debug_return_int(SESH_ERR_FAILURE);
364 	argv += 2;
365 	argc -= 2;
366     }
367 
368     if (argc < 3)
369 	debug_return_int(SESH_ERR_FAILURE);
370 
371     /*
372      * We need to know whether we are performing the copy operation
373      * before or after the editing. Without this we would not know
374      * which files are temporary and which are the originals.
375      *  post = 0 ... before
376      *  post = 1 ... after
377      */
378     if (strcmp(argv[2], "0") == 0)
379 	post = 0;
380     else if (strcmp(argv[2], "1") == 0)
381 	post = 1;
382     else /* invalid value */
383 	debug_return_int(SESH_ERR_INVALID);
384 
385     /* Align argv & argc to the beginning of the file list. */
386     argv += 3;
387     argc -= 3;
388 
389     /* no files specified, nothing to do */
390     if (argc == 0)
391 	debug_return_int(SESH_SUCCESS);
392     /* odd number of paths specified */
393     if (argc & 1)
394 	debug_return_int(SESH_ERR_BAD_PATHS);
395 
396     /* Masquerade as sudoedit so the user gets consistent error messages. */
397     setprogname("sudoedit");
398 
399     /*
400      * sudoedit runs us with the effective user-ID and group-ID of
401      * the target user as well as with the target user's group list.
402      */
403     run_cred.uid = run_cred.euid = geteuid();
404     run_cred.gid = run_cred.egid = getegid();
405     run_cred.ngroups = getgroups(0, NULL); // -V575
406     if (run_cred.ngroups > 0) {
407 	run_cred.groups = reallocarray(NULL, run_cred.ngroups,
408 	    sizeof(GETGROUPS_T));
409 	if (run_cred.groups == NULL) {
410 	    sudo_warnx(U_("%s: %s"), __func__,
411 		U_("unable to allocate memory"));
412 	    debug_return_int(SESH_ERR_FAILURE);
413 	}
414 	run_cred.ngroups = getgroups(run_cred.ngroups, run_cred.groups);
415 	if (run_cred.ngroups < 0) {
416 	    sudo_warn("%s", U_("unable to get group list"));
417 	    free(run_cred.groups);
418 	    debug_return_int(SESH_ERR_FAILURE);
419 	}
420     } else {
421 	run_cred.ngroups = 0;
422 	run_cred.groups = NULL;
423     }
424 
425     ret = post ?
426 	sesh_edit_copy_tfiles(edit_flags, &user_cred, &run_cred, argc, argv) :
427 	sesh_edit_create_tfiles(edit_flags, &user_cred, &run_cred, argc, argv);
428     debug_return_int(ret);
429 }
430