eval '<$(;)' causes Segmentation Fault

2024-08-25 Thread youheng.lue
From: Youheng Lü

To: bug-bash@gnu.org

Subject: eval '<$(;)' causes Segmentation Fault

 

Configuration Information [Automatically generated, do not change]:

Machine: x86_64

OS: linux-gnu

Compiler: gcc

Compilation CFLAGS: -g -O2 -flto=auto -ffat-lto-objects -flto=auto 
-ffat-lto-objects -fstack-protector-strong -Wformat -Werror=format-security 
-Wall

uname output: Linux sw-c-098 6.8.0-40-generic #40~22.04.3-Ubuntu SMP 
PREEMPT_DYNAMIC Tue Jul 30 17:30:19 UTC 2 x86_64 x86_64 x86_64 GNU/Linux

Machine Type: x86_64-pc-linux-gnu

 

Bash Version: 5.1

Patch Level: 16

Release Status: release

 

Description:

Certain strings when given to `eval` cause a Segmentation Fault in bash.

 

Repeat-By:

1. Create a script, i.e. `poc.sh` with the problematic string

2. Execute `bash poc.sh`

 

Example:

$ cat poc.sh

eval '<$(;)'

 

$ bash poc.sh

poc.sh: command substitution: line 2: syntax error near unexpected 
token `;'

poc.sh: command substitution: line 2: `;)'

poc.sh: line 1: 42674 Segmentation fault  (core dumped)

 

Related Issues:

All the following scripts can create a Segmentation Fault

eval '<$[;]'

eval '<$(;)'

eval '<${;}'

eval '<$[|]'

eval '<$(|)'

eval '<${|}'

 

GDB:

The Segmentation Fault appears in `parse_and_execute`.

At some point $rax gets corrupted and the program tries

to read from $rax=0x, which is an invalid address

 

$ gdb -nx bash

Reading symbols from bash...

(No debugging symbols found in bash)

(gdb) set follow-fork-mode child

(gdb) run poc.sh

Starting program: /usr/bin/bash poc.sh

[Thread debugging using libthread_db enabled]

Using host libthread_db library 
"/lib/x86_64-linux-gnu/libthread_db.so.1".

[Attaching after Thread 0x77f6f740 (LWP 46590) fork to child 
process 46593]

[New inferior 2 (process 46593)]

[Detaching after fork from parent process 46590]

[Inferior 1 (process 46590) detached]

[Thread debugging using libthread_db enabled]

Using host libthread_db library 
"/lib/x86_64-linux-gnu/libthread_db.so.1".

poc.sh: command substitution: line 3: syntax error near unexpected 
token `;'

poc.sh: command substitution: line 3: `;)'

 

Thread 2.1 "bash" received signal SIGSEGV, Segmentation fault.

[Switching to Thread 0x77f6f740 (LWP 46593)]

0x55601f27 in parse_and_execute ()

(gdb) x/i $rip

=> 0x55601f27 :  cmpb   $0x0,(%rax)

(gdb) x/gx $rax

0x: Cannot access memory at address 0x

(gdb) bt

#0  0x55601f27 in parse_and_execute ()

#1  0x556037a1 in evalstring ()

#2  0x555a798c in ?? ()

#3  0x555a06b4 in ?? ()

#4  0x555a1b5d in execute_command_internal ()

#5  0x555a41b8 in execute_command ()

#6  0x555953cb in reader_loop ()

#7  0x55586c46 in main ()



RE: eval '<$(;)' causes Segmentation Fault

2024-08-26 Thread youheng.lue
>> These still cause the current devel branch to segfault, but (at least 
>> for me, on macOS) only when invoked via argument, as OP directed.  For 
>> example, reading the scripts via stdin avoids the segfault.
Tested on commit cf694865de527e597de5a906643a74037341a431 
I reproduced within a Docker container based on the official Bash development 
image (bash:devel), using Alpine Linux 3.20 with GCC as the compiler. Detailed 
Dockerfile is at the end of the mail.

Configuration Information [Automatically generated, do not change]:
Machine: x86_64
OS: linux-musl
Compiler: gcc
Compilation CFLAGS: -g -O2
uname output: Linux 839141c7e5ba 6.8.0-40-generic #40~22.04.3-Ubuntu SMP 
PREEMPT_DYNAMIC Tue Jul 30 17:30:19 UTC 2 x86_64 Linux
Machine Type: x86_64-pc-linux-musl

