As the Internet has grown, so has the number of savvy users. They want the ability to go beyond point-and-click and customize their software experience. Many of these advanced users want to write code, but they want programming to be easy, too. So, languages like Lua have sprouted up to fill the niche. Lua is both easy to use and embed in an application. So, it's shown up in everything from desktop applications and games to command-line utilities and consumer devices.
But there's a problem. When you embed a programming language, however trivial or simple, you add a new attack vector. It gives your users the ability to make your code do things you might not expect. Even with careful design and planning, scripting languages can become powerful weapons. For example, Lua has access to the system interface baked in. So, we've seen more than a few examples of Lua command injection attacks.
This post will cover the Lua language, command injection, and an example of Lua command injection in the wild. We'll wrap up with how you can protect yourself from this type of attack.
What Is Lua?
Lua is a high-level programming language, and it's easy to embed in applications.
Lua's designers wrote the interpreter in ANSI C, so it's cross-platform. It also compiles Lua source into bytecode, making it reasonably fast. The API for embedding the language into an app is straightforward, making it an excellent option for adding scripting capabilities to your code. It also has a tiny footprint, making it especially attractive for developers.
Lua has about 20 keywords and is remarkably user-friendly. It doesn't take long for a novice programmer to master Lua's simple grammar and syntax. One of its most popular client applications is Roblox, an online game for children.
Too many applications use Lua to list here. Its designers intended it to help customize applications, and they clearly succeeded. But there are a few examples worth pointing out.
PowerDNS supports Lua scripts for operation on DNS servers.
You can write Pandoc filters in Lua.
RPM, Redhat's packaging system, has an embedded Lua interpreter.
Adobe's Lightroom Classic has a Lua interface for plugins.
OpenResty is a platform for extending Nginx with Lua.
What Is Command Injection?
Command injection sends unexpected input to an application. The input executes arbitrary commands on the targeted systems. For example, if an application designed to evaluate arithmetic expressions doesn't verify its input, it could be coerced into executing a shell command. This kind of attack can result in a severe security breach. There's a detailed description of command injection in CWE-94.
A command injection attack relies on an application using input without verification or filtering. It starts with an app that accepts input with code syntax. This opens the possibility of an attacker giving the targeted app with code the designer didn't anticipate.
The best way to demonstrate command injection is with an example.
Lua Command Injection Example
This hub had (it's no longer available) a Lua interface for extending the device's capabilities. For example, the script host made it possible to write drivers for sensors and switches. Users could edit and submit scripts via a web interface. But, the API endpoint that accepted scripts had no authorization or authentication requirements. It didn't whitelist source addresses for submitting scripts, either. Any client could submit scripts directly to the device without logging into the interface.
While it was possible to submit a script without authentication, there was a mechanism to verify that scripts were safe. Unfortunately, the vendor didn't enable it. This article illustrates an exploit that added a new operating system user with a cURL command. The new account made it possible for an attacker to shell directly into the hub.
Command Injection Via Lua
Let's look at the example exploit.
Here's the Lua code passed to the controller:
os.execute("adduser -h /root -s /bin/ash testuser");os.execute("echo -e \"test\ntest\" | passwd testuser")
This command adds a new operating system account named testuser and then sets a password.
Lua's operating system library (os) exposes an interface to the underlying operating system. The os.execute() call runs a system command. This exploit shows that the controller was running Linux, and the Lua interface allowed scripts to run arbitrary commands with root access.
Preventing Lua Command Injection
There are several ways that the designers of the smart home hub could have avoided this attack.
The smart home hub accepted user input and executed it without verifying its contents, probably with Lua's loadstring() API call. That made it possible to inject a system API call that started a shell process and executed arbitrary code.
Accepting unverified input is especially dangerous with Lua because it has access to the underlying system interface. It's easy to execute a shell program with os.execute() or io.popen(), but it's just as easy to open sockets, create files, and perform other potentially nefarious tasks.
Different Lua applications accept input from a wide variety of sources, too. Many developers use Lua to customize user interfaces, write application extensions, and extend video games. In these cases, it's not difficult to imagine a user community inadvertently sharing a malicious piece of code via user forums or GitHub. All these interfaces need to verify that Lua scripts don't contain harmful code.
Use a Sandbox
While sanitizing inputs is always a good idea, a better idea is to protect your application and the system it's running on from malicious code. Lua doesn't run in a sandbox by default, but several options are available to make it safer.
One is to use a runtime sandbox. This method is similar to sanitizing input since it has the effect of whitelisting access to a subset of the Lua API. It's probably the easiest way to wrap scripts in your code in a protective shell.
The other approach is to modify the Lua source and remove the unsafe portions. Doing this before adding Lua to your code is more work, but it's also the safest approach. You reduce the attack surface when you comment out the unsafe calls.
Enforce Proper Access Controls
Even without sanitizing input or running in a sandbox, there was at least one more way this application could have avoided the command injection attack; by not allowing Lua scripts to have privileged access. The attack relied on running the adduser command and then running passwd to modify another user's password entry. These activities require root access to the underlying operating system, so the webserver on the smart home hub was likely running as the root user. This level of access wasn't necessary for any of the tasks it was responsible for. Most modern Linux distributions create unprivileged users to run services like web servers and databases.
Avoid Command Injection Attacks
In this post, we examined Lua command Injection attacks. We started with an overview of the popular Lua scripting language and what command injection is. Then, we reviewed a real-world Lua command injection attack. Finally, we looked at three different ways the attack and ones similar to it can be avoided.
Now that you have a better understanding of command injection attacks and how to prevent them in Lua look at your code. Take a look at StackHawk and see how it can help you protect your application. There's a free plan with unlimited scans that you can put t work today!
This post was written by Eric Goebelbecker. Eric has worked in the financial markets in New York City for 25 years, developing infrastructure for market data and financial information exchange (FIX) protocol networks. He loves to talk about what makes teams effective (or not so effective!).