Skip to content

Commit 970812e

Browse files
authored
Fix arbitrary command execution/code injection bugs (#866)
* Security patch part 1 Never use $SHELL or sh.shpath when executing shebang-less scripts - sh_ntfork(): Removed a misguided optimization that causes ksh to run scripts without a shebang using the binary pointed to by either $SHELL or sh.shpath. This has a few problems: - The shell in sh.shpath or more disastrously in $SHELL has no guarantee of being identical to the currently running copy of ksh93 or even existing at all, so using either is not only bogus, but potentially dangerous. - The optimization has no ability to pass down the current POSIX mode status to the script. - It's only activated for virtual subshells, resulting in arbitrarily different behavior depending on whether or not we're in a virtual subshell. - It does some weird stuff with /dev/fd that seems superfluous, and also lacks any SHOPT_DEVFD if directive. (Additionally, if it did have one, that stat(2) would likely become mere dead code and a waste of context switches.) The optimization was probably intended to be used for running a shebang-less script via posix_spawn, which is ostensibly faster than fork. But this simply isn't possible without risking running the shebang-less script in a shell environment different from the current one. (If ksh were updated by the package manager while ksh is still running, this optimization would cause the script to run via the new version, rather than the currently running older version.) The optimization is unfixable by design, and as such must be removed to ensure correct behavior. * Security patch part 2 (re: bae02c3) rm setuid script code leading to arbitrary command execution Changes: - Delete code for setuid scripts on "Solaris 2.5+" because it allows for arbitrary command execution. One millisecond you think you're launching ksh, the next you're at the mercy of a hijacker. Example: SHELL=/bin/ppmflash /bin/ksh -l /dev/fd/0 < <(true) MANPATH: usage: MANPATH flashfactor [ppmfile] flashfactor: 0.0 = original picture, 1.0 = total whiteout The pathshell() code doesn't *seem* to be vulnerable to privilege escalation, but who knows (cf. CVE-2019-14868; this might need its own CVE 2025. Maybe pathshell() should be scrapped entirely???) - Add fickle but functional regression test (you may need to pass KSH=/bin/ksh or some such to get it to fail against vulnerable versions of ksh). The test uses a login shell via the -l option, but the bug *does not* need a login shell. See: #874 (comment) Modify the execveat reproducer to pass along environ (which could include a hijacked SHELL), and you're in for a BAD time. Maybe the deleted code (introduced sometime within the period of 1995 and 1999) was relevant to some Solaris-specific use case or something. Maybe the erasure even causes an incompatibility. But that code must go; it's far too dangerous to execv whatever the hell pathshell gives us during **init**. (Need I bring CVE-2019-14868 back to remembrance again? This bug has similarities to that one.) FWIW, all of the regression tests in the ksh and modernish suites pass with this patch applied. * Security patch part 3 Delete pathshell() and replace uses of it with safer equivalents This function is a dangerous attack vector that ought not remain in the code base. The value returned by astconf() is doubtless safer than what is returned by pathshell(). * Other changes The libast pathprog function and prog feature test are now unused, and are removed.
1 parent 9e2e6ca commit 970812e

File tree

19 files changed

+53
-374
lines changed

19 files changed

+53
-374
lines changed

NEWS

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@ This documents significant changes in the dev branch of ksh 93u+m.
22
For full details, see the git log at: https://github.com/ksh93/ksh
33
Uppercase BUG_* IDs are shell bug IDs as used by the Modernish shell library.
44

5+
2025-06-14:
6+
7+
- Fixed a bug occurring on systems with posix_spawn(3), spawnve(2), and
8+
spawn(2), which when exploited could cause ksh93 to execute scripts
9+
without a shebang with a shell other than itself, provided /bin/ksh
10+
was hijacked, or in the case of ksh being unable to obtain it's own
11+
executable path, whatever the SHELL variable was set to.
12+
13+
- Fixed a bug that could cause scripts without a shebang to run
14+
without POSIX mode enabled when executed in a virtual subshell.
15+
16+
- Fixed a bug that could allow an attacker to hijack ksh93 during
17+
shell initialization to execv a shell other than ksh itself, leading to
18+
the ksh process being surreptitiously replaced with bash, zsh, or
19+
really anything, provided the binary name ends with the letters
20+
'sh'.
21+
522
2025-05-31:
623

724
- Fixed a bug that caused the multiline option to break when PS*

src/cmd/ksh93/include/shell.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,6 @@ struct sh_scoped
225225
char **otrapcom; /* save parent EXIT and signals for v=$(trap) */
226226
void *timetrap; /* for the 'alarm' built-in */
227227
struct Ufunction *real_fun; /* current 'function name' function */
228-
int repl_index;
229-
char *repl_arg;
230228
};
231229

