Pennyw0rth / NetExec

The Network Execution Tool
https://netexec.wiki/
BSD 2-Clause "Simplified" License
2.83k stars 304 forks source link

SSH module DB exceptions #395

Open dazzgt opened 1 month ago

dazzgt commented 1 month ago

Describe the bug There is a few exception when i run ssh. Both are connected to db.

To Reproduce Command: netexec ssh 172.x.x.x/24 -u realuser -p realpass Don't think that this is important but there is quite a few machines with this creds in subnet. Also winrm module gave similar errors but where fixed after i ran pipx upgrade-all so i believe that there were fixes for similar issues in previous two weeks or so. Unfortunatelly i didn't look into error much so it possible that it was some kind of other error.

Resulted in: 1) less commmon index error:

          ERROR    Exception while calling proto_flow() on target 172.x.x.x: tuple   connection.py:174
                    index out of range                                                                    
                    ╭─────────────── Traceback (most recent call last) ────────────────╮                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/nxc/connection.py:166 in __init__                      │                  
                    │                                                                  │                  
                    │   163 │   │   self.logger.info(f"Socket info: host={self.host},  │                  
                    │       kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local  │                  
                    │       ipv6={self.is_link_local_ipv6}")                           │                  
                    │   164 │   │                                                      │                  
                    │   165 │   │   try:                                               │                  
                    │ ❱ 166 │   │   │   self.proto_flow()                              │                  
                    │   167 │   │   except Exception as e:                             │                  
                    │   168 │   │   │   if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e │                  
                    │   169 │   │   │   │   self.logger.error(f"Exception while callin │                  
                    │       {target}: {e}")                                            │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/nxc/protocols/ssh.py:29 in proto_flow                  │                  
                    │                                                                  │                  
                    │    26 │   │   self.logger.debug("Kicking off proto_flow")        │                  
                    │    27 │   │   self.proto_logger()                                │                  
                    │    28 │   │   if self.create_conn_obj():                         │                  
                    │ ❱  29 │   │   │   self.enum_host_info()                          │                  
                    │    30 │   │   │   self.print_host_info()                         │                  
                    │    31 │   │   │   if self.remote_version == "Unknown SSH Version │                  
                    │    32 │   │   │   │   self.conn.close()                          │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/nxc/protocols/ssh.py:61 in enum_host_info              │                  
                    │                                                                  │                  
                    │    58 │   │   if self.conn._transport.remote_version:            │                  
                    │    59 │   │   │   self.remote_version = self.conn._transport.rem │                  
                    │    60 │   │   self.logger.debug(f"Remote version: {self.remote_v │                  
                    │ ❱  61 │   │   self.db.add_host(self.host, self.port, self.remote │                  
                    │    62 │                                                          │                  
                    │    63 │   def create_conn_obj(self):                             │                  
                    │    64 │   │   self.conn = paramiko.SSHClient()                   │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/nxc/protocols/ssh/database.py:142 in add_host          │                  
                    │                                                                  │                  
                    │   139 │   │   # update existing hosts data                       │                  
                    │   140 │   │   else:                                              │                  
                    │   141 │   │   │   for host_result in results:                    │                  
                    │ ❱ 142 │   │   │   │   host_data = host_result._asdict()          │                  
                    │   143 │   │   │   │   nxc_logger.debug(f"host: {host_result}")   │                  
                    │   144 │   │   │   │   nxc_logger.debug(f"host_data: {host_data}" │                  
                    │   145 │   │   │   │   # only update column if it is being passed │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/engine/row.py:280 in _asdict                │                  
                    │                                                                  │                  
                    │   277 │   │   │   :attr:`.Row._mapping`                          │                  
                    │   278 │   │                                                      │                  
                    │   279 │   │   """                                                │                  
                    │ ❱ 280 │   │   return dict(self._mapping)                         │                  
                    │   281                                                            │                  
                    │   282                                                            │                  
                    │   283 BaseRowProxy = BaseRow                                     │                  
                    │                                                                  │                  
                    │ in                                                               │                  
                    │ sqlalchemy.cyextension.resultproxy.BaseRow._get_by_key_impl_mapp │                  
                    │ ing:57                                                           │                  
                    │                                                                  │                  
                    │ in                                                               │                  
                    │ sqlalchemy.cyextension.resultproxy.BaseRow._get_by_key_impl:62   │                  
                    ╰──────────────────────────────────────────────────────────────────╯                  
                    IndexError: tuple index out of range                                        

2) And more common exception: sqlite3.InterfaceError

           ERROR    Exception while calling proto_flow() on target 172.x.x.x:         connection.py:174
                    (sqlite3.InterfaceError) bad parameter or other API misuse                            
                    [SQL: SELECT hosts.id, hosts.host, hosts.port, hosts.banner,                          
                    hosts.os                                                                              
                    FROM hosts                                                                            
                    WHERE hosts.host = ?]                                                                 
                    [parameters: ('172.x.x.x',)]                                                       
                    (Background on this error at: https://sqlalche.me/e/20/rvf5)                          
                    ╭─────────────── Traceback (most recent call last) ────────────────╮                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/engine/base.py:1967 in _exec_single_context │                  
                    │                                                                  │                  
                    │   1964 │   │   │   │   │   │   │   evt_handled = True            │                  
                    │   1965 │   │   │   │   │   │   │   break                         │                  
                    │   1966 │   │   │   │   if not evt_handled:                       │                  
                    │ ❱ 1967 │   │   │   │   │   self.dialect.do_execute(              │                  
                    │   1968 │   │   │   │   │   │   cursor, str_statement, effective_ │                  
                    │   1969 │   │   │   │   │   )                                     │                  
                    │   1970                                                           │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/engine/default.py:924 in do_execute         │                  
                    │                                                                  │                  
                    │    921 │   │   cursor.executemany(statement, parameters)         │                  
                    │    922 │                                                         │                  
                    │    923 │   def do_execute(self, cursor, statement, parameters, c │                  
                    │ ❱  924 │   │   cursor.execute(statement, parameters)             │                  
                    │    925 │                                                         │                  
                    │    926 │   def do_execute_no_params(self, cursor, statement, con │                  
                    │    927 │   │   cursor.execute(statement)                         │                  
                    ╰──────────────────────────────────────────────────────────────────╯                  
                    InterfaceError: bad parameter or other API misuse                                     

                    The above exception was the direct cause of the following exception:                  

                    ╭─────────────── Traceback (most recent call last) ────────────────╮                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/nxc/connection.py:166 in __init__                      │                  
                    │                                                                  │                  
                    │   163 │   │   self.logger.info(f"Socket info: host={self.host},  │                  
                    │       kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local  │                  
                    │       ipv6={self.is_link_local_ipv6}")                           │                  
                    │   164 │   │                                                      │                  
                    │   165 │   │   try:                                               │                  
                    │ ❱ 166 │   │   │   self.proto_flow()                              │                  
                    │   167 │   │   except Exception as e:                             │                  
                    │   168 │   │   │   if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e │                  
                    │   169 │   │   │   │   self.logger.error(f"Exception while callin │                  
                    │       {target}: {e}")                                            │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/nxc/protocols/ssh.py:29 in proto_flow                  │                  
                    │                                                                  │                  
                    │    26 │   │   self.logger.debug("Kicking off proto_flow")        │                  
                    │    27 │   │   self.proto_logger()                                │                  
                    │    28 │   │   if self.create_conn_obj():                         │                  
                    │ ❱  29 │   │   │   self.enum_host_info()                          │                  
                    │    30 │   │   │   self.print_host_info()                         │                  
                    │    31 │   │   │   if self.remote_version == "Unknown SSH Version │                  
                    │    32 │   │   │   │   self.conn.close()                          │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/nxc/protocols/ssh.py:61 in enum_host_info              │                  
                    │                                                                  │                  
                    │    58 │   │   if self.conn._transport.remote_version:            │                  
                    │    59 │   │   │   self.remote_version = self.conn._transport.rem │                  
                    │    60 │   │   self.logger.debug(f"Remote version: {self.remote_v │                  
                    │ ❱  61 │   │   self.db.add_host(self.host, self.port, self.remote │                  
                    │    62 │                                                          │                  
                    │    63 │   def create_conn_obj(self):                             │                  
                    │    64 │   │   self.conn = paramiko.SSHClient()                   │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/nxc/protocols/ssh/database.py:127 in add_host          │                  
                    │                                                                  │                  
                    │   124 │   │   updated_ids = []                                   │                  
                    │   125 │   │                                                      │                  
                    │   126 │   │   q = select(self.HostsTable).filter(self.HostsTable │                  
                    │ ❱ 127 │   │   results = self.sess.execute(q).all()               │                  
                    │   128 │   │   nxc_logger.debug(f"add_host(): Initial hosts resul │                  
                    │   129 │   │                                                      │                  
                    │   130 │   │   # create new host                                  │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/orm/session.py:2351 in execute              │                  
                    │                                                                  │                  
                    │   2348 │   │                                                     │                  
                    │   2349 │   │                                                     │                  
                    │   2350 │   │   """                                               │                  
                    │ ❱ 2351 │   │   return self._execute_internal(                    │                  
                    │   2352 │   │   │   statement,                                    │                  
                    │   2353 │   │   │   params,                                       │                  
                    │   2354 │   │   │   execution_options=execution_options,          │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/orm/session.py:2245 in _execute_internal    │                  
                    │                                                                  │                  
                    │   2242 │   │   │   │   conn,                                     │                  
                    │   2243 │   │   │   )                                             │                  
                    │   2244 │   │   else:                                             │                  
                    │ ❱ 2245 │   │   │   result = conn.execute(                        │                  
                    │   2246 │   │   │   │   statement, params or {}, execution_option │                  
                    │   2247 │   │   │   )                                             │                  
                    │   2248                                                           │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/engine/base.py:1418 in execute              │                  
                    │                                                                  │                  
                    │   1415 │   │   except AttributeError as err:                     │                  
                    │   1416 │   │   │   raise exc.ObjectNotExecutableError(statement) │                  
                    │   1417 │   │   else:                                             │                  
                    │ ❱ 1418 │   │   │   return meth(                                  │                  
                    │   1419 │   │   │   │   self,                                     │                  
                    │   1420 │   │   │   │   distilled_parameters,                     │                  
                    │   1421 │   │   │   │   execution_options or NO_OPTIONS,          │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/sql/elements.py:515 in                      │                  
                    │ _execute_on_connection                                           │                  
                    │                                                                  │                  
                    │    512 │   │   if self.supports_execution:                       │                  
                    │    513 │   │   │   if TYPE_CHECKING:                             │                  
                    │    514 │   │   │   │   assert isinstance(self, Executable)       │                  
                    │ ❱  515 │   │   │   return connection._execute_clauseelement(     │                  
                    │    516 │   │   │   │   self, distilled_params, execution_options │                  
                    │    517 │   │   │   )                                             │                  
                    │    518 │   │   else:                                             │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/engine/base.py:1640 in                      │                  
                    │ _execute_clauseelement                                           │                  
                    │                                                                  │                  
                    │   1637 │   │   │   schema_translate_map=schema_translate_map,    │                  
                    │   1638 │   │   │   linting=self.dialect.compiler_linting | compi │                  
                    │   1639 │   │   )                                                 │                  
                    │ ❱ 1640 │   │   ret = self._execute_context(                      │                  
                    │   1641 │   │   │   dialect,                                      │                  
                    │   1642 │   │   │   dialect.execution_ctx_cls._init_compiled,     │                  
                    │   1643 │   │   │   compiled_sql,                                 │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/engine/base.py:1846 in _execute_context     │                  
                    │                                                                  │                  
                    │   1843 │   │   if context.execute_style is ExecuteStyle.INSERTMA │                  
                    │   1844 │   │   │   return self._exec_insertmany_context(dialect, │                  
                    │   1845 │   │   else:                                             │                  
                    │ ❱ 1846 │   │   │   return self._exec_single_context(             │                  
                    │   1847 │   │   │   │   dialect, context, statement, parameters   │                  
                    │   1848 │   │   │   )                                             │                  
                    │   1849                                                           │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/engine/base.py:1986 in _exec_single_context │                  
                    │                                                                  │                  
                    │   1983 │   │   │   result = context._setup_result_proxy()        │                  
                    │   1984 │   │                                                     │                  
                    │   1985 │   │   except BaseException as e:                        │                  
                    │ ❱ 1986 │   │   │   self._handle_dbapi_exception(                 │                  
                    │   1987 │   │   │   │   e, str_statement, effective_parameters, c │                  
                    │   1988 │   │   │   )                                             │                  
                    │   1989                                                           │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/engine/base.py:2353 in                      │                  
                    │ _handle_dbapi_exception                                          │                  
                    │                                                                  │                  
                    │   2350 │   │   │   │   raise newraise.with_traceback(exc_info[2] │                  
                    │   2351 │   │   │   elif should_wrap:                             │                  
                    │   2352 │   │   │   │   assert sqlalchemy_exception is not None   │                  
                    │ ❱ 2353 │   │   │   │   raise sqlalchemy_exception.with_traceback │                  
                    │   2354 │   │   │   else:                                         │                  
                    │   2355 │   │   │   │   assert exc_info[1] is not None            │                  
                    │   2356 │   │   │   │   raise exc_info[1].with_traceback(exc_info │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/engine/base.py:1967 in _exec_single_context │                  
                    │                                                                  │                  
                    │   1964 │   │   │   │   │   │   │   evt_handled = True            │                  
                    │   1965 │   │   │   │   │   │   │   break                         │                  
                    │   1966 │   │   │   │   if not evt_handled:                       │                  
                    │ ❱ 1967 │   │   │   │   │   self.dialect.do_execute(              │                  
                    │   1968 │   │   │   │   │   │   cursor, str_statement, effective_ │                  
                    │   1969 │   │   │   │   │   )                                     │                  
                    │   1970                                                           │                  
                    │                                                                  │                  
                    │ /home/dazzgt/.local/share/pipx/venvs/netexec/lib/python3.12/site │                  
                    │ -packages/sqlalchemy/engine/default.py:924 in do_execute         │                  
                    │                                                                  │                  
                    │    921 │   │   cursor.executemany(statement, parameters)         │                  
                    │    922 │                                                         │                  
                    │    923 │   def do_execute(self, cursor, statement, parameters, c │                  
                    │ ❱  924 │   │   cursor.execute(statement, parameters)             │                  
                    │    925 │                                                         │                  
                    │    926 │   def do_execute_no_params(self, cursor, statement, con │                  
                    │    927 │   │   cursor.execute(statement)                         │                  
                    ╰──────────────────────────────────────────────────────────────────╯                  
                    InterfaceError: (sqlite3.InterfaceError) bad parameter or other API                   
                    misuse                                                                                
                    [SQL: SELECT hosts.id, hosts.host, hosts.port, hosts.banner,                          
                    hosts.os                                                                              
                    FROM hosts                                                                            
                    WHERE hosts.host = ?]                                                                 
                    [parameters: ('172.x.x.x',)]                                                       
                    (Background on this error at: https://sqlalche.me/e/20/rvf5)   

NetExec info

NeffIsBack commented 1 month ago

These are Exception thrown by sqlalchemy, not sure if we can do much about it except updating the package

NeffIsBack commented 1 month ago

Maybe this is relatedd: https://github.com/Pennyw0rth/NetExec/issues/209#issuecomment-2278660141

dazzgt commented 1 month ago

@NeffIsBack documentation of SQLAlchemy says that this is driver error. I think it can be some kind of misuse of concurrention with SQLite. In my project with async tasks i just used Lock on my side because SQLite support is only one read write operation at the same time anyway. I tried to look for problem by myself but didn't localize it. Here is description of error from documenatation.

InterfaceError
Exception raised for errors that are related to the database interface rather than the database itself.

This error is a [DBAPI Error](https://docs.sqlalchemy.org/en/20/errors.html#error-dbapi) and originates from the database driver (DBAPI), not SQLAlchemy itself.

The InterfaceError is sometimes raised by drivers in the context of the database connection being dropped, or not being able to connect to the database. For tips on how to deal with this, see the section [Dealing with Disconnects](https://docs.sqlalchemy.org/en/20/core/pooling.html#pool-disconnects).

Most confusing part is that it write in DB normally but, because it have read problems, there a lot more entities in DB in the end.

NeffIsBack commented 4 weeks ago

From my understanding sqlalchemy should take care of concurrency issues with sqlite. Of course we are calling the nxcdb from several different (protocol) instances, but that should (afaik) not cause an issue and didn't to me until now. At least when the db file is on my own system drive. I once had an issue with sqlite when it was on an smb mounted share, but that is probably an edge case, which we can't solve anyway.

Can you reproduce it consistently somehow? If this a normal setup, or some kind of weird folder structure stuff that could confuse sqlalchemy?

AkechiShiro commented 4 weeks ago

I believe I can reproduce the issue, I'll try to run the command a 100 times and check if the output is always strictly the same or not. I'll report back my findings, on my side it was with nxc smb, I haven't tried the ssh module yet.

NeffIsBack commented 4 weeks ago

Just a setup would be nice so i can configure it and reproduce it on my side. Or indicators what the issue could be

AkechiShiro commented 4 weeks ago

I deployed Game Of Active Directory just as a lab for AD training (using Ludus on Proxmox), I think it would be possible to reproduce the issue with the default env but a more minimal example would help for sure maybe, two Windows VMs

dazzgt commented 4 weeks ago

Can you reproduce it consistently somehow? If this a normal setup, or some kind of weird folder structure stuff that could confuse sqlalchemy?

UPD: While i wrote this i actually though i can try to lock self.session.execute and check locally if it will resolve issue. I will check it now and tell you about result a bit later today

@NeffIsBack just normal setup through pipx on ubuntu. Absolutly basic user with my nickname in sudoers, but nxc installed and running without sudo. Everything in their base location like ~/.local and ~/.nxc. I think important part that i run it against subnet where a lot of hosts with SSH, like 30-50. In that case bug is reproduced in 100% cases but only in the begining when a lot of select query is executed. Then sometimes i see index error. If i run ssh module against just a few it work normal. Because of that i think root of problem in concurency.

I didin't work with DB isolation level much but how i know SQLAlchemy don't care about concurency at all. It just store data in memory based on different scope, dump this data on commit and use lazy load for read. But i work with DB in other direction. I create model and then make migrations with alembic and then create tables, but you create tables and then reflect on them. So i believe this may work differently. I using SQLite and SQLAlchemy in my project on work with async tasks, so i catched a lot of errors when there 20+ tasks working with DB. Because i need only simple select, upsert query so i just locked it with context manager like that.

class BaseRepository:
    lock = Lock()
    _conn: AsyncConnection

    async def get():....
    async def del():....
    async def etc():....

    async def __aenter__(self):
        await self.lock.acquire()
        self._conn = await engine.connect()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        try:
            await self._conn.commit()
            await self._conn.close()
        finally:
            self.lock.release()
dazzgt commented 4 weeks ago

@NeffIsBack lock on session execute completely eradicate of InterfaceError just replaced self.sess.execute with self.db_execute

from threading import Lock

class database:
    def __init__(self, db_engine):
        self.lock = Lock()

    def db_execute(self, *args):
        self.lock.acquire()
        res = self.sess.execute(*args)
        self.lock.release()
        return res

UPD: I will prepare PR in next few day. I think about create base class for database of all protocols. I beleave it's common issue for networks where a lot of hosts with available proto. If you think my solution is acceptable

dazzgt commented 4 weeks ago

Ok. I found that IndexError origin is the same. RowBase use some calls to DB. Most strange behavior for me is that calling __repr__ of host_result call the same _mapping as _asdict but don't raise this execption. I think this is because dict() call some other methods, that raise it. Also if __repr__ called before _asdict then there is no IndexError, probably because data is preloaded after __repr__ call.

AkechiShiro commented 3 weeks ago

Thanks a lot for your digging on this issue @dazzgt

Once you find some time to start a PR make sure to tag me along, I will try and test it on my side

NeffIsBack commented 2 weeks ago

Thanks a lot from me too! Probably @Marshall-Hallenbeck will take a look at the PR as soon as he has the time (he worked on the database the most and has much more knowledge about it than me)