From 73e4e70ea8e6b890c3918b52bb2e647313a09faa Mon Sep 17 00:00:00 2001
From: Matt Johnston <matt@ucc.asn.au>
Date: Tue, 9 Dec 2025 09:05:30 +0900
Subject: [PATCH] Retain utmp saved group when dropping privileges

utmp is required to record logout. The saved group
is reset by the OS for the executed user shell.

This requires setresgid() function which is not available on all
platforms. Notable platforms are netbsd and macos. Those platforms will
have to set DROPBEAR_SVR_DROP_PRIVS 0 unless an alternative approach is
found.

CVE: CVE-2025-14282
Upstream-Status: Backport [https://github.com/mkj/dropbear/commit/73e4e70ea8e6b890c3918b52bb2e647313a09faa]
Signed-off-by: Peter Marko <peter.marko@siemens.com>
---
 .github/workflows/build.yml |  6 ++++
 configure                   |  7 +++++
 configure.ac                |  1 +
 src/auth.h                  |  2 ++
 src/config.h.in             |  3 ++
 src/loginrec.c              |  6 ----
 src/session.h               |  6 ++++
 src/svr-auth.c              | 61 +++++++++++++++++++++++++++++++++++--
 src/svr-chansession.c       |  8 +++++
 src/sysoptions.h            |  4 +++
 10 files changed, 96 insertions(+), 8 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5c07d28..4fe41bd 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -78,6 +78,9 @@ jobs:
             # fails with:
             # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols
             ranlib: ranlib -no_warning_for_no_symbols
+            # macos doesn't have setresgid
+            localoptions: |
+              #define DROPBEAR_SVR_DROP_PRIVS 0
 
           - name: macos 15
             os: macos-15
@@ -90,6 +93,9 @@ jobs:
             # fails with:
             # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols
             ranlib: ranlib -no_warning_for_no_symbols
+            # macos doesn't have setresgid
+            localoptions: |
+              #define DROPBEAR_SVR_DROP_PRIVS 0
 
           # Check that debug code doesn't bitrot
           - name: DEBUG_TRACE
diff --git a/configure b/configure
index 13c911e..8867f8a 100755
--- a/configure
+++ b/configure
@@ -7597,6 +7597,13 @@ then :
 
 fi
 
+ac_fn_c_check_func "$LINENO" "setresgid" "ac_cv_func_setresgid"
+if test "x$ac_cv_func_setresgid" = xyes
+then :
+  printf "%s\n" "#define HAVE_SETRESGID 1" >>confdefs.h
+
+fi
+
 
 # Might be a macro. Might be sys/endian.h on BSDs
 ac_fn_c_check_header_compile "$LINENO" "endian.h" "ac_cv_header_endian_h" "$ac_includes_default"
diff --git a/configure.ac b/configure.ac
index 674fd4d..0e7e331 100644
--- a/configure.ac
+++ b/configure.ac
@@ -545,6 +545,7 @@ AC_CHECK_FUNCS(utmpname)
 AC_CHECK_FUNCS(endutxent getutxent getutxid getutxline pututxline )
 AC_CHECK_FUNCS(setutxent utmpxname)
 AC_CHECK_FUNCS(logout updwtmp logwtmp)
+AC_CHECK_FUNCS(setresgid)
 
 # Might be a macro. Might be sys/endian.h on BSDs
 AC_CHECK_HEADERS([endian.h])
diff --git a/src/auth.h b/src/auth.h
index 096d23d..1145ad7 100644
--- a/src/auth.h
+++ b/src/auth.h
@@ -41,6 +41,8 @@ void svr_auth_password(int valid_user);
 void svr_auth_pubkey(int valid_user);
 void svr_auth_pam(int valid_user);
 void svr_switch_user(void);
+void svr_raise_gid_utmp(void);
+void svr_restore_gid(void);
 
 #if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT
 int svr_pubkey_allows_agentfwd(void);
diff --git a/src/config.h.in b/src/config.h.in
index 0590e0c..589786e 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -231,6 +231,9 @@
 /* Define to 1 if you have the <security/pam_appl.h> header file. */
 #undef HAVE_SECURITY_PAM_APPL_H
 
+/* Define to 1 if you have the `setresgid' function. */
+#undef HAVE_SETRESGID
+
 /* Define to 1 if you have the `setutent' function. */
 #undef HAVE_SETUTENT
 
diff --git a/src/loginrec.c b/src/loginrec.c
index d4fdb62..3118bf6 100644
--- a/src/loginrec.c
+++ b/src/loginrec.c
@@ -304,12 +304,6 @@ login_set_current_time(struct logininfo *li)
 void
 login_write (struct logininfo *li)
 {
-#ifndef HAVE_CYGWIN
-	if ((int)geteuid() != 0) {
-	  return 1;
-	}
-#endif
-
 	/* set the timestamp */
 	login_set_current_time(li);
 #ifdef USE_LOGIN
diff --git a/src/session.h b/src/session.h
index f37e7ff..e1a5cfa 100644
--- a/src/session.h
+++ b/src/session.h
@@ -276,6 +276,12 @@ struct serversession {
 	/* The instance created by the plugin_new function */
 	struct PluginInstance *plugin_instance;
 #endif
+
+#if DROPBEAR_SVR_DROP_PRIVS
+	/* Set to 1 when utmp_gid is valid */
+	int have_utmp_gid;
+	gid_t utmp_gid;
+#endif
 };
 
 typedef enum {
diff --git a/src/svr-auth.c b/src/svr-auth.c
index 46ba012..de01458 100644
--- a/src/svr-auth.c
+++ b/src/svr-auth.c
@@ -458,13 +458,14 @@ void send_msg_userauth_success() {
 	 * delayed-zlib mode */
 	ses.authstate.authdone = 1;
 
-#if DROPBEAR_DROP_PRIVS
+#if DROPBEAR_SVR_DROP_PRIVS
+	/* Drop privileges as soon as authentication has happened. */
 	svr_switch_user();
 #endif
 	ses.connect_time = 0;
 
 
-#if DROPBEAR_DROP_PRIVS
+#if DROPBEAR_SVR_DROP_PRIVS
 	/* If running as the user, we can rely on the OS
 	 * to limit allowed ports */
 	ses.allowprivport = 1;
@@ -483,6 +484,20 @@ void send_msg_userauth_success() {
 
 }
 
+#if DROPBEAR_SVR_DROP_PRIVS
+/* Returns DROPBEAR_SUCCESS or DROPBEAR_FAILURE */
+static int utmp_gid(gid_t *ret_gid) {
+	struct group *utmp_gr = getgrnam("utmp");
+	if (!utmp_gr) {
+		TRACE(("No utmp group"));
+		return DROPBEAR_FAILURE;
+	}
+
+	*ret_gid = utmp_gr->gr_gid;
+	return DROPBEAR_SUCCESS;
+}
+#endif
+
 /* Switch to the ses.authstate user.
  * Fails if not running as root and the user differs.
  *
@@ -500,6 +515,25 @@ void svr_switch_user(void) {
 						ses.authstate.pw_gid) < 0)) {
 			dropbear_exit("Error changing user group");
 		}
+
+#if DROPBEAR_SVR_DROP_PRIVS
+		/* Retain utmp saved group so that wtmp/utmp can be written */
+		int ret = utmp_gid(&svr_ses.utmp_gid);
+		if (ret == DROPBEAR_SUCCESS) {
+			/* Set saved gid to utmp so that it can be
+			 * restored for login_logout() etc. This saved
+			 * group is cleared by the OS on execve() */
+			int rc = setresgid(-1, -1, svr_ses.utmp_gid);
+			if (rc == 0) {
+				svr_ses.have_utmp_gid = 1;
+			} else {
+				/* Will not attempt to switch to utmp gid.
+				 * login() etc may fail. */
+				TRACE(("utmp setresgid failed"));
+			}
+		}
+#endif
+
 		if (setuid(ses.authstate.pw_uid) < 0) {
 			dropbear_exit("Error changing user");
 		}
@@ -517,3 +551,26 @@ void svr_switch_user(void) {
 	}
 }
 
+void svr_raise_gid_utmp(void) {
+#if DROPBEAR_SVR_DROP_PRIVS
+	if (!svr_ses.have_utmp_gid) {
+		return;
+	}
+
+	if (setegid(svr_ses.utmp_gid) != 0) {
+		dropbear_log(LOG_WARNING, "failed setegid");
+	}
+#endif
+}
+
+void svr_restore_gid(void) {
+#if DROPBEAR_SVR_DROP_PRIVS
+	if (!svr_ses.have_utmp_gid) {
+		return;
+	}
+
+	if (setegid(getgid()) != 0) {
+		dropbear_log(LOG_WARNING, "failed setegid");
+	}
+#endif
+}
diff --git a/src/svr-chansession.c b/src/svr-chansession.c
index 0a37fbf..11205f3 100644
--- a/src/svr-chansession.c
+++ b/src/svr-chansession.c
@@ -326,7 +326,11 @@ static void cleanupchansess(const struct Channel *channel) {
 	if (chansess->tty) {
 		/* write the utmp/wtmp login record */
 		li = chansess_login_alloc(chansess);
+
+		svr_raise_gid_utmp();
 		login_logout(li);
+		svr_restore_gid();
+
 		login_free_entry(li);
 
 		pty_release(chansess->tty);
@@ -847,7 +851,11 @@ static int ptycommand(struct Channel *channel, struct ChanSess *chansess) {
 		 * terminal used for stdout with the dup2 above, otherwise
 		 * the wtmp login will not be recorded */
 		li = chansess_login_alloc(chansess);
+
+		svr_raise_gid_utmp();
 		login_login(li);
+		svr_restore_gid();
+
 		login_free_entry(li);
 
 		/* Can now dup2 stderr. Messages from login_login() have gone
diff --git a/src/sysoptions.h b/src/sysoptions.h
index 32b0a13..9bdcb0c 100644
--- a/src/sysoptions.h
+++ b/src/sysoptions.h
@@ -358,6 +358,10 @@
 	#error "At least one hostkey or public-key algorithm must be enabled; RSA is recommended."
 #endif
 
+#if DROPBEAR_SVR_DROP_PRIVS && !defined(HAVE_SETRESGID)
+	#error "DROPBEAR_SVR_DROP_PRIVS requires setresgid()."
+#endif
+
 /* Source for randomness. This must be able to provide hundreds of bytes per SSH
  * connection without blocking. */
 #ifndef DROPBEAR_URANDOM_DEV