232230
struct limits
@@ -269,7 +267,6 @@ struct Shell_s
269267
Namval_t *bltin_nodes;
270268
Namval_t *bltin_cmds;
271269
History_t *hist_ptr;
272-
char *shpath;
273270
char *user;
274271
char **sigmsg;
275272
char **login_files;

src/cmd/ksh93/include/version.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#include <ast_release.h>
1919
#include "git.h"
2020

21-
#define SH_RELEASE_DATE "2025-06-09" /* must be in this format for $((.sh.version)) */
21+
#define SH_RELEASE_DATE "2025-06-14" /* must be in this format for $((.sh.version)) */
2222
/*
2323
* This comment keeps SH_RELEASE_DATE a few lines away from SH_RELEASE_SVER to avoid
2424
* merge conflicts when cherry-picking dev branch commits onto a release branch.

src/cmd/ksh93/sh/init.c

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,34 +1266,6 @@ Shell_t *sh_init(int argc,char *argv[], Shinit_f userinit)
12661266
}
12671267
/* increase SHLVL */
12681268
sh.shlvl++;
1269-
#if SHOPT_SPAWN
1270-
{
1271-
/*
1272-
* try to find the pathname for this interpreter
1273-
* try using environment variable _ or argv[0]
1274-
*/
1275-
char *cp=nv_getval(L_ARGNOD);
1276-
char buff[PATH_MAX+1];
1277-
size_t n;
1278-
sh.shpath = 0;
1279-
if((n = pathprog(NULL, buff, sizeof(buff))) > 0 && n <= sizeof(buff))
1280-
sh.shpath = sh_strdup(buff);
1281-
else if((cp && (sh_type(cp)&SH_TYPE_SH)) || (argc>0 && strchr(cp= *argv,'/')))
1282-
{
1283-
if(*cp=='/')
1284-
sh.shpath = sh_strdup(cp);
1285-
else if(cp = nv_getval(PWDNOD))
1286-
{
1287-
int offset = stktell(sh.stk);
1288-
sfputr(sh.stk,cp,'/');
1289-
sfputr(sh.stk,argv[0],-1);
1290-
pathcanon(stkptr(sh.stk,offset),PATH_DOTDOT);
1291-
sh.shpath = sh_strdup(stkptr(sh.stk,offset));
1292-
stkseek(sh.stk,offset);
1293-
}
1294-
}
1295-
}
1296-
#endif
12971269
nv_putval(IFSNOD,(char*)e_sptbnl,NV_RDONLY);
12981270
astconfdisc(newconf);
12991271
#if SHOPT_TIMEOUT
@@ -1306,7 +1278,6 @@ Shell_t *sh_init(int argc,char *argv[], Shinit_f userinit)
13061278
#endif
13071279
if(argc>0)
13081280
{
1309-
int dolv_index;
13101281
/* check for restricted shell */
13111282
if(type&SH_TYPE_RESTRICTED)
13121283
sh_onoption(SH_RESTRICTED);
@@ -1318,10 +1289,7 @@ Shell_t *sh_init(int argc,char *argv[], Shinit_f userinit)
13181289
sh_done(0);
13191290
}
13201291
opt_info.disc = 0;
1321-
dolv_index = (argc - 1) - sh.st.dolc;
1322-
sh.st.dolv = argv + dolv_index;
1323-
sh.st.repl_index = dolv_index;
1324-
sh.st.repl_arg = argv[dolv_index];
1292+
sh.st.dolv = argv + (argc - 1) - sh.st.dolc;
13251293
sh.st.dolv[0] = argv[0];
13261294
if(sh.st.dolc < 1)
13271295
{

src/cmd/ksh93/sh/main.c

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -221,33 +221,12 @@ noreturn void sh_main(int ac, char *av[], Shinit_f userinit)
221221
/* open stream should have been passed into shell */
222222
if(strmatch(name,e_devfdNN))
223223
{
224-
#if !_WINIX
225-
char *cp;
226-
int type;
227-
#endif
228224
fdin = (int)strtol(name+8, NULL, 10);
229225
if(fstat(fdin,&statb)<0)
230226
{
231227
errormsg(SH_DICT,ERROR_system(1),e_open,name);
232228
UNREACHABLE();
233229
}
234-
#if !_WINIX
235-
/*
236-
* try to undo effect of Solaris 2.5+
237-
* change for argv for setuid scripts
238-
*/
239-
if(sh.st.repl_index > 0)
240-
av[sh.st.repl_index] = sh.st.repl_arg;
241-
if(((type = sh_type(cp = av[0])) & SH_TYPE_SH) && (name = nv_getval(L_ARGNOD)) && (!((type = sh_type(cp = name)) & SH_TYPE_SH)))
242-
{
243-
av[0] = (type & SH_TYPE_LOGIN) ? cp : path_basename(cp);
244-
/* exec to change $0 for ps */
245-
execv(pathshell(),av);
246-
/* exec fails */
247-
sh.st.dolv[0] = av[0];
248-
fixargs(sh.st.dolv,1);
249-
}
250-
#endif
251230
name = av[0];
252231
sh_offoption(SH_VERBOSE);
253232
sh_offoption(SH_XTRACE);

src/cmd/ksh93/sh/path.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,8 +1221,6 @@ pid_t path_spawn(const char *opath,char **argv, char **envp, Pathcomp_t *libpath
12211221
*/
12221222
if(spawn)
12231223
{
1224-
if(sh.subshell)
1225-
return -1;
12261224
do
12271225
{
12281226
if((pid=fork())>0)

src/cmd/ksh93/sh/xec.c

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3365,26 +3365,6 @@ static pid_t sh_ntfork(const Shnode_t *t,char *argv[],int *jobid,int topfd)
33653365
job_fork(-1);
33663366
jobfork = 1;
33673367
spawnpid = path_spawn(path,argv,arge,pp,(grp<<1)|1);
3368-
if(spawnpid < 0 && errno==ENOEXEC)
3369-
{
3370-
char *devfd;
3371-
int fd = open(path,O_RDONLY);
3372-
argv[-1] = argv[0];
3373-
argv[0] = path;
3374-
if(fd>=0)
3375-
{
3376-
struct stat statb;
3377-
sfprintf(sh.strbuf,"/dev/fd/%d",fd);
3378-
if(stat(devfd=sfstruse(sh.strbuf),&statb)>=0)
3379-
argv[0] = devfd;
3380-
}
3381-
if(!sh.shpath)
3382-
sh.shpath = pathshell();
3383-
spawnpid = path_spawn(sh.shpath,&argv[-1],arge,pp,(grp<<1)|1);
3384-
if(fd>=0)
3385-
close(fd);
3386-
argv[0] = argv[-1];
3387-
}
33883368
fail:
33893369
if(jobfork && spawnpid<0)
33903370
job_fork(-2);

src/cmd/ksh93/tests/basic.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,5 +1084,33 @@ wait "$parallel_5" || err_exit 'command substitution causes pipefail option to h
10841084
wait "$parallel_6" || err_exit '"command | while read...done" finishing too fast'
10851085
wait "$parallel_7" || err_exit 'early termination not causing broken pipe'
10861086
1087+
# ======
1088+
# Hijacking ksh93 via $SHELL for arbitrary command execution during initialization.
1089+
# https://github.com/ksh93/ksh/issues/874
1090+
bindir=$tmp/dir.$RANDOM/bin
1091+
mkdir -p "$bindir"
1092+
echo $'#!/bin/sh\necho "CODE INJECTION"' > "$bindir"/hijack_sh
1093+
chmod +x "$bindir"/hijack_sh
1094+
got=$(set +x; SHELL="$bindir"/hijack_sh "$SHELL" -l <(echo) 2>&1)
1095+
[[ $got =~ "CODE INJECTION" ]] && err_exit 'ksh93 is vulnerable to being hijacked during init via $SHELL' \
1096+
"(got $(printf %q "$got"))"
1097+
1098+
# Hijacking ksh93 shebang-less scripts for arbitrary command execution.
1099+
# https://github.com/ksh93/ksh/pull/866
1100+
export bindir
1101+
print 'echo GOOD' > "$bindir/dummy.sh"
1102+
chmod +x "$bindir/dummy.sh"
1103+
cp "$SHELL" "$bindir/hijack_sh"
1104+
exp=$'GOOD\nGOOD'
1105+
got=$("$bindir/hijack_sh" -c $'print $\'\#!/bin/sh\necho HIJACKED\' > "$bindir/hijack_shell"
1106+
chmod +x "$bindir/hijack_shell"
1107+
rm "$bindir/hijack_sh"
1108+
cp "$bindir/hijack_shell" "$bindir/hijack_sh"
1109+
("$bindir/dummy.sh"); "$bindir/dummy.sh"; :')
1110+
rm -r "$bindir"
1111+
unset bindir
1112+
[[ $exp == $got ]] || err_exit 'ksh93 shebang-less scripts are vulnerable to being hijacked for arbitrary code execution' \
1113+
"(exp $(printf %q "$exp"), got $(printf %q "$got"))"
1114+
10871115
# ======
10881116
exit $((Errors<125?Errors:125))

src/cmd/ksh93/tests/posix.sh

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,13 @@ got=$(<out)
8181
[[ $got == "$exp" ]] || err_exit "incorrect --posix settings on invoking hashbangless script from posix shell (direct)" \
8282
"(expected $(printf %q "$exp"), got $(printf %q "$got"))"
8383

84-
: <<\DISABLED # TODO: these do not pass yet, unless SHOPT_SPAWN is disabled.
8584
(./script) >|out
8685
got=$(<out)
8786
[[ $got == "$exp" ]] || err_exit "incorrect --posix settings on invoking hashbangless script from posix shell (subshell)" \
8887
"(expected $(printf %q "$exp"), got $(printf %q "$got"))"
8988
got=$(./script)
9089
[[ $got == "$exp" ]] || err_exit "incorrect --posix settings on invoking hashbangless script from posix shell (comsub)" \
9190
"(expected $(printf %q "$exp"), got $(printf %q "$got"))"
92-
DISABLED
9391

9492
got=$("$SHELL" --posix -c "$(<script)")
9593
[[ $got == "$exp" ]] || err_exit "incorrect --posix settings on invoking -c script from posix shell" \

src/lib/libast/Mamfile

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -322,11 +322,6 @@ make install virtual
322322
exec - invoke_iffe %{<}
323323
done
324324

325-
make FEATURE/prog
326-
makp features/prog
327-
exec - invoke_iffe %{<}
328-
done
329-
330325
make FEATURE/locale
331326
makp features/locale
332327
exec - invoke_iffe %{<}
@@ -1222,13 +1217,6 @@ make install virtual
12221217
exec - compile %{<}
12231218
done
12241219

1225-
make pathshell.o
1226-
make path/pathshell.c
1227-
prev include/ast.h
1228-
done
1229-
exec - compile %{<}
1230-
done
1231-
12321220
make pathcd.o
12331221
make path/pathcd.c
12341222
prev include/stk.h
@@ -1238,15 +1226,6 @@ make install virtual
12381226
exec - compile %{<}
12391227
done
12401228

1241-
make pathprog.o
1242-
make path/pathprog.c
1243-
prev FEATURE/prog
1244-
prev include/ast_windows.h
1245-
prev include/ast.h
1246-
done
1247-
exec - compile %{<}
1248-
done
1249-
12501229
make ftwalk.o
12511230
make misc/ftwalk.c
12521231
make include/ftwalk.h

0 commit comments

Comments
 (0)