Bash Version: 5.3
Patch Level: 0
Release Status: alpha

POC:
$ cat poc.sh
eval '<${;}'

> The specific case is an empty command containing only a redirection that 
> results in an expansion error read from a script or string.
I can confirm that the error is triggerted in the "execute_null_command" 
function and later containing a redirection.
Specifically the variable `INPUT_STREAM bashinput.location` is both a char 
pointer and an int.


At first it is used as a char pointer in the function "parse_and_execute"

BEFORE 
```
gdb> p bash_input.location.string
$3 = 0x7fb3dc0db3b0 "<${;}"
```
However at shell.c:1758 in the fuction unset_bash_input it gets overwritten to 
a fd:
```
bash_input.location.buffered_fd = -1;
```
which overwrites the last 4 bytes of the pointer with 0x
Backtrace for unset_bash_input:
► 0   0x5c55279555d3 unset_bash_input+35
   1   0x5c5527986ed8 make_child+952
   2   0x5c552796bb73 execute_null_command+387
   3   0x5c55279703fa execute_command_internal+9786
   4   0x5c55279703fa execute_command_internal+9786
   5   0x5c55279ce969 parse_and_execute+1513


AFTER
```
gdb> p bash_input.location.string
$14 = 0x7fb3 
```
This leads to an OOB read, which leads to a segfault at evalstring.c:359
```c
while (*(bash_input.location.string) || parser_expanding_alias ())
```


Security:
   This might pose a security risk, since an attacker might leverage this to 
break out of a restricted shell.
   1. It might be possible for them to get lucky with ASLR and get a useful OOB 
read that doesn't crash.
   2. An attacker controls the size of `bash_input.location.string` which is 
allocated and therefore have some control where the OOB Read happens

Proposed fix:
I suggest modifying INPUT_STREAM from a union to a struct to prevent 
the type confusion between pointer and int that leads to the OOB read. 
This is a theoretical fix and I have not tested it yet, so further 
validation is required to confirm its effectiveness.


### Dockerfile ##
FROM alpine:3.20

# https://git.savannah.gnu.org/cgit/bash.git/log/?h=devel
ENV _BASH_COMMIT cf694865de527e597de5a906643a74037341a431
# optimize asynchronous function invocations; fix for running return from trap 
while sourcing a file; restore completion function if read -e is interrupted
ENV _BASH_VERSION devel-20240815
# prefixed with "_" since "$BASH..." have meaning in Bash parlance

RUN set -eux; \
\
apk add --no-cache --virtual .build-deps \
bison \
coreutils \
dpkg-dev dpkg \
gcc \
libc-dev \
make \
ncurses-dev \
patch \
tar \
; \
\
wget -O bash.tar.gz 
"https://git.savannah.gnu.org/cgit/bash.git/snapshot/bash-$_BASH_COMMIT.tar.gz";;
 \
\
mkdir -p /usr/src/bash; \
tar \
--extract \
--file=bash.tar.gz \
--strip-components=1 \
--directory=/usr/src/bash \
; \
rm bash.tar.gz; \
\
if [ -d bash-patches ]; then \
apk add --no-cache --virtual .patch-deps patch; \
for p in bash-patches/*; do \
patch \
--directory=/usr/src/bash \
--input="$(readlink -f "$p")" \
--strip=0 \
; \
rm "$p"; \
done; \
rmdir bash-patches; \
apk del --no-network .patch-deps; \
fi; \
\
# https://lists.gnu.org/archive/html/bug-bash/2023-05/msg00011.html
{ echo '#include '; echo; cat /usr/src/bash/lib/sh/strscpy.c; 
} > /usr/src/bash/lib/sh/strscpy.c.new; \
mv /usr/src/bash/lib/sh/strscpy.c.new /usr/src/bash/lib/sh/strscpy.c; \
\
cd /usr/src/bash; \
gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \
./configure \
--build="$gnuArch" \
--enable-readline \
--with-curses \
# musl does not implement brk/sbrk (they simply return -ENOMEM)
#