diff --git a/backport-CVE-2023-28486_CVE-2023-28487.patch b/backport-CVE-2023-28486_CVE-2023-28487.patch new file mode 100644 index 0000000000000000000000000000000000000000..e3ea4cb793a4285aa29948f269b31775bab92382 --- /dev/null +++ b/backport-CVE-2023-28486_CVE-2023-28487.patch @@ -0,0 +1,970 @@ +From 334daf92b31b79ce68ed75e2ee14fca265f029ca Mon Sep 17 00:00:00 2001 +From: "Todd C. Miller" +Date: Wed, 18 Jan 2023 08:21:34 -0700 +Subject: [PATCH] Escape control characters in log messages and "sudoreplay -l" + output. The log message contains user-controlled strings that could include + things like terminal control characters. Space characters in the command + path are now also escaped. + +Command line arguments that contain spaces are surrounded with +single quotes and any literal single quote or backslash characters +are escaped with a backslash. This makes it possible to distinguish +multiple command line arguments from a single argument that contains +spaces. + +Issue found by Matthieu Barjole and Victor Cutillas of Synacktiv +(https://synacktiv.com). +--- + docs/sudoers.man.in | 44 ++++++-- + docs/sudoers.mdoc.in | 38 +++++-- + docs/sudoreplay.man.in | 9 ++ + docs/sudoreplay.mdoc.in | 10 ++ + include/sudo_lbuf.h | 7 ++ + lib/eventlog/eventlog.c | 210 ++++++++++------------------------- + lib/iolog/iolog_json.c | 39 ------- + lib/util/lbuf.c | 106 ++++++++++++++++++ + lib/util/util.exp.in | 1 + + plugins/sudoers/sudoreplay.c | 144 ++++++++++++++++++++---- + 10 files changed, 383 insertions(+), 225 deletions(-) + +diff --git a/docs/sudoers.man.in b/docs/sudoers.man.in +index 787591111..be9dc327e 100644 +--- a/docs/sudoers.man.in ++++ b/docs/sudoers.man.in +@@ -5877,14 +5877,31 @@ can log events via + syslog(3), + to a local log file, or both. + The log format is almost identical in both cases. ++Any control characters present in the log data are formatted in octal ++with a leading ++\(oq#\(cq ++character. ++For example, a horizontal tab is stored as ++\(oq#011\(cq ++and an embedded carriage return is stored as ++\(oq#015\(cq. ++In addition, space characters in the command path are stored as ++\(oq#040\(cq. ++Command line arguments that contain spaces are enclosed in single quotes ++(''). ++This makes it possible to distinguish multiple command line arguments ++from a single argument that contains spaces. ++Literal single quotes and backslash characters ++(\(oq\e\(cq) ++in command line arguments are escaped with a backslash. + .SS "Accepted command log entries" + Commands that sudo runs are logged using the following format (split + into multiple lines for readability): + .nf + .sp + .RS 4n +-date hostname progname: username : TTY=ttyname ; PWD=cwd ; \e +- USER=runasuser ; GROUP=runasgroup ; TSID=logid ; \e ++date hostname progname: username : TTY=ttyname ; CHROOT=chroot ; \e ++ PWD=cwd ; USER=runasuser ; GROUP=runasgroup ; TSID=logid ; \e + ENV=env_vars COMMAND=command + .RE + .fi +@@ -5933,6 +5950,9 @@ was run on, or + \(lqunknown\(rq + if there was no terminal present. + .TP 14n ++chroot ++The root directory that the command was run in, if one was specified. ++.TP 14n + cwd + The current working directory that + \fBsudo\fR +@@ -5957,7 +5977,7 @@ A list of environment variables specified on the command line, + if specified. + .TP 14n + command +-The actual command that was executed. ++The actual command that was executed, including any command line arguments. + .PP + Messages are logged using the locale specified by + \fIsudoers_locale\fR, +@@ -6195,17 +6215,21 @@ with a few important differences: + 1.\& + The + \fIprogname\fR +-and +-\fIhostname\fR +-fields are not present. ++field is not present. + .TP 5n + 2.\& +-If the +-\fIlog_year\fR +-option is enabled, +-the date will also include the year. ++The ++\fIhostname\fR ++is only logged if the ++\fIlog_host\fR ++option is enabled. + .TP 5n + 3.\& ++The date does not include the year unless the ++\fIlog_year\fR ++option is enabled. ++.TP 5n ++4.\& + Lines that are longer than + \fIloglinelen\fR + characters (80 by default) are word-wrapped and continued on the +diff --git a/docs/sudoers.mdoc.in b/docs/sudoers.mdoc.in +index c72bc660d..92edacb9d 100644 +--- a/docs/sudoers.mdoc.in ++++ b/docs/sudoers.mdoc.in +@@ -5503,12 +5503,29 @@ can log events via + .Xr syslog 3 , + to a local log file, or both. + The log format is almost identical in both cases. ++Any control characters present in the log data are formatted in octal ++with a leading ++.Ql # ++character. ++For example, a horizontal tab is stored as ++.Ql #011 ++and an embedded carriage return is stored as ++.Ql #015 . ++In addition, space characters in the command path are stored as ++.Ql #040 . ++Command line arguments that contain spaces are enclosed in single quotes ++.Pq '' . ++This makes it possible to distinguish multiple command line arguments ++from a single argument that contains spaces. ++Literal single quotes and backslash characters ++.Pq Ql \e ++in command line arguments are escaped with a backslash. + .Ss Accepted command log entries + Commands that sudo runs are logged using the following format (split + into multiple lines for readability): + .Bd -literal -offset 4n +-date hostname progname: username : TTY=ttyname ; PWD=cwd ; \e +- USER=runasuser ; GROUP=runasgroup ; TSID=logid ; \e ++date hostname progname: username : TTY=ttyname ; CHROOT=chroot ; \e ++ PWD=cwd ; USER=runasuser ; GROUP=runasgroup ; TSID=logid ; \e + ENV=env_vars COMMAND=command + .Ed + .Pp +@@ -5551,6 +5568,8 @@ or + was run on, or + .Dq unknown + if there was no terminal present. ++.It chroot ++The root directory that the command was run in, if one was specified. + .It cwd + The current working directory that + .Nm sudo +@@ -5570,7 +5589,7 @@ option is enabled. + A list of environment variables specified on the command line, + if specified. + .It command +-The actual command that was executed. ++The actual command that was executed, including any command line arguments. + .El + .Pp + Messages are logged using the locale specified by +@@ -5794,14 +5813,17 @@ with a few important differences: + .It + The + .Em progname +-and ++field is not present. ++.It ++The + .Em hostname +-fields are not present. ++is only logged if the ++.Em log_host ++option is enabled. + .It +-If the ++The date does not include the year unless the + .Em log_year +-option is enabled, +-the date will also include the year. ++option is enabled. + .It + Lines that are longer than + .Em loglinelen +diff --git a/docs/sudoreplay.man.in b/docs/sudoreplay.man.in +index f1fed2d42..73dbd52f9 100644 +--- a/docs/sudoreplay.man.in ++++ b/docs/sudoreplay.man.in +@@ -170,6 +170,15 @@ In this mode, + will list available sessions in a format similar to the + \fBsudo\fR + log file format, sorted by file name (or sequence number). ++Any control characters present in the log data are formated in octal ++with a leading ++\(oq#\(cq ++character. ++For example, a horizontal tab is displayed as ++\(oq#011\(cq ++and an embedded carriage return is displayed as ++\(oq#015\(cq. ++.sp + If a + \fIsearch expression\fR + is specified, it will be used to restrict the IDs that are displayed. +diff --git a/docs/sudoreplay.mdoc.in b/docs/sudoreplay.mdoc.in +index 940a41462..005cf1f7f 100644 +--- a/docs/sudoreplay.mdoc.in ++++ b/docs/sudoreplay.mdoc.in +@@ -162,6 +162,16 @@ In this mode, + will list available sessions in a format similar to the + .Nm sudo + log file format, sorted by file name (or sequence number). ++Any control characters present in the log data are formatted in octal ++with a leading ++.Ql # ++character. ++For example, a horizontal tab is displayed as ++.Ql #011 ++and an embedded carriage return is displayed as ++.Ql #015 . ++Space characters in the command name and arguments are also formatted in octal. ++.Pp + If a + .Ar search expression + is specified, it will be used to restrict the IDs that are displayed. +diff --git a/include/sudo_lbuf.h b/include/sudo_lbuf.h +index be0a04f73..29090cb8d 100644 +--- a/include/sudo_lbuf.h ++++ b/include/sudo_lbuf.h +@@ -36,9 +36,15 @@ struct sudo_lbuf { + + typedef int (*sudo_lbuf_output_t)(const char *); + ++/* Flags for sudo_lbuf_append_esc() */ ++#define LBUF_ESC_CNTRL 0x01 ++#define LBUF_ESC_BLANK 0x02 ++#define LBUF_ESC_QUOTE 0x04 ++ + sudo_dso_public void sudo_lbuf_init_v1(struct sudo_lbuf *lbuf, sudo_lbuf_output_t output, int indent, const char *continuation, int cols); + sudo_dso_public void sudo_lbuf_destroy_v1(struct sudo_lbuf *lbuf); + sudo_dso_public bool sudo_lbuf_append_v1(struct sudo_lbuf *lbuf, const char *fmt, ...) sudo_printflike(2, 3); ++sudo_dso_public bool sudo_lbuf_append_esc_v1(struct sudo_lbuf *lbuf, int flags, const char *fmt, ...) sudo_printflike(3, 4); + sudo_dso_public bool sudo_lbuf_append_quoted_v1(struct sudo_lbuf *lbuf, const char *set, const char *fmt, ...) sudo_printflike(3, 4); + sudo_dso_public void sudo_lbuf_print_v1(struct sudo_lbuf *lbuf); + sudo_dso_public bool sudo_lbuf_error_v1(struct sudo_lbuf *lbuf); +@@ -47,6 +53,7 @@ sudo_dso_public void sudo_lbuf_clearerr_v1(struct sudo_lbuf *lbuf); + #define sudo_lbuf_init(_a, _b, _c, _d, _e) sudo_lbuf_init_v1((_a), (_b), (_c), (_d), (_e)) + #define sudo_lbuf_destroy(_a) sudo_lbuf_destroy_v1((_a)) + #define sudo_lbuf_append sudo_lbuf_append_v1 ++#define sudo_lbuf_append_esc sudo_lbuf_append_esc_v1 + #define sudo_lbuf_append_quoted sudo_lbuf_append_quoted_v1 + #define sudo_lbuf_print(_a) sudo_lbuf_print_v1((_a)) + #define sudo_lbuf_error(_a) sudo_lbuf_error_v1((_a)) +diff --git a/lib/eventlog/eventlog.c b/lib/eventlog/eventlog.c +index c0183d3d2..e5dae626a 100644 +--- a/lib/eventlog/eventlog.c ++++ b/lib/eventlog/eventlog.c +@@ -1,7 +1,7 @@ + /* + * SPDX-License-Identifier: ISC + * +- * Copyright (c) 1994-1996, 1998-2021 Todd C. Miller ++ * Copyright (c) 1994-1996, 1998-2023 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -51,24 +51,13 @@ + #include "sudo_compat.h" + #include "sudo_debug.h" + #include "sudo_eventlog.h" ++#include "sudo_lbuf.h" + #include "sudo_fatal.h" + #include "sudo_gettext.h" + #include "sudo_json.h" + #include "sudo_queue.h" + #include "sudo_util.h" + +-#define LL_HOST_STR "HOST=" +-#define LL_TTY_STR "TTY=" +-#define LL_CHROOT_STR "CHROOT=" +-#define LL_CWD_STR "PWD=" +-#define LL_USER_STR "USER=" +-#define LL_GROUP_STR "GROUP=" +-#define LL_ENV_STR "ENV=" +-#define LL_CMND_STR "COMMAND=" +-#define LL_TSID_STR "TSID=" +-#define LL_EXIT_STR "EXIT=" +-#define LL_SIGNAL_STR "SIGNAL=" +- + #define IS_SESSID(s) ( \ + isalnum((unsigned char)(s)[0]) && isalnum((unsigned char)(s)[1]) && \ + (s)[2] == '/' && \ +@@ -93,26 +82,28 @@ new_logline(int event_type, int flags, struct eventlog_args *args, + const struct eventlog *evlog) + { + const struct eventlog_config *evl_conf = eventlog_getconf(); +- char *line = NULL, *evstr = NULL; + const char *iolog_file; + const char *tty, *tsid = NULL; + char exit_str[(((sizeof(int) * 8) + 2) / 3) + 2]; + char sessid[7], offsetstr[64] = ""; +- size_t len = 0; ++ struct sudo_lbuf lbuf; + int i; + debug_decl(new_logline, SUDO_DEBUG_UTIL); + ++ sudo_lbuf_init(&lbuf, NULL, 0, NULL, 0); ++ + if (ISSET(flags, EVLOG_RAW) || evlog == NULL) { + if (args->reason != NULL) { + if (args->errstr != NULL) { +- if (asprintf(&line, "%s: %s", args->reason, args->errstr) == -1) +- goto oom; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "%s: %s", ++ args->reason, args->errstr); + } else { +- if ((line = strdup(args->reason)) == NULL) +- goto oom; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "%s", args->reason); + } ++ if (sudo_lbuf_error(&lbuf)) ++ goto oom; + } +- debug_return_str(line); ++ debug_return_str(lbuf.buf); + } + + /* A TSID may be a sudoers-style session ID or a free-form string. */ +@@ -150,169 +141,90 @@ new_logline(int event_type, int flags, struct eventlog_args *args, + } + + /* +- * Compute line length ++ * Format the log line as an lbuf, escaping control characters in ++ * octal form (#0nn). Error checking (ENOMEM) is done at the end. + */ +- if (args->reason != NULL) +- len += strlen(args->reason) + 3; +- if (args->errstr != NULL) +- len += strlen(args->errstr) + 3; +- if (evlog->submithost != NULL && !evl_conf->omit_hostname) +- len += sizeof(LL_HOST_STR) + 2 + strlen(evlog->submithost); +- if (tty != NULL) +- len += sizeof(LL_TTY_STR) + 2 + strlen(tty); +- if (evlog->runchroot != NULL) +- len += sizeof(LL_CHROOT_STR) + 2 + strlen(evlog->runchroot); +- if (evlog->runcwd != NULL) +- len += sizeof(LL_CWD_STR) + 2 + strlen(evlog->runcwd); +- if (evlog->runuser != NULL) +- len += sizeof(LL_USER_STR) + 2 + strlen(evlog->runuser); +- if (evlog->rungroup != NULL) +- len += sizeof(LL_GROUP_STR) + 2 + strlen(evlog->rungroup); +- if (tsid != NULL) { +- len += sizeof(LL_TSID_STR) + 2 + strlen(tsid) + strlen(offsetstr); +- } +- if (evlog->env_add != NULL) { +- size_t evlen = 0; +- char * const *ep; +- +- for (ep = evlog->env_add; *ep != NULL; ep++) +- evlen += strlen(*ep) + 1; +- if (evlen != 0) { +- if ((evstr = malloc(evlen)) == NULL) +- goto oom; +- ep = evlog->env_add; +- if (strlcpy(evstr, *ep, evlen) >= evlen) +- goto toobig; +- while (*++ep != NULL) { +- if (strlcat(evstr, " ", evlen) >= evlen || +- strlcat(evstr, *ep, evlen) >= evlen) +- goto toobig; +- } +- len += sizeof(LL_ENV_STR) + 2 + evlen; +- } +- } +- if (evlog->command != NULL) { +- len += sizeof(LL_CMND_STR) - 1 + strlen(evlog->command); +- if (evlog->argv != NULL && evlog->argv[0] != NULL) { +- for (i = 1; evlog->argv[i] != NULL; i++) +- len += strlen(evlog->argv[i]) + 1; +- } +- if (event_type == EVLOG_EXIT) { +- if (evlog->signal_name != NULL) +- len += sizeof(LL_SIGNAL_STR) + 2 + strlen(evlog->signal_name); +- if (evlog->exit_value != -1) { +- (void)snprintf(exit_str, sizeof(exit_str), "%d", evlog->exit_value); +- len += sizeof(LL_EXIT_STR) + 2 + strlen(exit_str); +- } +- } +- } +- +- /* +- * Allocate and build up the line. +- */ +- if ((line = malloc(++len)) == NULL) +- goto oom; +- line[0] = '\0'; +- + if (args->reason != NULL) { +- if (strlcat(line, args->reason, len) >= len || +- strlcat(line, args->errstr ? " : " : " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "%s%s", args->reason, ++ args->errstr ? " : " : " ; "); + } + if (args->errstr != NULL) { +- if (strlcat(line, args->errstr, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "%s ; ", args->errstr); + } + if (evlog->submithost != NULL && !evl_conf->omit_hostname) { +- if (strlcat(line, LL_HOST_STR, len) >= len || +- strlcat(line, evlog->submithost, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "HOST=%s ; ", ++ evlog->submithost); + } + if (tty != NULL) { +- if (strlcat(line, LL_TTY_STR, len) >= len || +- strlcat(line, tty, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "TTY=%s ; ", tty); + } + if (evlog->runchroot != NULL) { +- if (strlcat(line, LL_CHROOT_STR, len) >= len || +- strlcat(line, evlog->runchroot, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "CHROOT=%s ; ", ++ evlog->runchroot); + } + if (evlog->runcwd != NULL) { +- if (strlcat(line, LL_CWD_STR, len) >= len || +- strlcat(line, evlog->runcwd, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "PWD=%s ; ", ++ evlog->runcwd); + } + if (evlog->runuser != NULL) { +- if (strlcat(line, LL_USER_STR, len) >= len || +- strlcat(line, evlog->runuser, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "USER=%s ; ", ++ evlog->runuser); + } + if (evlog->rungroup != NULL) { +- if (strlcat(line, LL_GROUP_STR, len) >= len || +- strlcat(line, evlog->rungroup, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "GROUP=%s ; ", ++ evlog->rungroup); + } + if (tsid != NULL) { +- if (strlcat(line, LL_TSID_STR, len) >= len || +- strlcat(line, tsid, len) >= len || +- strlcat(line, offsetstr, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; +- } +- if (evstr != NULL) { +- if (strlcat(line, LL_ENV_STR, len) >= len || +- strlcat(line, evstr, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; +- free(evstr); +- evstr = NULL; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "TSID=%s%s ; ", tsid, ++ offsetstr); ++ } ++ if (evlog->env_add != NULL && evlog->env_add[0] != NULL) { ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "ENV=%s", ++ evlog->env_add[0]); ++ for (i = 1; evlog->env_add[i] != NULL; i++) { ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, " %s", ++ evlog->env_add[i]); ++ } + } + if (evlog->command != NULL) { +- if (strlcat(line, LL_CMND_STR, len) >= len) +- goto toobig; +- if (strlcat(line, evlog->command, len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL|LBUF_ESC_BLANK, ++ "COMMAND=%s", evlog->command); + if (evlog->argv != NULL && evlog->argv[0] != NULL) { + for (i = 1; evlog->argv[i] != NULL; i++) { +- if (strlcat(line, " ", len) >= len || +- strlcat(line, evlog->argv[i], len) >= len) +- goto toobig; ++ sudo_lbuf_append(&lbuf, " "); ++ if (strchr(evlog->argv[i], ' ') != NULL) { ++ /* Wrap args containing spaces in single quotes. */ ++ sudo_lbuf_append(&lbuf, "'"); ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL|LBUF_ESC_QUOTE, ++ "%s", evlog->argv[i]); ++ sudo_lbuf_append(&lbuf, "'"); ++ } else { ++ /* Escape quotes here too for consistency. */ ++ sudo_lbuf_append_esc(&lbuf, ++ LBUF_ESC_CNTRL|LBUF_ESC_BLANK|LBUF_ESC_QUOTE, ++ "%s", evlog->argv[i]); ++ } + } + } + if (event_type == EVLOG_EXIT) { + if (evlog->signal_name != NULL) { +- if (strlcat(line, " ; ", len) >= len || +- strlcat(line, LL_SIGNAL_STR, len) >= len || +- strlcat(line, evlog->signal_name, len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, " ; SIGNAL=%s", ++ evlog->signal_name); + } + if (evlog->exit_value != -1) { +- if (strlcat(line, " ; ", len) >= len || +- strlcat(line, LL_EXIT_STR, len) >= len || +- strlcat(line, exit_str, len) >= len) +- goto toobig; ++ (void)snprintf(exit_str, sizeof(exit_str), "%d", ++ evlog->exit_value); ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, " ; EXIT=%s", ++ exit_str); + } + } + } +- +- debug_return_str(line); ++ if (!sudo_lbuf_error(&lbuf)) ++ debug_return_str(lbuf.buf); + oom: +- free(evstr); ++ sudo_lbuf_destroy(&lbuf); + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_str(NULL); +-toobig: +- free(evstr); +- free(line); +- sudo_warnx(U_("internal error, %s overflow"), __func__); +- debug_return_str(NULL); + } + + static void +diff --git a/lib/iolog/iolog_json.c b/lib/iolog/iolog_json.c +index 5ea338e5d..6f384ea59 100644 +--- a/lib/iolog/iolog_json.c ++++ b/lib/iolog/iolog_json.c +@@ -551,45 +551,6 @@ iolog_parse_json_object(struct json_object *object, struct eventlog *evlog) + } + } + +- /* Merge cmd and argv as sudoreplay expects. */ +- if (evlog->command != NULL && evlog->argv != NULL && evlog->argv[0] != NULL) { +- size_t len, bufsize = strlen(evlog->command) + 1; +- char *cp, *buf; +- int ac; +- +- /* Skip argv[0], we use evlog->command instead. */ +- for (ac = 1; evlog->argv[ac] != NULL; ac++) +- bufsize += strlen(evlog->argv[ac]) + 1; +- +- if ((buf = malloc(bufsize)) == NULL) { +- sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); +- goto done; +- } +- cp = buf; +- +- len = strlcpy(cp, evlog->command, bufsize); +- if (len >= bufsize) +- sudo_fatalx(U_("internal error, %s overflow"), __func__); +- cp += len; +- bufsize -= len; +- +- for (ac = 1; evlog->argv[ac] != NULL; ac++) { +- if (bufsize < 2) +- sudo_fatalx(U_("internal error, %s overflow"), __func__); +- *cp++ = ' '; +- bufsize--; +- +- len = strlcpy(cp, evlog->argv[ac], bufsize); +- if (len >= bufsize) +- sudo_fatalx(U_("internal error, %s overflow"), __func__); +- cp += len; +- bufsize -= len; +- } +- +- free(evlog->command); +- evlog->command = buf; +- } +- + ret = true; + + done: +diff --git a/lib/util/lbuf.c b/lib/util/lbuf.c +index 101982065..72bcac26f 100644 +--- a/lib/util/lbuf.c ++++ b/lib/util/lbuf.c +@@ -94,6 +94,112 @@ sudo_lbuf_expand(struct sudo_lbuf *lbuf, unsigned int extra) + debug_return_bool(true); + } + ++/* ++ * Escape a character in octal form (#0n) and store it as a string ++ * in buf, which must have at least 6 bytes available. ++ * Returns the length of buf, not counting the terminating NUL byte. ++ */ ++static int ++escape(unsigned char ch, char *buf) ++{ ++ const int len = ch < 0100 ? (ch < 010 ? 3 : 4) : 5; ++ ++ /* Work backwards from the least significant digit to most significant. */ ++ switch (len) { ++ case 5: ++ buf[4] = (ch & 7) + '0'; ++ ch >>= 3; ++ FALLTHROUGH; ++ case 4: ++ buf[3] = (ch & 7) + '0'; ++ ch >>= 3; ++ FALLTHROUGH; ++ case 3: ++ buf[2] = (ch & 7) + '0'; ++ buf[1] = '0'; ++ buf[0] = '#'; ++ break; ++ } ++ buf[len] = '\0'; ++ ++ return len; ++} ++ ++/* ++ * Parse the format and append strings, only %s and %% escapes are supported. ++ * Any non-printable characters are escaped in octal as #0nn. ++ */ ++bool ++sudo_lbuf_append_esc_v1(struct sudo_lbuf *lbuf, int flags, const char *fmt, ...) ++{ ++ unsigned int saved_len = lbuf->len; ++ bool ret = false; ++ const char *s; ++ va_list ap; ++ debug_decl(sudo_lbuf_append_esc, SUDO_DEBUG_UTIL); ++ ++ if (sudo_lbuf_error(lbuf)) ++ debug_return_bool(false); ++ ++#define should_escape(ch) \ ++ ((ISSET(flags, LBUF_ESC_CNTRL) && iscntrl((unsigned char)ch)) || \ ++ (ISSET(flags, LBUF_ESC_BLANK) && isblank((unsigned char)ch))) ++#define should_quote(ch) \ ++ (ISSET(flags, LBUF_ESC_QUOTE) && (ch == '\'' || ch == '\\')) ++ ++ va_start(ap, fmt); ++ while (*fmt != '\0') { ++ if (fmt[0] == '%' && fmt[1] == 's') { ++ if ((s = va_arg(ap, char *)) == NULL) ++ s = "(NULL)"; ++ while (*s != '\0') { ++ if (should_escape(*s)) { ++ if (!sudo_lbuf_expand(lbuf, sizeof("#0177") - 1)) ++ goto done; ++ lbuf->len += escape(*s++, lbuf->buf + lbuf->len); ++ continue; ++ } ++ if (should_quote(*s)) { ++ if (!sudo_lbuf_expand(lbuf, 2)) ++ goto done; ++ lbuf->buf[lbuf->len++] = '\\'; ++ lbuf->buf[lbuf->len++] = *s++; ++ continue; ++ } ++ if (!sudo_lbuf_expand(lbuf, 1)) ++ goto done; ++ lbuf->buf[lbuf->len++] = *s++; ++ } ++ fmt += 2; ++ continue; ++ } ++ if (should_escape(*fmt)) { ++ if (!sudo_lbuf_expand(lbuf, sizeof("#0177") - 1)) ++ goto done; ++ if (*fmt == '\'') { ++ lbuf->buf[lbuf->len++] = '\\'; ++ lbuf->buf[lbuf->len++] = *fmt++; ++ } else { ++ lbuf->len += escape(*fmt++, lbuf->buf + lbuf->len); ++ } ++ continue; ++ } ++ if (!sudo_lbuf_expand(lbuf, 1)) ++ goto done; ++ lbuf->buf[lbuf->len++] = *fmt++; ++ } ++ ret = true; ++ ++done: ++ if (!ret) ++ lbuf->len = saved_len; ++ if (lbuf->size != 0) ++ lbuf->buf[lbuf->len] = '\0'; ++ va_end(ap); ++ ++ debug_return_bool(ret); ++} ++ + /* + * Parse the format and append strings, only %s and %% escapes are supported. + * Any characters in set are quoted with a backslash. +diff --git a/lib/util/util.exp.in b/lib/util/util.exp.in +index 554904c5f..6c8130a02 100644 +--- a/lib/util/util.exp.in ++++ b/lib/util/util.exp.in +@@ -98,6 +98,7 @@ sudo_json_get_len_v1 + sudo_json_init_v1 + sudo_json_open_array_v1 + sudo_json_open_object_v1 ++sudo_lbuf_append_esc_v1 + sudo_lbuf_append_quoted_v1 + sudo_lbuf_append_v1 + sudo_lbuf_clearerr_v1 +diff --git a/plugins/sudoers/sudoreplay.c b/plugins/sudoers/sudoreplay.c +index 750dfeed9..43ff23999 100644 +--- a/plugins/sudoers/sudoreplay.c ++++ b/plugins/sudoers/sudoreplay.c +@@ -62,6 +62,7 @@ + #include "sudo_debug.h" + #include "sudo_event.h" + #include "sudo_eventlog.h" ++#include "sudo_lbuf.h" + #include "sudo_fatal.h" + #include "sudo_gettext.h" + #include "sudo_iolog.h" +@@ -373,6 +374,10 @@ main(int argc, char *argv[]) + if ((evlog = iolog_parse_loginfo(iolog_dir_fd, iolog_dir)) == NULL) + goto done; + printf(_("Replaying sudo session: %s"), evlog->command); ++ if (evlog->argv != NULL && evlog->argv[0] != NULL) { ++ for (i = 1; evlog->argv[i] != NULL; i++) ++ printf(" %s", evlog->argv[i]); ++ } + + /* Setup terminal if appropriate. */ + if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) +@@ -1315,11 +1320,57 @@ parse_expr(struct search_node_list *head, char *argv[], bool sub_expr) + debug_return_int(av - argv); + } + ++static char * ++expand_command(struct eventlog *evlog, char **newbuf) ++{ ++ size_t len, bufsize = strlen(evlog->command) + 1; ++ char *cp, *buf; ++ int ac; ++ debug_decl(expand_command, SUDO_DEBUG_UTIL); ++ ++ if (evlog->argv == NULL || evlog->argv[0] == NULL || evlog->argv[1] == NULL) { ++ /* No arguments, we can use the command as-is. */ ++ *newbuf = NULL; ++ debug_return_str(evlog->command); ++ } ++ ++ /* Skip argv[0], we use evlog->command instead. */ ++ for (ac = 1; evlog->argv[ac] != NULL; ac++) ++ bufsize += strlen(evlog->argv[ac]) + 1; ++ ++ if ((buf = malloc(bufsize)) == NULL) ++ sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); ++ cp = buf; ++ ++ len = strlcpy(cp, evlog->command, bufsize); ++ if (len >= bufsize) ++ sudo_fatalx(U_("internal error, %s overflow"), __func__); ++ cp += len; ++ bufsize -= len; ++ ++ for (ac = 1; evlog->argv[ac] != NULL; ac++) { ++ if (bufsize < 2) ++ sudo_fatalx(U_("internal error, %s overflow"), __func__); ++ *cp++ = ' '; ++ bufsize--; ++ ++ len = strlcpy(cp, evlog->argv[ac], bufsize); ++ if (len >= bufsize) ++ sudo_fatalx(U_("internal error, %s overflow"), __func__); ++ cp += len; ++ bufsize -= len; ++ } ++ ++ *newbuf = buf; ++ debug_return_str(buf); ++} ++ + static bool + match_expr(struct search_node_list *head, struct eventlog *evlog, bool last_match) + { + struct search_node *sn; + bool res = false, matched = last_match; ++ char *tofree; + int rc; + debug_decl(match_expr, SUDO_DEBUG_UTIL); + +@@ -1353,13 +1404,15 @@ match_expr(struct search_node_list *head, struct eventlog *evlog, bool last_matc + res = strcmp(sn->u.user, evlog->submituser) == 0; + break; + case ST_PATTERN: +- rc = regexec(&sn->u.cmdre, evlog->command, 0, NULL, 0); ++ rc = regexec(&sn->u.cmdre, expand_command(evlog, &tofree), ++ 0, NULL, 0); + if (rc && rc != REG_NOMATCH) { + char buf[BUFSIZ]; + regerror(rc, &sn->u.cmdre, buf, sizeof(buf)); + sudo_fatalx("%s", buf); + } + res = rc == REG_NOMATCH ? 0 : 1; ++ free(tofree); + break; + case ST_FROMDATE: + res = sudo_timespeccmp(&evlog->submit_time, &sn->u.tstamp, >=); +@@ -1380,12 +1433,13 @@ match_expr(struct search_node_list *head, struct eventlog *evlog, bool last_matc + } + + static int +-list_session(char *log_dir, regex_t *re, const char *user, const char *tty) ++list_session(struct sudo_lbuf *lbuf, char *log_dir, regex_t *re, ++ const char *user, const char *tty) + { + char idbuf[7], *idstr, *cp; + struct eventlog *evlog = NULL; + const char *timestr; +- int ret = -1; ++ int i, ret = -1; + debug_decl(list_session, SUDO_DEBUG_UTIL); + + if ((evlog = iolog_parse_loginfo(-1, log_dir)) == NULL) +@@ -1417,23 +1471,71 @@ list_session(char *log_dir, regex_t *re, const char *user, const char *tty) + } + /* XXX - print lines + cols? */ + timestr = get_timestr(evlog->submit_time.tv_sec, 1); +- printf("%s : %s : ", timestr ? timestr : "invalid date", evlog->submituser); +- if (evlog->submithost != NULL) +- printf("HOST=%s ; ", evlog->submithost); +- if (evlog->ttyname != NULL) +- printf("TTY=%s ; ", evlog->ttyname); +- if (evlog->runchroot != NULL) +- printf("CHROOT=%s ; ", evlog->runchroot); +- if (evlog->runcwd != NULL || evlog->cwd != NULL) +- printf("CWD=%s ; ", evlog->runcwd ? evlog->runcwd : evlog->cwd); +- printf("USER=%s ; ", evlog->runuser); +- if (evlog->rungroup != NULL) +- printf("GROUP=%s ; ", evlog->rungroup); +- printf("TSID=%s ; COMMAND=%s\n", idstr, evlog->command); +- +- ret = 0; ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "%s : %s : ", ++ timestr ? timestr : "invalid date", evlog->submituser); ++ if (evlog->submithost != NULL) { ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "HOST=%s ; ", ++ evlog->submithost); ++ } ++ if (evlog->ttyname != NULL) { ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "TTY=%s ; ", ++ evlog->ttyname); ++ } ++ if (evlog->runchroot != NULL) { ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "CHROOT=%s ; ", ++ evlog->runchroot); ++ } ++ if (evlog->runcwd != NULL || evlog->cwd != NULL) { ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "CWD=%s ; ", ++ evlog->runcwd ? evlog->runcwd : evlog->cwd); ++ } ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "USER=%s ; ", evlog->runuser); ++ if (evlog->rungroup != NULL) { ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "GROUP=%s ; ", ++ evlog->rungroup); ++ } ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "TSID=%s ; ", idstr); ++ ++ /* ++ * If we have both command and argv from info.json we can escape ++ * blanks in the the command and arguments. If all we have is a ++ * single string containing both the command and arguments we cannot. ++ */ ++ if (evlog->argv != NULL) { ++ /* Command plus argv from the info.json file. */ ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL|LBUF_ESC_BLANK, ++ "COMMAND=%s", evlog->command); ++ if (evlog->argv[0] != NULL) { ++ for (i = 1; evlog->argv[i] != NULL; i++) { ++ sudo_lbuf_append(lbuf, " "); ++ if (strchr(evlog->argv[i], ' ') != NULL) { ++ /* Wrap args containing spaces in single quotes. */ ++ sudo_lbuf_append(lbuf, "'"); ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL|LBUF_ESC_QUOTE, ++ "%s", evlog->argv[i]); ++ sudo_lbuf_append(lbuf, "'"); ++ } else { ++ /* Escape quotes here too for consistency. */ ++ sudo_lbuf_append_esc(lbuf, ++ LBUF_ESC_CNTRL|LBUF_ESC_BLANK|LBUF_ESC_QUOTE, ++ "%s", evlog->argv[i]); ++ } ++ } ++ } ++ } else { ++ /* Single string from the legacy info file. */ ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "COMMAND=%s", ++ evlog->command); ++ } ++ ++ if (!sudo_lbuf_error(lbuf)) { ++ puts(lbuf->buf); ++ ret = 0; ++ } + + done: ++ lbuf->error = 0; ++ lbuf->len = 0; + eventlog_free(evlog); + debug_return_int(ret); + } +@@ -1453,6 +1555,7 @@ find_sessions(const char *dir, regex_t *re, const char *user, const char *tty) + DIR *d; + struct dirent *dp; + struct stat sb; ++ struct sudo_lbuf lbuf; + size_t sdlen, sessions_len = 0, sessions_size = 0; + unsigned int i; + int len; +@@ -1464,6 +1567,8 @@ find_sessions(const char *dir, regex_t *re, const char *user, const char *tty) + #endif + debug_decl(find_sessions, SUDO_DEBUG_UTIL); + ++ sudo_lbuf_init(&lbuf, NULL, 0, NULL, 0); ++ + d = opendir(dir); + if (d == NULL) + sudo_fatal(U_("unable to open %s"), dir); +@@ -1524,7 +1629,7 @@ find_sessions(const char *dir, regex_t *re, const char *user, const char *tty) + /* Check for dir with a log file. */ + if (lstat(pathbuf, &sb) == 0 && S_ISREG(sb.st_mode)) { + pathbuf[sdlen + len - 4] = '\0'; +- list_session(pathbuf, re, user, tty); ++ list_session(&lbuf, pathbuf, re, user, tty); + } else { + /* Strip off "/log" and recurse if a non-log dir. */ + pathbuf[sdlen + len - 4] = '\0'; +@@ -1535,6 +1640,7 @@ find_sessions(const char *dir, regex_t *re, const char *user, const char *tty) + } + free(sessions); + } ++ sudo_lbuf_destroy(&lbuf); + + debug_return_int(0); + } diff --git a/sudo.spec b/sudo.spec index 29968e5b690cb8f49c628005d111c23412bf27a1..e0799176c948e88611cac866941b2bdff0762a22 100644 --- a/sudo.spec +++ b/sudo.spec @@ -1,6 +1,6 @@ Name: sudo Version: 1.9.12p2 -Release: 3 +Release: 4 Summary: Allows restricted root access for specified users License: ISC URL: https://www.sudo.ws @@ -11,6 +11,7 @@ Source2: sudo Source3: sudo-i Patch0: backport-CVE-2023-27320.patch +Patch1: backport-CVE-2023-28486_CVE-2023-28487.patch Buildroot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) Requires: pam @@ -155,6 +156,9 @@ chrpath -d $RPM_BUILD_ROOT/usr/libexec/sudo/* %exclude %{_pkgdocdir}/ChangeLog %changelog +* Tue Mar 28 2023 wangcheng - 1.9.12p2-4 +- Fix CVE-2023-28486 and CVE-2023-28487 + * Fri Mar 10 2023 wangyu - 1.9.12p2-3 - Fix CVE-2023-27320.