* multi level shell!
* standardize duration logging
This commit is contained in:
Michel Oosterhof
2024-11-26 20:37:11 +08:00
committed by GitHub
parent f9333c9c7d
commit 6718b28d94
12 changed files with 110 additions and 76 deletions

View File

@ -9,6 +9,7 @@ __all__ = [
"awk", "awk",
"base", "base",
"base64", "base64",
"bash",
"busybox", "busybox",
"cat", "cat",
"chmod", "chmod",

View File

@ -17,7 +17,6 @@ from twisted.python import failure, log
from cowrie.core import utils from cowrie.core import utils
from cowrie.shell.command import HoneyPotCommand from cowrie.shell.command import HoneyPotCommand
from cowrie.shell.honeypot import HoneyPotShell
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
@ -206,19 +205,6 @@ commands["/usr/bin/printf"] = Command_printf
commands["printf"] = Command_printf commands["printf"] = Command_printf
class Command_exit(HoneyPotCommand):
def call(self) -> None:
stat = failure.Failure(error.ProcessDone(status=""))
self.protocol.terminal.transport.processEnded(stat)
def exit(self) -> None:
pass
commands["exit"] = Command_exit
commands["logout"] = Command_exit
class Command_clear(HoneyPotCommand): class Command_clear(HoneyPotCommand):
def call(self) -> None: def call(self) -> None:
self.protocol.terminal.reset() self.protocol.terminal.reset()
@ -1003,42 +989,6 @@ commands["/usr/bin/yes"] = Command_yes
commands["yes"] = Command_yes commands["yes"] = Command_yes
class Command_sh(HoneyPotCommand):
def call(self) -> None:
if self.args and self.args[0].strip() == "-c":
line = " ".join(self.args[1:])
# it might be sh -c 'echo "sometext"', so don't use line.strip('\'\"')
if (line[0] == "'" and line[-1] == "'") or (
line[0] == '"' and line[-1] == '"'
):
line = line[1:-1]
self.execute_commands(line)
elif self.input_data:
self.execute_commands(self.input_data.decode("utf8"))
# TODO: handle spawning multiple shells, support other sh flags
def execute_commands(self, cmds: str) -> None:
# self.input_data holds commands passed via PIPE
# create new HoneyPotShell for our a new 'sh' shell
self.protocol.cmdstack.append(HoneyPotShell(self.protocol, interactive=False))
# call lineReceived method that indicates that we have some commands to parse
self.protocol.cmdstack[-1].lineReceived(cmds)
# remove the shell
self.protocol.cmdstack.pop()
commands["/bin/bash"] = Command_sh
commands["bash"] = Command_sh
commands["/bin/sh"] = Command_sh
commands["sh"] = Command_sh
class Command_php(HoneyPotCommand): class Command_php(HoneyPotCommand):
HELP = ( HELP = (
"Usage: php [options] [-f] <file> [--] [args...]\n" "Usage: php [options] [-f] <file> [--] [args...]\n"

View File

@ -0,0 +1,83 @@
# Copyright (c) 2009 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information
# coding=utf-8
from __future__ import annotations
from twisted.internet import error
from twisted.python import failure
from cowrie.shell.command import HoneyPotCommand
from cowrie.shell.honeypot import HoneyPotShell
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable
commands: dict[str, Callable] = {}
class Command_sh(HoneyPotCommand):
def start(self) -> None:
if self.args and self.args[0].strip() == "-c":
line = " ".join(self.args[1:])
# it might be sh -c 'echo "sometext"', so don't use line.strip('\'\"')
if (line[0] == "'" and line[-1] == "'") or (
line[0] == '"' and line[-1] == '"'
):
line = line[1:-1]
self.execute_commands(line)
self.exit()
elif self.input_data:
self.execute_commands(self.input_data.decode("utf8"))
self.exit()
else:
self.interactive_shell()
# TODO: handle spawning multiple shells, support other sh flags
def execute_commands(self, cmds: str) -> None:
# self.input_data holds commands passed via PIPE
# create new HoneyPotShell for our a new 'sh' shell
self.protocol.cmdstack.append(HoneyPotShell(self.protocol, interactive=False))
# call lineReceived method that indicates that we have some commands to parse
self.protocol.cmdstack[-1].lineReceived(cmds)
# remove the shell
self.protocol.cmdstack.pop()
def interactive_shell(self) -> None:
shell = HoneyPotShell(self.protocol, interactive=True)
parentshell = self.protocol.cmdstack[-2]
# TODO: copy more variables, but only exported variables
try:
shell.environ["SHLVL"] = str(int(parentshell.environ["SHLVL"]) + 1)
except KeyError:
shell.environ["SHLVL"] = "1"
self.protocol.cmdstack.append(shell)
self.protocol.cmdstack.remove(self)
commands["/bin/bash"] = Command_sh
commands["bash"] = Command_sh
commands["/bin/sh"] = Command_sh
commands["sh"] = Command_sh
class Command_exit(HoneyPotCommand):
def call(self) -> None:
# this removes the second last command, which is the shell
self.protocol.cmdstack.pop(-2)
if len(self.protocol.cmdstack) < 2:
stat = failure.Failure(error.ProcessDone(status=""))
self.protocol.terminal.transport.processEnded(stat)
commands["exit"] = Command_exit
commands["logout"] = Command_exit

View File

@ -28,8 +28,8 @@ For complete documentation, run: info coreutils 'env invocation'
class Command_env(HoneyPotCommand): class Command_env(HoneyPotCommand):
def call(self) -> None: def call(self) -> None:
# This only show environ vars, not the shell vars. Need just to mimic real systems # This only show environ vars, not the shell vars. Need just to mimic real systems
for i in list(self.protocol.environ.keys()): for i in list(self.environ.keys()):
self.write(f"{i}={self.protocol.environ[i]}\n") self.write(f"{i}={self.environ[i]}\n")
commands["/usr/bin/env"] = Command_env commands["/usr/bin/env"] = Command_env

View File

@ -56,7 +56,7 @@ class Command_gcc(HoneyPotCommand):
scheduled: Deferred scheduled: Deferred
def start(self) -> None: def call(self) -> None:
""" """
Parse as much as possible from a GCC syntax and generate the output Parse as much as possible from a GCC syntax and generate the output
that is requested. The file that is generated can be read (and will) that is requested. The file that is generated can be read (and will)
@ -151,17 +151,16 @@ class Command_gcc(HoneyPotCommand):
def no_files(self) -> None: def no_files(self) -> None:
""" """
Notify user there are no input files, and exit Notify user there are no input files
""" """
self.write( self.write(
"""gcc: fatal error: no input files """gcc: fatal error: no input files
compilation terminated.\n""" compilation terminated.\n"""
) )
self.exit()
def version(self, short: bool) -> None: def version(self, short: bool) -> None:
""" """
Print long or short version, and exit Print long or short version
""" """
# Generate version number # Generate version number
@ -184,7 +183,6 @@ gcc version {version} (Debian {version}-5)"""
# Write # Write
self.write(f"{data}\n") self.write(f"{data}\n")
self.exit()
def generate_file(self, outfile: str) -> None: def generate_file(self, outfile: str) -> None:
data = b"" data = b""
@ -228,15 +226,11 @@ gcc version {version} (Debian {version}-5)"""
# Trick the 'new compiled file' as an segfault # Trick the 'new compiled file' as an segfault
self.protocol.commands[outfile] = segfault_command self.protocol.commands[outfile] = segfault_command
# Done
self.exit()
def arg_missing(self, arg: str) -> None: def arg_missing(self, arg: str) -> None:
""" """
Print missing argument message, and exit Print missing argument message, and exit
""" """
self.write(f"{Command_gcc.APP_NAME}: argument to '{arg}' is missing\n") self.write(f"{Command_gcc.APP_NAME}: argument to '{arg}' is missing\n")
self.exit()
def help(self) -> None: def help(self) -> None:
""" """
@ -306,7 +300,6 @@ For bug reporting instructions, please see:
<file:///usr/share/doc/gcc-4.7/README.Bugs>. <file:///usr/share/doc/gcc-4.7/README.Bugs>.
""" """
) )
self.exit()
commands["/usr/bin/gcc"] = Command_gcc commands["/usr/bin/gcc"] = Command_gcc

View File

@ -33,6 +33,7 @@ class LoggingServerProtocol(insults.ServerProtocol):
self.type: str self.type: str
self.ttylogFile: str self.ttylogFile: str
self.ttylogSize: int = 0 self.ttylogSize: int = 0
self.bytesSent: int = 0
self.bytesReceived: int = 0 self.bytesReceived: int = 0
self.redirFiles: set[list[str]] = set() self.redirFiles: set[list[str]] = set()
self.redirlogOpen: bool = False # it will be set at core/protocol.py self.redirlogOpen: bool = False # it will be set at core/protocol.py
@ -95,6 +96,7 @@ class LoggingServerProtocol(insults.ServerProtocol):
self.terminalProtocol.execcmd.encode("utf8") self.terminalProtocol.execcmd.encode("utf8")
def write(self, data: bytes) -> None: def write(self, data: bytes) -> None:
self.bytesSent += len(data)
if self.ttylogEnabled and self.ttylogOpen: if self.ttylogEnabled and self.ttylogOpen:
ttylog.ttylog_write( ttylog.ttylog_write(
self.ttylogFile, len(data), ttylog.TYPE_OUTPUT, time.time(), data self.ttylogFile, len(data), ttylog.TYPE_OUTPUT, time.time(), data
@ -121,10 +123,7 @@ class LoggingServerProtocol(insults.ServerProtocol):
self.ttylogFile, len(data), ttylog.TYPE_INPUT, time.time(), data self.ttylogFile, len(data), ttylog.TYPE_INPUT, time.time(), data
) )
# prevent crash if something like this was passed: insults.ServerProtocol.dataReceived(self, data)
# echo cmd ; exit; \n\n
if self.terminalProtocol:
insults.ServerProtocol.dataReceived(self, data)
def eofReceived(self) -> None: def eofReceived(self) -> None:
""" """
@ -225,12 +224,12 @@ class LoggingServerProtocol(insults.ServerProtocol):
log.msg( log.msg(
eventid="cowrie.log.closed", eventid="cowrie.log.closed",
format="Closing TTY Log: %(ttylog)s after %(duration)d seconds", format="Closing TTY Log: %(ttylog)s after %(duration)s seconds",
ttylog=shasumfile, ttylog=shasumfile,
size=self.ttylogSize, size=self.ttylogSize,
shasum=shasum, shasum=shasum,
duplicate=duplicate, duplicate=duplicate,
duration=time.time() - self.startTime, duration=f"{time.time() - self.startTime:.1f}",
) )
insults.ServerProtocol.connectionLost(self, reason) insults.ServerProtocol.connectionLost(self, reason)

View File

@ -34,7 +34,7 @@ class HoneyPotCommand:
def __init__(self, protocol, *args): def __init__(self, protocol, *args):
self.protocol = protocol self.protocol = protocol
self.args = list(args) self.args = list(args)
self.environ = self.protocol.cmdstack[0].environ self.environ = self.protocol.cmdstack[-1].environ
self.fs = self.protocol.fs self.fs = self.protocol.fs
self.data: bytes = b"" # output data self.data: bytes = b"" # output data
self.input_data: None | (bytes) = ( self.input_data: None | (bytes) = (
@ -174,7 +174,8 @@ class HoneyPotCommand:
self.protocol.terminal.redirFiles.add((self.safeoutfile, "")) self.protocol.terminal.redirFiles.add((self.safeoutfile, ""))
if len(self.protocol.cmdstack): if len(self.protocol.cmdstack):
self.protocol.cmdstack.pop() self.protocol.cmdstack.remove(self)
if len(self.protocol.cmdstack): if len(self.protocol.cmdstack):
self.protocol.cmdstack[-1].resume() self.protocol.cmdstack[-1].resume()
else: else:

View File

@ -32,6 +32,8 @@ class HoneyPotShell:
self.environ["COLUMNS"] = str(protocol.user.windowSize[1]) self.environ["COLUMNS"] = str(protocol.user.windowSize[1])
self.environ["LINES"] = str(protocol.user.windowSize[0]) self.environ["LINES"] = str(protocol.user.windowSize[0])
self.lexer: shlex.shlex | None = None self.lexer: shlex.shlex | None = None
# this is the first prompt after starting
self.showPrompt() self.showPrompt()
def lineReceived(self, line: str) -> None: def lineReceived(self, line: str) -> None:
@ -109,8 +111,10 @@ class HoneyPotShell:
return return
if self.cmdpending: if self.cmdpending:
# if we have a complete command, go and run it
self.runCommand() self.runCommand()
else: else:
# if there's no command, display a prompt again
self.showPrompt() self.showPrompt()
def do_command_substitution(self, start_tok: str) -> str: def do_command_substitution(self, start_tok: str) -> str:

View File

@ -190,6 +190,8 @@ class HoneyPotBaseProtocol(insults.TerminalProtocol, TimeoutMixin):
self.cmdstack[-1].lineReceived(string) self.cmdstack[-1].lineReceived(string)
else: else:
log.msg(f"discarding input {string}") log.msg(f"discarding input {string}")
stat = failure.Failure(error.ProcessDone(status=""))
self.terminal.transport.processEnded(stat)
def call_command(self, pp, cmd, *args): def call_command(self, pp, cmd, *args):
self.pp = pp self.pp = pp

View File

@ -30,12 +30,13 @@ class SSHSessionForCowrieUser:
self.gid = avatar.gid self.gid = avatar.gid
self.username = avatar.username self.username = avatar.username
self.environ = { self.environ = {
"HOME": self.avatar.home,
"LOGNAME": self.username, "LOGNAME": self.username,
"SHELL": "/bin/bash", "SHELL": "/bin/bash",
"USER": self.username, "SHLVL": "1",
"HOME": self.avatar.home,
"TMOUT": "1800", "TMOUT": "1800",
"UID": str(self.uid), "UID": str(self.uid),
"USER": self.username,
} }
if self.uid == 0: if self.uid == 0:
self.environ["PATH"] = ( self.environ["PATH"] = (

View File

@ -69,10 +69,10 @@ class CowrieSSHChannel(channel.SSHChannel):
def closed(self) -> None: def closed(self) -> None:
log.msg( log.msg(
eventid="cowrie.log.closed", eventid="cowrie.log.closed",
format="Closing TTY Log: %(ttylog)s after %(duration)f seconds", format="Closing TTY Log: %(ttylog)s after %(duration)s seconds",
ttylog=self.ttylogFile, ttylog=self.ttylogFile,
size=self.bytesReceived + self.bytesWritten, size=self.bytesReceived + self.bytesWritten,
duration=time.time() - self.startTime, duration=f"{time.time() - self.startTime:.1f}",
) )
ttylog.ttylog_close(self.ttylogFile, time.time()) ttylog.ttylog_close(self.ttylogFile, time.time())
channel.SSHChannel.closed(self) channel.SSHChannel.closed(self)

View File

@ -246,10 +246,10 @@ class HoneyPotSSHTransport(transport.SSHServerTransport, TimeoutMixin):
transport.SSHServerTransport.connectionLost(self, reason) transport.SSHServerTransport.connectionLost(self, reason)
self.transport.connectionLost(reason) self.transport.connectionLost(reason)
self.transport = None self.transport = None
duration = time.time() - self.startTime duration = f"{time.time() - self.startTime:.1f}"
log.msg( log.msg(
eventid="cowrie.session.closed", eventid="cowrie.session.closed",
format="Connection lost after %(duration)d seconds", format="Connection lost after %(duration)s seconds",
duration=duration, duration=duration,
) )