I found /usr/bin/lesspipe.sh and ~/.lessfilter.

ls -al /usr/bin/lesspipe.sh
-rwxr-xr-x. 1 root root 3622 Sep  1 14:25 /usr/bin/lesspipe.sh

⚠️ This post is in no way detailing a vulnerability or exploit. I am sharing a novel persistence mechanism that I was not aware of until recently. I am writing this post because I do not believe many defenders — SOC, DFIR and system administrators alike — know about this. Not MITRE ATT&CK nor GTFOBins.com include this level of detail in their respective repositories, either.

For the tl;dr see the conclusion below.

Re-Introducing the less Command

The less program, normally located at /usr/bin/less, is a well-known pager that comes prebaked in *nix operating systems. The less program allows a user to view a file via mouse scroll, to regex search, and even list the contents of compressed archives. We’ll talk more about that last part later.

Recently I was perusing my environment variables (envar) on my Linux-based system and found this “.sh” file that was just screaming for me to investigate further.

env
...skipping
LESSOPEN=||/usr/bin/lesspipe.sh %s
PAGER=less
...skipping

Upon opening the shell script, it became apparent lesspipe.sh is a helper file to “filter the output” that less presents to the user. After reading the lesspipe man page it was clear that less calls /usr/bin/lesspipe.sh by default. Additionally, lesspipe.sh will attempt to list the contents of various types of compressed archives (ie. gz, bz2, lz, xz, and more).

(Care)less Functionality

The less command is considered a LOLBin (Living-off-the-Land Binary) but not just in the ways described by GTFOBins. I have found two significant attack vectors that were not widely understood, until now.

  1. less any_file calls the LESSOPEN envar; default value /usr/bin/lesspipe.sh
  2. less any_compressed_file will list the contents of a compressed file
  3. (Off-by-one error) The plethora of envars and files called on every execution of less

LESSOPEN and lesspipe.sh

As seen below less will always call what is stored in the LESSOPEN envar. By default this will be /usr/bin/lesspipe.sh; this is set by the distributions, not the program itself. lesspipe.sh will then check if ~/.lessfilter exists, and will then either handle the file or give control back to lesspipe.sh if the file does not exist. But more on ~/.lessfilter later.

#lesspipe.sh is called during every run of less
echo "HELLO WORLD" > hi.txt

strace -f less hi.txt
#make sure to include -f to follow the fork
54624 execve("/bin/sh", ["sh", "-c", "--", "/usr/bin/zsh -c /usr/bin/lesspip"...], 0x7ffc4beebbd0 /* 93 vars */ <unfinished ...>
54624 execve("/usr/bin/zsh", ["/usr/bin/zsh", "-c", "/usr/bin/lesspipe.sh hi.txt"], 0x5575e0a41b00 /* 93 vars */) = 0
54624 execve("/usr/bin/lesspipe.sh", ["/usr/bin/lesspipe.sh", "hi.txt"], 0x7ffcbec20e28 /* 93 vars */) = 0

ltrace -f less hi.txt
...skipping
getenv("LESSOPEN")                              = "||/usr/bin/lesspipe.sh %s"
...
getenv("LESSHISTFILE")                          = nil
strlen(".lesshst")                              = 7
...
getenv("LESSMETAESCAPE")                        = nil
...

less for Compression

The fact that less will list files in compressed files via known file extensions is great for attackers because this blends into normal execution logs a defender may review. For example, to the defender less compressed.bz2 may look like a lesser-skilled power user decided to open a compressed file (most defenders I talked to when writing this did not know of this capability.) But in reality, the attacker could have overwrote the functionality, doing something more nefarious. Even to a more seasoned analyst that would read the man page it would show this as a feature and would probably think nothing more of it.

#Excerpt from /usr/bin/lesspipe.sh
	case "$1" in
	*.gz)           DECOMPRESSOR="gzip -dc" ;;
	*.bz2)          DECOMPRESSOR="bzip2 -dc" ;;
	*.lz)           DECOMPRESSOR="lzip -dc" ;;
	*.zst)          DECOMPRESSOR="zstd -dcq" ;;
...

less’ Sprawl

Code sprawl, also known as the Swiss Army Knife anti-pattern, occurs when software accumulates functionality far beyond its original purpose, creating security risks and maintenance burdens. This feature creep transforms a simple text pager into a complex system with multiple execution paths, each representing potential security vulnerabilities.

My analysis found about 30 unnecessary instances of sprawl found in environment variables and files used during a less execution. Some are obvious, like LESSOPEN which points to preprocessing scripts. Others are subtle and dangerous, like .lessfilter, which provides a user-controlled execution vector that most users don’t know exists.

The .lessfilter file is the intended file for users to populate for handling custom preprocessing steps. This file is just a bash script that a user creates in their home directory. Unlike LESSOPEN, this file is not prepopulated on systems nor is there any mention of it in the online less man page. Also, note the hidden nature of the file with the prefixed ‘.’ to the file name so ls listings will miss it. Evenmore, this file is not known from my testing to be prepopulated on any operating system version.

One caveat, the mention of ~/.lessfilter in the man page is OS-dependant. There is no mention of it in the Ubuntu or Debian versions of the man page. However it is explained in the Fedora and CentOs man pages.

For how best to create your own, see the lesspipe man page.

To recap, the ~/.lessfilter file is a user-controlled shell script that will run any time the user runs less. Also, this is an undocumented (within the less man page) feature. So if ~/.lessfilter file exists in your network– absent of any less power users, it should be reviewed by defenders.

less’ Lesser Known Features

The less man pages also describes the LESSCLOSE envar. This dictates what occurs on exit of a less session. This is not always set by default, though it can be set to run a bash script.

Another fun feature of less is that it has history (~/.lesshst by default). That’s right. Another history file defenders have to sort through- and attackers should delete to hide their tracks. Though, one could put less in secure mode which disables the history file and prevents shell escapes (":! cmd"), along with other features.

The LESS_IS_MORE envar will force less to behave like the more(1) POSIX command.

Man page: https://www.man7.org/linux/man-pages/man1/less.1.html.

less is Everyone’s Favorite Pager

The importance of this finding should not be understated due to its ubiquity in both various operating systems and its use by other common programs most certainly on your systems. less is used by many of your other favorite Unix utilities as it is the default PAGER on most systems.

  • How do you view the manual pages for commands (man, perldoc, python -m pydoc)? Depends on the PAGER or MANPAGER envars that are usually set to less
  • What is the goto pager that runs with git log, git diff, git help, or git show? Git actually uses whatever value is in git config core.pager or GIT_PAGER envar. Those by default are set to less.
  • When you want to know about system status via systemctl status or journalctl? These get their pager from the SYSTEMD_PAGER or PAGER envar. Also the default being less.

Be aware, these programs call less as their PAGER but they will NOT call /usr/bin/lesspipe.sh or even LESSOPEN.

strace -f man less
* /usr/bin/.sysless /etc/syslesskey /etc/sysless ~/.config/lesskey ~/.lesskey

strace -f systemctl status docker
* checks only ~/.config/less ~/.less ~/.lesskey ~/.config/lesskey

strace -f git diff #in .git project
* checks only /usr/bin/.sysless /etc/syslesskey /etc/sysless ~/.config/lesskey ~/.lesskey ~/.config/less ~/.less ~/.less

#None of these call ~/.lessfilter or /usr/bin/lesspipe.sh

The Why: Possible TTPs

As a red teamer, what can be surreptitiously affected by these tactics? Here are some ideas which will not tip off a user or an EDR product. Limited testing has been done so YMMV.

A) Add your own command to usr/bin/lesspipe.sh or any of the less-related envars, as long as it does not print to standard output and alert the user.

What not to do
How not to use this persistence mechanism

B) Replace a specific filetype’s list command with something worse. Ie, change this bzip2 to an attacker controlled shell script. This is similar to HKEY_Current_User fileExts registry key on Windows.

#Excerpt from /usr/bin/lesspipe.sh
*.bz2) DECOMPRESSOR="bzip2 -dc" ;;

#Now any bz2 file less tries to read will run bad.sh instead
*.bz2) DECOMPRESSOR="/tmp/bad.sh" ;;

This part about less about being able to list compressed files is a more appropriate way to hide from defenders. Usually a log file showing the execution of less compressed_file.zip would blend in. Unless, the attacker does not drop an obvious stager to be detected by low-hanging fruit detections (my rant on TTPs over static indicators will be saved for another blog post).

C) Change the envar values of LESSOPEN or LESSCLOSE

D) I am sure there are many other ways to abuse this LOLBin I will leave as an exercise for the hackers :)

All of this functionality from the previous section and chained file executions are built into less on a majority of operating systems. I successfully tested those capabilities on the following OS’s: Debian, Ubuntu, Fedora, CentOS, and Raspberry Pi OS.

How to use this Knowledge

Defenders: Add the following checks to your scans, hunts, AVs, or whatever it is your organization uses to scan en masse.

  • Check key envars (LESSOPEN LESSCLOSE LESSHISTFILE LESSECURE) by looping through proc/$pid/environ files; I counted over 30 distinct envars should be checked
  • Diff with a trusted version of /usr/bin/lesspipe.sh in case of modifications
  • find / -xdev -name .lessfilter

Attackers: ~/.lessfilter and /usr/bin/lesspipe.sh

  • ~/.lessfilter is meant for users to make custom preprocessing actions. Hence the file is not owned by root
  • /usr/bin/lesspipe.sh is root-owned and called by default on less installations
ls -al /usr/bin/lesspipe.sh
-rwxr-xr-x. 1 root root 3622 Sep  1 14:25 /usr/bin/lesspipe.sh
  • Even if a user tries to read a binary (ex. png) and then sends “n” or Ctl+c, the preprocessors will still be called.
less image.png
"image.png" may be a binary file.  See it anyway?

Conclusion

The program less is already considered a Living-off-the-Land Binary, though there is much more to the program that is not widely known within the security space. Specifically, its filtering procedure includes lesspipe.sh and .lessfilter. These features, along with the other functionality detailed above, could be used for malicious purposes.

Defenders need to be wary of:

  1. The less binary being modified
  2. The LESSOPEN envar deviating from its default LESSOPEN=||/usr/bin/lesspipe.sh %s
  3. The /usr/bin/lesspipe.sh script being modified
  4. The ~/.lessfilter script existing
  5. The LESSCLOSE envar. Though it is not necessarily indicative of malicious actions, the mere presence of it should be reviewed. The LESSCLOSE envar is also not set by default.
  6. (Bonus) The less history file, LESSHISTFILE
  7. (Bonus) The less secure mode, LESSSECURE
  8. (Bonus) The recent command injection vulnerability, CVE-2024-32487