One thing I like to do whenever I get bored is dig through my boxes of old IoT equipment to find something interesting to play around with. In this case, that something was a Microhard BulletLTE-NA2 LTE bridge device that I still had from an audit I did a while back. The last time I investigated this device, I focused exclusively on the web management portal and managed to find one or two decent vulnerabilities before the project was over. Those vulnerabilities were fixed by Microhard in firmware update v1.2.0-r1112. This time around, I wanted to assess something different, so I decided to focus on the service that was listening on the telnet port.
The Device
The Microhard BulletLTE-NA2 is a bridge device designed to add cellular connectivity to ethernet or serial devices in order to facilitate remote management and communication. It is used in SCADA/ICS environments, cellular infrastructure, power utilities, and more. From a hardware perspective, the BulletLTE-NA2 is composed of an Atmel SAMA5D35 ARM Cortex-A5 32bit processor, 256M RAM, 128M of SPI flash storage, and a Quectel EC25-AF mini PCIe card for cellular. It has a slot for a sim card, a PoE ethernet port, and both USB and serial ports.
And on the software side, the update files are composed of a boot image, a Linux 3.6.9 ARM kernel, and a full squashfs file system which is everything you would need to run this system in emulation for testing purposes. The OS appears to be a heavily modified version of OpenWRT, and it uses uhttpd for web management, dropbear for ssh, and telnetd for telnet. Connecting to telnet puts the user in a heavily restricted debug shell and entering a “?” at the input prompt returns a list of a small number of basic networking configuration options as well as many AT commands, presumably for interacting with the Quectel module. Attempting to run any command other than the ones that are specifically allowed results in an “Invalid command” error and is ignored. Let’s take a closer look at how that debug shell works to see if we can find a way around that restriction.
Clitest
Examining the squashfs file system that was included in the firmware updates showed that when the admin user logged into the system, it would run the /etc/m_cli/m_cli.sh script which in turn would spawn /bin/clitest. This turned out to be the program that handled all functionality for the debug shell. Clitest is a 32-bit ARM binary and, as such, was easy enough to load into a program like Ghidra to pull it apart and get a better idea of how the original source code looked. From there, I was able to quickly find each of the commands that were listed within the help message of the clitest binary, but what I also found was that these were not the only commands you were allowed to run. Specifically, there was one AT command that immediately caught my attention: AT+MBASHCMD.
Decompilation of function cmd_bash_cmd() showing undocumented command AT+MBASHCMD.
Sure enough, after logging into the telnet port and typing “at+mbashcmd=?” I was shown the help for an undocumented secret command that let me run commands in the bash shell…well, two commands – ps and cat – but still, that was progress! Not satisfied with just those two commands, I went back to dig deeper into the implementation of the AT+MBASHCMD command. Essentially, it works like this: “param_3” is the input we provide to the AT+MBASHCMD command which gets stored in “pcVar4”. If pcVar4 is “?”, it outputs the help and quits. If pcVar4 is not “?”, it compares pcVar4 to the strings “ps” and “cat” using a call to strncasecmp(). If the return value of that comparison is not equal to 0, it outputs “+MBASHCMD: Unsupported bash command” and exits. If it is equal to 0 though, it stores pcVar4 in the “acStack_a128” stack variable and appends the string “ > /var/run/cmd_output.txt” to it. It then passes the whole string to a call to system() which runs it.
Decompiled code showing the calls strncasecmp() and system()
That all seems relatively fine so far. Strncasecmp() takes one string and compares each byte to another string for up to a given length of bytes, which in this case is “sVar2”. The problem, however, is that sVar2 comes from the length of the stored command strings “ps” and “cat” without considering the length of the user supplied input. This means that as long as our input starts with the right string, the rest can be whatever we want. In other words, if we want to run the “ls” command or the “id”, we can just type “at+mbashcmd=ps;ls” or “at+mbashcmd=ps;id”, and it will pass the check and run the supplied commands without issue, as long as the command string parses properly in bash. And since this service is running as the root user, whatever commands we inject will be run as the root user as well, giving us complete control over the system at the OS level.
Mitigating Factors
The most important thing to point out in all of this is that the AT+MBASHCMD command was coincidentally removed completely in a firmware update released in May of this year. As long as you are running at least v1.2.0-r1132 you should be safe from this attack. The second thing that should be pointed out is that this attack requires a valid user login. And while there is a factory default password shipped with the product for the admin user, the password must be changed as part of the configuration process, so it is extremely unlikely that you would find the default login in use in the wild.
And as far as crafting a fully-fledged exploit was concerned, there were a couple of hurdles that needed to be overcome. First, any spaces in the command string would be parsed into separate arguments by clitest, causing the AT+MBASHCMD command to fail with an “Invalid parameters” message. To get around this, I used the special shell variable $IFS which is an “internal field separator” that basically tells bash to treat things as separate without having to use a space character. So instead of typing “ls /etc” for example, you would type “ls$IFS/etc” which is interpreted the same way by the shell but no longer needs spaces. The second hurdle was the fact that there is an output redirection appended to the end of the user input before it is sent to the system() call which could potentially mess with the input/output of whatever attacker code we wanted to run. This was easy to get around however by simply tacking on another semi-colon and command at the very end of my input. That way, whatever command at the very end, such as “;ls” for example, would be what got redirected to the cmd_output.txt file. In the end, exploitation ended up looking like this:
The major lesson learned here is the importance of always treating user input as potentially malicious and never sending unverified input to calls such as system() or popen(). Microhard did attempt to limit what was being sent to system(), but they failed to fully understand the function calls they were using for that limitation, and thus their attempts still fell short. It is essential to always use secure development practices and to have your code bases audited for exactly these kinds of simple oversights. And honestly, just avoid calls to known problematic functions altogether.