* 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",
"base",
"base64",
"bash",
"busybox",
"cat",
"chmod",

View File

@ -17,7 +17,6 @@ from twisted.python import failure, log
from cowrie.core import utils
from cowrie.shell.command import HoneyPotCommand
from cowrie.shell.honeypot import HoneyPotShell
from typing import TYPE_CHECKING
if TYPE_CHECKING:
@ -206,19 +205,6 @@ commands["/usr/bin/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):
def call(self) -> None:
self.protocol.terminal.reset()
@ -1003,42 +989,6 @@ commands["/usr/bin/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):
HELP = (
"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):
def call(self) -> None:
# This only show environ vars, not the shell vars. Need just to mimic real systems
for i in list(self.protocol.environ.keys()):
self.write(f"{i}={self.protocol.environ[i]}\n")
for i in list(self.environ.keys()):
self.write(f"{i}={self.environ[i]}\n")
commands["/usr/bin/env"] = Command_env

View File

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

View File

@ -33,6 +33,7 @@ class LoggingServerProtocol(insults.ServerProtocol):
self.type: str
self.ttylogFile: str
self.ttylogSize: int = 0
self.bytesSent: int = 0
self.bytesReceived: int = 0
self.redirFiles: set[list[str]] = set()
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")
def write(self, data: bytes) -> None:
self.bytesSent += len(data)
if self.ttylogEnabled and self.ttylogOpen:
ttylog.ttylog_write(
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
)
# prevent crash if something like this was passed:
# echo cmd ; exit; \n\n
if self.terminalProtocol:
insults.ServerProtocol.dataReceived(self, data)
insults.ServerProtocol.dataReceived(self, data)
def eofReceived(self) -> None:
"""
@ -225,12 +224,12 @@ class LoggingServerProtocol(insults.ServerProtocol):
log.msg(
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,
size=self.ttylogSize,
shasum=shasum,
duplicate=duplicate,
duration=time.time() - self.startTime,
duration=f"{time.time() - self.startTime:.1f}",
)
insults.ServerProtocol.connectionLost(self, reason)

View File

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

View File

@ -32,6 +32,8 @@ class HoneyPotShell:
self.environ["COLUMNS"] = str(protocol.user.windowSize[1])
self.environ["LINES"] = str(protocol.user.windowSize[0])
self.lexer: shlex.shlex | None = None
# this is the first prompt after starting
self.showPrompt()
def lineReceived(self, line: str) -> None:
@ -109,8 +111,10 @@ class HoneyPotShell:
return
if self.cmdpending:
# if we have a complete command, go and run it
self.runCommand()
else:
# if there's no command, display a prompt again
self.showPrompt()
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)
else:
log.msg(f"discarding input {string}")
stat = failure.Failure(error.ProcessDone(status=""))
self.terminal.transport.processEnded(stat)
def call_command(self, pp, cmd, *args):
self.pp = pp

View File

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

View File

@ -69,10 +69,10 @@ class CowrieSSHChannel(channel.SSHChannel):
def closed(self) -> None:
log.msg(
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,
size=self.bytesReceived + self.bytesWritten,
duration=time.time() - self.startTime,
duration=f"{time.time() - self.startTime:.1f}",
)
ttylog.ttylog_close(self.ttylogFile, time.time())
channel.SSHChannel.closed(self)

View File

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