This was covered in Linux PrivEsc, task 15, in this TryHackMe room.

I am having trouble understanding how this debugging mode is executing the commands in the PS4 variable, and why I must put /usr/local/bin/suid-env2 instead of another path at the end of the command?

Here is a copy of the room's content:

Note: This will not work on Bash versions 4.4 and above.

When in debugging mode, Bash uses the environment variable PS4 to display an extra prompt for debugging statements.

Run the /usr/local/bin/suid-env2 executable with bash debugging enabled and the PS4 variable set to an embedded command which creates an SUID version of /bin/bash:

env -i SHELLOPTS=xtrace PS4='$(cp /bin/bash /tmp/rootbash; chmod +xs /tmp/rootbash)' /usr/local/bin/suid-env2

Run the /tmp/rootbash executable with -p to gain a shell running with root privileges:

/tmp/rootbash -p

2 Answers 2


Traces that are enabled by bash -x (or the long form bash -o xtrace) are formatted according to the PS4 variable. The content of PS4 undergoes prompt expansion, and unless the option promptvars is turned off, this includes variable and command substitutions.

$ bash -x -c 'echo world'
+ echo world
$ PS4='$(echo hello)' bash -x -c 'echo world'
helloecho world

It's possible to use this as part of privilege escalation, but the circumstances where this would be useful are unusual. There has to be a bash script that runs with elevated privileges, with an environment that's under the attacker's control. When that's the case, it's usually game over: there are lots of other environment variables that can allow arbitrary command execution, such as PATH, LD_LIBRARY_PATH, LD_PRELOAD, etc. The recommended way to run a program with additional privileges is via sudo, which takes care of scrubbing the environment except for a whitelist which wouldn't include PS4 unless the administrator deliberately added it.

suid-env2 is something that was created specifically for the exploit to be possible. It's apparently a setuid root program that runs bash wihtout cleaning up the environment, which as I explained above is a well-known security hole that can be exploited in many ways. The bit about /tmp/rootbash is a convoluted and conspicuous way of making the exploit persistent (i.e. of gaining a way to elevate privileges if the original exploit is fixed without completely scrapping the existing instance of the vulnerable system).

  • So the reason I had to use /usr/local/bin/suid-env2 is because the suid-env2 is run with the sticky bit and when it runs bash it does not clear up the environment? In regards to the part about making the exploit persistent at the end, why did you say "without completely scrapping the existing instance of the vulnerable system"?
    – questioner
    Commented Jul 13, 2022 at 16:46
  • 1
    @questioner Setuid bit, not sticky bit, but otherwise, yes. About making the system persistent: any competent administrator will reinstall the system from scratch (“nuke it from orbit”) when they detect a potential compromise. But if they don't (for example because they eliminate the potential vulnerability but don't realize it's been exploited), the attacker wants to keep a way to stay in. A setuid binary is really easy to detect though, and pretty silly when there are much more discreet off-the-shelf rootkits. Commented Jul 13, 2022 at 17:37

The decompiled suid-env2 code is

undefined8 main(void)
  system("/usr/sbin/service apache2 start");
  return 0;

Seems the target executable has to call setresuid(0,0,0) first before calling system() for this exploit to work. But I still don't know the exact reason.

When a non-privileged user runs a SUID program, the process will inherit the user's uid as real uid and inherit file owner's uid as effective uid, in this case, real uid is 1000 and effective uid is 0.

Processes with 0 effective uid can execute a lot of privileged commands, but according to man 2 access, open a file requires real UID also be 0.

The check is done using the calling process’s real UID and GID, rather than the ef‐ fective IDs as is done when actually attempting an operation (e.g., open(2)) on the file.

That explains why PS4=$(cp /bin/bash /tmp/rootbash) doesn't work without setresuid(0,0,0), because cp uses open() system call.

Last, bash 4.4 fixed this exploit by preventing privileged processes inherit PS4 from environment

Shells running as root no longer inherit PS4 from the environment, closing a security hole involving PS4 expansion performing command substitution. https://github.com/bminor/bash/blob/a99d905216cc0aac5de0c3050f4afc54e21c6bc5/CHANGES#L2333-L2334

  • Someone with expertise will edit this diamond in the rough answer one day and it will be a complete blood diamond! Commented Nov 13, 2023 at 19:12

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .