If you own a consumer-grade network router then you have likely used a web browser to configure the router and set up your network. For commercial and industrial applications networking devices typically implement a command-line interface, more commonly just called a CLI. When you hear CLI don’t think “bash”… you’ll find that most network equipment vendors implement specialized CLIs that aren’t anything like a Bash shell. CLIs are often, though not always, intended to be used by authenticated users with different levels of authorization. For example the network router
adminmay be able to modify any setting but the
oper user should only be able to monitor and restart the device if needed. The CLI’s role in implementing authorization controls makes it a target for privilege escalation attacks to turn a
oper user into an
adminand ultimately a
root user in the underlying OS.
CLIs are a subset of the ever popular “restricted shell”. The idea of a restricted shell is that one wishes to grant a non-
root user direct access to a shell, such as Bash, but desire for them to only be able to execute a small subset of the available commands. Bash itself supports a limited degree of restricted execution, but I would not recommend that path. Bash, and other shells, are complex creatures which have evolved over decades. They are complex and most developers (including me!) would be hard pressed to claim a complete grasp of every Bash feature and capability. For example, you can use Bash to do genuinely weird things such as opening TCP sockets.
Developers who are fearful of Bash restricted shells will at times turn to building shells in a scripting language such as Python. The central idea is to use the Python REPL (or a custom REPL) to execute “commands” which are really implemented as Python functions. Knowing that this is a dangerous setup, developers will attempt to build sandboxes to execute untrusted Python code. Sandboxing is a good approach, but the difficulties of implementing a successful language level sandbox in Python are well documented. We’ve worked on production systems that carefully built Python sandboxes, only to have them popped open by a few mistakes.
The most secure CLIs that I’ve worked with tend to be backed by a very strong model of what the CLI is being used to configure. Strong configuration models are useful because they can be used for input validation. One such model I’ve observed in production systems is YANG (RFC 6020). YANG alone won’t protect against all risks, however. There are plenty of dangerous strings which might get past a YANG level validation only to be problematic when interpreted by other software.
During a security assessment what really draws my attention are those CLI commands which pass values to a OS command. Interfaces between a CLI and an external binary are ripe for OS command injection. Focus time and attention on how parameters are passed to applications. For goodness sake… please don’t ever invoke sub-commands using a shell. That’s just asking for trouble.
Sometimes, even when developers are doing their best to avoid OS command injection, they shoot themselves in the foot by letting an attacker invoke _dangerous commands_. My favorite example of this comes from a security assessment of a quite expensive industrial router. The CLI is tight. The device was actively maintained, up to date, and actually pretty good. Whenever the web app or CLI invoked a command, they used a clever wrapper that securely passed parameters; it avoided invoking a sub-shell, and dropped to a low-privilege user before it executed the binary. Then I ran
help and saw that the device permitted the authenticated user to execute the
ssh is a dangerous command to let untrusted users execute, because unless properly constrained the
ssh command permits a user to open a non-restricted sub-shell. Use
ssh from the CLI to connect anywhere:
ssh -o PermitLocalCommand=yes -e ~ firstname.lastname@example.org
and then escape to a shell with this simple sequence:
- Enter a newline.
- Enter the
sshescape sequence to start the
- Launch the OS shell:
There are numerous commands that are just too risky to let an untrusted user execute:
tee to name a few. On this particular box escalating to
root was more difficult than usual. Most “embedded Linux” boxes make it trivial to find a path to
root. To finish this one off, we used what is not my new favorite exploit, dirty cow. Dirty Cow is a gift that will keep giving for years.
The moral of this story for developers is that implementing secure CLIs is harder than you might otherwise think. Carve’s design guidance for developers includes:
- Be very careful about any CLI commands which invoke processes.
- White-list commands and the arguments to those commands.
- Use defense in depth to protect against unanticipated paths to restricted shells.
- Carefully audit filesystem contents to ensure that the base configuration is secure and that unnecessary binaries and privileges are removed.