Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

[BUG] Two vulnerabilities in zsh



Another win for AFL!

There are two vulnerabilities in zsh <5.8. I'll request a couple CVE IDs if that's okay.

I contacted Peter <p.w.stephenson@xxxxxxxxxxxx> about these a few days ago. Neither have common, practical attack scenarios ("oh no! someone with a shell could pop a shell!"), so I'm disclosing them here.

I did a little analysis on each vulnerability. The more severe one appears to be some form of memory corruption, but it's decently complex, and I couldn't find the root cause. I think it'll take someone more experienced than me to find it. But, I was able to track down the null dereference bug. :)

Frankly, I'm not a C developer, but I think I at least know how to fix the null dereference vuln, so I added a section with a patch there.

Thanks for the awesome shell!

Sincerely,
Aaron Esau

https://aaronesau.com/


------ #1 :: null dereference in check_colon_subscript in subst.c ------


A denial of service vulnerability exists in zsh <5.8 due to a null dereference in check_colon_subscript in subst.c.

## Reproduction Steps

I was able to reproduce this bug on every zsh version I tested. I am running zsh 5.8 (x86_64-pc-linux-gnu) on Arch Linux.

1. Start zsh and execute the following (including quotes):

  "${: :${{{\"{{i use arch btw}}"

2. Observe that the command causes a segmentation fault. The stack trace is:

  $rip 0x5555555eb22e => movzx eax, BYTE PTR [rax]
  [#1] 0x5555555ed61e => prefork()
  [#2] 0x555555583815 => mov rsi, r12
  [#3] 0x555555588e01 => mov rbx, QWORD PTR [rbx]
  [#4] 0x55555558c32f => pop rcx
  [#5] 0x55555558c6d1 => mov eax, DWORD PTR [rip+0x9e8c1] # [rip+0x9e8c1] => 0x55555562af98
  [#6] 0x55555558e051 => execlist()
  [#7] 0x55555558e564 => execode()
  [#8] 0x5555555a43d4 => loop()
  [#9] 0x5555555a7d36 => zsh_main()

## Analysis

The following code in check_colon_subscript in subst.c checks if the value at a pointer obtained from a call to parse_subscript is NULL:

  *endp = parse_subscript(str, 0, ':');
  if (!*endp) {
      /* No trailing colon? */
      *endp = parse_subscript(str, 0, '\0');
      if (!*endp)
          return NULL;
  }

However, the pointer itself can be NULL. In parse_subscript in lex.c, if err != 0, the returning variable, s, is set to NULL:

  err = dquote_parse(endchar, sub);
  ...
  if (err) {
      ...
      s = NULL;
  } else {
      s += toklen;
  }
  ..
  return s;

The function dquote_parse in lex.c returns NULL on many error-related conditions.

## Patch

diff --git Src/subst.c Src/subst.c
index 90b5fc121..ac12c6d0e 100644
--- Src/subst.c
+++ Src/subst.c
@@ -1571,10 +1571,10 @@ check_colon_subscript(char *str, char **endp)
     }

     *endp = parse_subscript(str, 0, ':');
-    if (!*endp) {
+    if (endp && !*endp) {
 	/* No trailing colon? */
 	*endp = parse_subscript(str, 0, '\0');
-	if (!*endp)
+	if (endp && !*endp)
 	   return NULL;
     }
     sav = **endp;


------ #2 :: memory corruption in ?? ------


A memory corruption vulnerability exists in zsh <5.8 which may enable arbitrary code execution or memory disclosure via unspecified methods.

## Reproduction Steps

I am running zsh 5.8 (x86_64-pc-linux-gnu) on Arch Linux, kernel version 5.6.11-arch1-1.

1. Execute the following PoC command:

  echo $'******** **********************$\\\n(>$' | zsh

2. Observe that the command causes a segmentation fault. The stack trace is:

  $rip 0x5555555eb22e => movzx eax, BYTE PTR [rax]
  [#1] 0x5555555ed61e => prefork()
  [#2] 0x555555583815 => mov rsi, r12
  [#3] 0x555555588e01 => mov rbx, QWORD PTR [rbx]
  [#4] 0x55555558c32f => pop rcx
  [#5] 0x55555558c6d1 => mov eax, DWORD PTR [rip+0x9e8c1] # [rip+0x9e8c1] => 0x55555562af98
  [#6] 0x55555558e051 => execlist()
  [#7] 0x55555558e564 => execode()
  [#8] 0x5555555a43d4 => loop()
  [#9] 0x5555555a7d36 => zsh_main()

## Analysis

The segmentation fault is caused by a mov instruction which dereferences a $rax, a pointer to invalid memory:

  * 0x555555588bac: setne  bl
  * 0x555555588baf: jmp    0x555555588bd9
  * 0x555555588bb1: nop    DWORD PTR [rax+0x0]
  =>0x555555588bb8: mov    rdi, QWORD PTR [rax+0x10]
  * 0x555555588bbc: addr32 call 0x5555555f27f0 <has_token>
  * 0x555555588bc2: test   eax, eax
  * 0x555555588bc4: je     0x555555589bc0
  * 0x555555588bca: mov    rsi, QWORD PTR [rbp+0x0]
  * 0x555555588bce: xor    edx, edx

At the segfault, the \$r[a-z]{2} registers are:

  $rax : 0x00007fff007ffff7
  $rbx : 0x00007ffff7fbe601 => 0xa8772e2a3a000000
  $rcx : 0x00007ffff7fbe608 => 0x00007ffff7fbe5a8 => 0x0000000000000000
  $rdx : 0x00007ffff7fbe5a8 => 0x0000000000000000
  $rsp : 0x00007fffffffbef0 => 0x0000000100000099
  $rbp : 0x00007ffff7fbe5f0 => 0x00007fff007ffff7
  $rsi : 0x00007ffff7fbe578 => 0x0000000000000000
  $rdi : 0x00007ffff7fbe5f0 => 0x00007fff007ffff7
  $rip : 0x0000555555588bb8 => mov rdi, QWORD PTR [rax+0x10]

So, again, the register $rax points to invalid memory. I stepped backward and found that the value in the register comes from the stack:

  =>0x555555588bef: mov    rax, QWORD PTR [rbp+0x0]
  * 0x555555588bf3: test   rax, rax
  * 0x555555588bf6: jne    0x555555588bb8

I set a breakpoint at 0x555555588bef, then set watchpoints on both $rbp and—as least significant 4 bytes of the address appear to be overwritten—the address $rbp-0x4.

It was really hard to trace back and find what overwrites that memory. It appears as though the lower bytes of the address are overwritten before the instruction that first triggered the second watchpoint in __memmove_avx_unaligned_erms:

  =>0x7ffff7d4d26f: mov    QWORD PTR [rdi+rdx*1-0x8], rcx

However, neither execlist in exec.c nor any of the functions it calls appear to execute glibc's memmove function. Perhaps another glibc function internally calls __memmove_avx_unaligned_erms? To be continued.



Messages sorted by: Reverse Date, Date, Thread, Author