├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── README.rst ├── htbcli ├── __init__.py └── __main__.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | dist/* 3 | htbcli.egg-info/* 4 | .bumpversion.cfg 5 | bin/* 6 | 7 | *__pycache__ 8 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019, Zach Hanson 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pylint = "*" 8 | 9 | [packages] 10 | requests = "==2.21.0" 11 | tabulate = "==0.8.5" 12 | argparse = "==1.4.0" 13 | htb = "==1.1.0" 14 | 15 | [requires] 16 | python_version = "3.7" 17 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "686fc36a7f8a8ff199e82f8d4175c1ecf018f0b1481dae9d6377693dc701f7dc" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "argparse": { 20 | "hashes": [ 21 | "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4", 22 | "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.4.0" 26 | }, 27 | "certifi": { 28 | "hashes": [ 29 | "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", 30 | "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" 31 | ], 32 | "version": "==2019.11.28" 33 | }, 34 | "chardet": { 35 | "hashes": [ 36 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 37 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 38 | ], 39 | "version": "==3.0.4" 40 | }, 41 | "htb": { 42 | "hashes": [ 43 | "sha256:655e2e3a8fa48fde498e625dfd65cc60283f4e514283a60384e495a3392b5f5e", 44 | "sha256:a2a119a86bb6d8d84f742165fbaeaf5a25c8b94c143ffa25f63c7739b21b8d8b" 45 | ], 46 | "index": "pypi", 47 | "version": "==1.1.0" 48 | }, 49 | "idna": { 50 | "hashes": [ 51 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 52 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 53 | ], 54 | "version": "==2.8" 55 | }, 56 | "requests": { 57 | "hashes": [ 58 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 59 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 60 | ], 61 | "index": "pypi", 62 | "version": "==2.21.0" 63 | }, 64 | "tabulate": { 65 | "hashes": [ 66 | "sha256:d0097023658d4dea848d6ae73af84532d1e86617ac0925d1adf1dd903985dac3" 67 | ], 68 | "index": "pypi", 69 | "version": "==0.8.5" 70 | }, 71 | "urllib3": { 72 | "hashes": [ 73 | "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", 74 | "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" 75 | ], 76 | "version": "==1.24.3" 77 | } 78 | }, 79 | "develop": { 80 | "astroid": { 81 | "hashes": [ 82 | "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", 83 | "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" 84 | ], 85 | "version": "==2.3.3" 86 | }, 87 | "isort": { 88 | "hashes": [ 89 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 90 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 91 | ], 92 | "version": "==4.3.21" 93 | }, 94 | "lazy-object-proxy": { 95 | "hashes": [ 96 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 97 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 98 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 99 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 100 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 101 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 102 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 103 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 104 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 105 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 106 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 107 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 108 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 109 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 110 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 111 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 112 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 113 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 114 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 115 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 116 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 117 | ], 118 | "version": "==1.4.3" 119 | }, 120 | "mccabe": { 121 | "hashes": [ 122 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 123 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 124 | ], 125 | "version": "==0.6.1" 126 | }, 127 | "pylint": { 128 | "hashes": [ 129 | "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", 130 | "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" 131 | ], 132 | "index": "pypi", 133 | "version": "==2.4.4" 134 | }, 135 | "six": { 136 | "hashes": [ 137 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 138 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 139 | ], 140 | "version": "==1.14.0" 141 | }, 142 | "typed-ast": { 143 | "hashes": [ 144 | "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", 145 | "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", 146 | "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", 147 | "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", 148 | "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", 149 | "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", 150 | "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", 151 | "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", 152 | "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", 153 | "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", 154 | "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", 155 | "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", 156 | "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", 157 | "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", 158 | "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", 159 | "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", 160 | "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", 161 | "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", 162 | "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", 163 | "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", 164 | "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" 165 | ], 166 | "markers": "implementation_name == 'cpython' and python_version < '3.8'", 167 | "version": "==1.4.1" 168 | }, 169 | "wrapt": { 170 | "hashes": [ 171 | "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" 172 | ], 173 | "version": "==1.11.2" 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hack the Box CLI 2 | ================ 3 | 4 | Just a small tool I threw together one day to make it easy to work with HTB from the command line. It uses the [API wrapper](https://github.com/kulinacs/htb) from @kulinacs with a few modifications. 5 | 6 | Usage 7 | -------- 8 | 9 | **Install** 10 | 11 | ```bash 12 | pip install htbcli 13 | ``` 14 | 15 | **Config** 16 | 17 | after installing the module. configure it with 18 | ```bash 19 | # For free users use this command. 20 | # Replace [your_key] with your actual api key from the settings page on HTB. 21 | htb config --lab=free --apiKey=[your_key] 22 | 23 | # For VIP Users its the same just pass vip instead of free to the --lab argument. 24 | htb config --lab=vip --apiKey=[your_key] 25 | ``` 26 | 27 | **List** 28 | 29 | You can list all the boxes on HTB. Just use the list command. 30 | ```bash 31 | $ htb list -h 32 | # usage: htb list [-h] [--retired] [--assigned] [--incomplete] [--sort-by field] 33 | # [--reverse] [-s SEPARATOR] [-rs ROW_SEPARATOR] [-q] 34 | # [-f field [field ...]] [-a] 35 | 36 | # optional arguments: 37 | # -h, --help show this help message and exit 38 | # --retired Include retired boxes in the output. [NOTE: Retired 39 | # boxes are only available to VIP users and cannot be 40 | # accessed by a free user.] 41 | # --assigned Show what machines are assigned to you. [VIP Only] 42 | # --incomplete Only show incomplete boxes in the output. An 43 | # incomplete box is one where you haven't owned both 44 | # user and root. 45 | # --sort-by field Field to sort by. This will sort the boxes by the 46 | # passed field. You can reverse the order by passing 47 | # --reverse. Certain fields like difficulty will be the 48 | # average value. To sort by the official HTB rank (ie 49 | # easy/medium/hard) sort by the amount of points the box 50 | # is/was assigned. 51 | # --reverse Reverse the order of boxes. This will return the list 52 | # sorted by the sort field in reverse. 53 | # -s SEPARATOR, --separator SEPARATOR 54 | # The separator to use when outputting the fields when 55 | # -q is set 56 | # -rs ROW_SEPARATOR, --row-separator ROW_SEPARATOR 57 | # The separator to use between rows when outputting the 58 | # fields when -q is set 59 | # -q, --quiet Output only the field values without any formatting. 60 | # Useful when parsing the output. 61 | # -f field [field ...], --fields field [field ...] 62 | # Limit the output to only these fields. All fields 63 | # shown when this is omitted. 64 | # -a, --all-fields Output every field on the machines. 65 | 66 | 67 | $ htb list 68 | # ╒══════╤════════════╤═════════╤══════════╤══════════════╤══════════════╤══════════╕ 69 | # │ id │ name │ os │ rating │ owned_user │ owned_root │ active │ 70 | # ╞══════╪════════════╪═════════╪══════════╪══════════════╪══════════════╪══════════╡ 71 | # │ 191 │ Smasher2 │ Linux │ 4.4 │ False │ False │ True │ 72 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 73 | # │ 193 │ Chainsaw │ Linux │ 4.2 │ False │ False │ True │ 74 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 75 | # │ 196 │ Player │ Linux │ 4.8 │ False │ False │ True │ 76 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 77 | # │ 197 │ Craft │ Linux │ 4.9 │ False │ False │ True │ 78 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 79 | # │ 198 │ RE │ Windows │ 4.4 │ False │ False │ True │ 80 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 81 | # │ 200 │ Rope │ Linux │ 4.7 │ False │ False │ True │ 82 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 83 | # │ 201 │ Heist │ Windows │ 4.4 │ True │ False │ True │ 84 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 85 | # │ 202 │ Scavenger │ Linux │ 3.3 │ False │ False │ True │ 86 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 87 | # │ 203 │ Networked │ Linux │ 3.7 │ False │ False │ True │ 88 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 89 | # │ 204 │ Zetta │ Linux │ 4.5 │ False │ False │ True │ 90 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 91 | # │ 207 │ Bitlab │ Linux │ 3.7 │ False │ False │ True │ 92 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 93 | # │ 208 │ Wall │ Linux │ 2.3 │ False │ False │ True │ 94 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 95 | # │ 209 │ Bankrobber │ Windows │ 2.7 │ False │ False │ True │ 96 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 97 | # │ 210 │ Json │ Windows │ 4.1 │ False │ False │ True │ 98 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 99 | # │ 211 │ Sniper │ Windows │ 4.5 │ False │ False │ True │ 100 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 101 | # │ 212 │ Forest │ Windows │ 4.6 │ False │ False │ True │ 102 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 103 | # │ 213 │ Registry │ Linux │ 4.4 │ False │ False │ True │ 104 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 105 | # │ 214 │ Mango │ Linux │ 3.8 │ True │ True │ True │ 106 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 107 | # │ 215 │ Postman │ Linux │ 3.9 │ False │ False │ True │ 108 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 109 | # │ 216 │ AI │ Linux │ 2.7 │ False │ False │ True │ 110 | # ╘══════╧════════════╧═════════╧══════════╧══════════════╧══════════════╧══════════╛ 111 | 112 | 113 | ``` 114 | 115 | 116 | **Info** 117 | 118 | You can see data on a single machine with the info command. 119 | 120 | ```bash 121 | $ htb info -h 122 | # usage: htb info [-h] [-s SEPARATOR] [-q] [-f field [field ...]] [-a] BOX 123 | 124 | # positional arguments: 125 | # BOX The name of the box you want info for. 126 | 127 | # optional arguments: 128 | # -h, --help show this help message and exit 129 | # -s SEPARATOR, --separator SEPARATOR 130 | # The separator to use when outputting the fields when 131 | # -q is set 132 | # -q, --quiet Output only the field values without any formatting. 133 | # Useful when parsing the output. 134 | # -f field [field ...], --fields field [field ...] 135 | # Limit the output to only these fields. All fields 136 | # shown when this is omitted. 137 | # -a, --all-fields Output every field on the machine. 138 | 139 | 140 | $ htb info lame 141 | # ╒═══════════════╤══════════════════════════════════════════════════════════════════════════════════════╕ 142 | # │ id │ 1 │ 143 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 144 | # │ name │ Lame │ 145 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 146 | # │ os │ Linux │ 147 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 148 | # │ ip │ 10.10.10.3 │ 149 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 150 | # │ avatar │ https://www.hackthebox.eu/storage/avatars/fb2d9f98400e3c802a0d7145e125c4ff.png │ 151 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 152 | # │ avatar_thumb │ https://www.hackthebox.eu/storage/avatars/fb2d9f98400e3c802a0d7145e125c4ff_thumb.png │ 153 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 154 | # │ points │ 20 │ 155 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 156 | # │ release │ 2017-03-14 21:54:51 │ 157 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 158 | # │ retired_date │ 2017-05-26 19:00:00 │ 159 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 160 | # │ maker │ id: 1 │ 161 | # │ │ name: ch4p │ 162 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 163 | # │ maker2 │ │ 164 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 165 | # │ ratings_pro │ 2331 │ 166 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 167 | # │ ratings_sucks │ 220 │ 168 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 169 | # │ user_blood │ id: 22 │ 170 | # │ │ name: 0x1Nj3cT0R │ 171 | # │ │ time: 18 days, 22 hours, 55 mins, 25 seconds │ 172 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 173 | # │ root_blood │ id: 22 │ 174 | # │ │ name: 0x1Nj3cT0R │ 175 | # │ │ time: 18 days, 22 hours, 54 mins, 36 seconds │ 176 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 177 | # │ user_owns │ 9949 │ 178 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 179 | # │ root_owns │ 10556 │ 180 | # ╘═══════════════╧══════════════════════════════════════════════════════════════════════════════════════╛ 181 | 182 | 183 | ``` 184 | 185 | 186 | **Reset** 187 | 188 | Of course you can also interact with the boxes. Here is how you request a reset of a box. 189 | 190 | ```bash 191 | $ htb reset -h 192 | # usage: htb reset [-h] BOX 193 | 194 | # positional arguments: 195 | # BOX The name of the box to reset. Resetting may take a few minutes 196 | # to take effect and may be cancelled by another user. 197 | 198 | # optional arguments: 199 | # -h, --help show this help message and exit 200 | 201 | $ htb reset mango 202 | # Attempting to reset Mango. This request often takes ~30 seconds, so be patient please... 203 | # success: 1 204 | # output: Mango will be reset in 2 minutes. 205 | # used: 0 206 | # of : 2 total resets 207 | # total: 2 208 | 209 | ``` 210 | 211 | 212 | **Own** 213 | 214 | You can submit flags with the own command. 215 | 216 | ```bash 217 | $ htb own -h 218 | # usage: htb own [-h] -f FLAG -d [1-10] BOX 219 | 220 | # positional arguments: 221 | # BOX The name of the box you want to own. 222 | 223 | # optional arguments: 224 | # -h, --help show this help message and exit 225 | # -f FLAG, --flag FLAG The flag you want to submit to own the box. user/root 226 | # is automatically determined by the server based on 227 | # what flag you submit. 228 | # -d [1-10], --difficulty [1-10] 229 | # The rating of how difficult you thought it was from 230 | # 1-10. 231 | 232 | 233 | $ htb own --flag=abcdefghijklmnopqrstuvwxyz123456 --difficulty=5 heist 234 | # Attempting to own Heist with flag: abcdefghijklmnopqrstuvwxyz123456 and rating: 5/9... 235 | # Heist user is now owned. 236 | # 1 237 | 238 | ``` 239 | 240 | 241 | VIP Only 242 | --------- 243 | 244 | **Spawn** 245 | 246 | You can interact with the new VIP interface's on demand launch capability with the spawn command. 247 | 248 | ```bash 249 | 250 | $ htb spawn -h 251 | # usage: htb spawn [-h] BOX 252 | 253 | # positional arguments: 254 | # BOX The name of the box to spawn. This will fail if you have another 255 | # box currently spawned. Terminate any spawned boxes and wait 256 | # until it actually shuts down before running this. 257 | 258 | # optional arguments: 259 | # -h, --help show this help message and exit 260 | 261 | $ htb spawn chainsaw 262 | # Attempting to spawn Chainsaw. This request often takes ~30 seconds, so be patient please... 263 | # success: 1 264 | # status: You have been assigned as an owner of this machine. 265 | 266 | ``` 267 | 268 | **Terminate** 269 | 270 | And once youre done owning a box. Just terminate it and move on. 271 | 272 | ```bash 273 | $ htb terminate -h 274 | # usage: htb terminate [-h] BOX 275 | 276 | # positional arguments: 277 | # BOX The name of the box to terminate. Termination may take up to a 278 | # few minutes to take effect. Until then you will not be able to 279 | # spawn any new boxes. 280 | 281 | # optional arguments: 282 | # -h, --help show this help message and exit 283 | 284 | $ htb terminate chainsaw 285 | # Attempting to terminate Chainsaw. This request often takes ~30 seconds, so be patient please... 286 | # success: 1 287 | # status: Machine scheduled for termination. 288 | ``` 289 | 290 | Suggestions 291 | ------------------ 292 | 293 | If anyone has any feature requests, I will gladly hear them out but can't guarantee I will have time to implement them. 294 | 295 | I'm @devx00 on HTB. And I am an admin of a Discord server dedicated to helping people get into InfoSec and (ethical) hacking in general. 296 | Feel free to message me at either, or on github. 297 | 298 | Heres a link to the Discord server for anyone interested. [NullzSec Discord](https://discord.gg/TYw582m) -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | Hack the Box CLI 3 | ================ 4 | 5 | Just a small tool I threw together one day to make it easy to work with HTB from the command line. It uses the `API wrapper `_ from @kulinacs with a few modifications. 6 | 7 | Usage 8 | ----- 9 | 10 | **Install** 11 | 12 | .. code-block:: bash 13 | 14 | pip install htbcli 15 | 16 | **Config** 17 | 18 | after installing the module. configure it with 19 | 20 | .. code-block:: bash 21 | 22 | # For free users use this command. 23 | # Replace [your_key] with your actual api key from the settings page on HTB. 24 | htb config --lab=free --apiKey=[your_key] 25 | 26 | # For VIP Users its the same just pass vip instead of free to the --lab argument. 27 | htb config --lab=vip --apiKey=[your_key] 28 | 29 | **List** 30 | 31 | You can list all the boxes on HTB. Just use the list command. 32 | 33 | .. code-block:: bash 34 | 35 | $ htb list -h 36 | # usage: htb list [-h] [--retired] [--assigned] [--incomplete] [--sort-by field] 37 | # [--reverse] [-s SEPARATOR] [-rs ROW_SEPARATOR] [-q] 38 | # [-f field [field ...]] [-a] 39 | 40 | # optional arguments: 41 | # -h, --help show this help message and exit 42 | # --retired Include retired boxes in the output. [NOTE: Retired 43 | # boxes are only available to VIP users and cannot be 44 | # accessed by a free user.] 45 | # --assigned Show what machines are assigned to you. [VIP Only] 46 | # --incomplete Only show incomplete boxes in the output. An 47 | # incomplete box is one where you haven't owned both 48 | # user and root. 49 | # --sort-by field Field to sort by. This will sort the boxes by the 50 | # passed field. You can reverse the order by passing 51 | # --reverse. Certain fields like difficulty will be the 52 | # average value. To sort by the official HTB rank (ie 53 | # easy/medium/hard) sort by the amount of points the box 54 | # is/was assigned. 55 | # --reverse Reverse the order of boxes. This will return the list 56 | # sorted by the sort field in reverse. 57 | # -s SEPARATOR, --separator SEPARATOR 58 | # The separator to use when outputting the fields when 59 | # -q is set 60 | # -rs ROW_SEPARATOR, --row-separator ROW_SEPARATOR 61 | # The separator to use between rows when outputting the 62 | # fields when -q is set 63 | # -q, --quiet Output only the field values without any formatting. 64 | # Useful when parsing the output. 65 | # -f field [field ...], --fields field [field ...] 66 | # Limit the output to only these fields. All fields 67 | # shown when this is omitted. 68 | # -a, --all-fields Output every field on the machines. 69 | 70 | 71 | $ htb list 72 | # ╒══════╤════════════╤═════════╤══════════╤══════════════╤══════════════╤══════════╕ 73 | # │ id │ name │ os │ rating │ owned_user │ owned_root │ active │ 74 | # ╞══════╪════════════╪═════════╪══════════╪══════════════╪══════════════╪══════════╡ 75 | # │ 191 │ Smasher2 │ Linux │ 4.4 │ False │ False │ True │ 76 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 77 | # │ 193 │ Chainsaw │ Linux │ 4.2 │ False │ False │ True │ 78 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 79 | # │ 196 │ Player │ Linux │ 4.8 │ False │ False │ True │ 80 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 81 | # │ 197 │ Craft │ Linux │ 4.9 │ False │ False │ True │ 82 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 83 | # │ 198 │ RE │ Windows │ 4.4 │ False │ False │ True │ 84 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 85 | # │ 200 │ Rope │ Linux │ 4.7 │ False │ False │ True │ 86 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 87 | # │ 201 │ Heist │ Windows │ 4.4 │ True │ False │ True │ 88 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 89 | # │ 202 │ Scavenger │ Linux │ 3.3 │ False │ False │ True │ 90 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 91 | # │ 203 │ Networked │ Linux │ 3.7 │ False │ False │ True │ 92 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 93 | # │ 204 │ Zetta │ Linux │ 4.5 │ False │ False │ True │ 94 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 95 | # │ 207 │ Bitlab │ Linux │ 3.7 │ False │ False │ True │ 96 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 97 | # │ 208 │ Wall │ Linux │ 2.3 │ False │ False │ True │ 98 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 99 | # │ 209 │ Bankrobber │ Windows │ 2.7 │ False │ False │ True │ 100 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 101 | # │ 210 │ Json │ Windows │ 4.1 │ False │ False │ True │ 102 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 103 | # │ 211 │ Sniper │ Windows │ 4.5 │ False │ False │ True │ 104 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 105 | # │ 212 │ Forest │ Windows │ 4.6 │ False │ False │ True │ 106 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 107 | # │ 213 │ Registry │ Linux │ 4.4 │ False │ False │ True │ 108 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 109 | # │ 214 │ Mango │ Linux │ 3.8 │ True │ True │ True │ 110 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 111 | # │ 215 │ Postman │ Linux │ 3.9 │ False │ False │ True │ 112 | # ├──────┼────────────┼─────────┼──────────┼──────────────┼──────────────┼──────────┤ 113 | # │ 216 │ AI │ Linux │ 2.7 │ False │ False │ True │ 114 | # ╘══════╧════════════╧═════════╧══════════╧══════════════╧══════════════╧══════════╛ 115 | 116 | **Info** 117 | 118 | You can see data on a single machine with the info command. 119 | 120 | .. code-block:: bash 121 | 122 | $ htb info -h 123 | # usage: htb info [-h] [-s SEPARATOR] [-q] [-f field [field ...]] [-a] BOX 124 | 125 | # positional arguments: 126 | # BOX The name of the box you want info for. 127 | 128 | # optional arguments: 129 | # -h, --help show this help message and exit 130 | # -s SEPARATOR, --separator SEPARATOR 131 | # The separator to use when outputting the fields when 132 | # -q is set 133 | # -q, --quiet Output only the field values without any formatting. 134 | # Useful when parsing the output. 135 | # -f field [field ...], --fields field [field ...] 136 | # Limit the output to only these fields. All fields 137 | # shown when this is omitted. 138 | # -a, --all-fields Output every field on the machine. 139 | 140 | 141 | $ htb info lame 142 | # ╒═══════════════╤══════════════════════════════════════════════════════════════════════════════════════╕ 143 | # │ id │ 1 │ 144 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 145 | # │ name │ Lame │ 146 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 147 | # │ os │ Linux │ 148 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 149 | # │ ip │ 10.10.10.3 │ 150 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 151 | # │ avatar │ https://www.hackthebox.eu/storage/avatars/fb2d9f98400e3c802a0d7145e125c4ff.png │ 152 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 153 | # │ avatar_thumb │ https://www.hackthebox.eu/storage/avatars/fb2d9f98400e3c802a0d7145e125c4ff_thumb.png │ 154 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 155 | # │ points │ 20 │ 156 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 157 | # │ release │ 2017-03-14 21:54:51 │ 158 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 159 | # │ retired_date │ 2017-05-26 19:00:00 │ 160 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 161 | # │ maker │ id: 1 │ 162 | # │ │ name: ch4p │ 163 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 164 | # │ maker2 │ │ 165 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 166 | # │ ratings_pro │ 2331 │ 167 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 168 | # │ ratings_sucks │ 220 │ 169 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 170 | # │ user_blood │ id: 22 │ 171 | # │ │ name: 0x1Nj3cT0R │ 172 | # │ │ time: 18 days, 22 hours, 55 mins, 25 seconds │ 173 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 174 | # │ root_blood │ id: 22 │ 175 | # │ │ name: 0x1Nj3cT0R │ 176 | # │ │ time: 18 days, 22 hours, 54 mins, 36 seconds │ 177 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 178 | # │ user_owns │ 9949 │ 179 | # ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ 180 | # │ root_owns │ 10556 │ 181 | # ╘═══════════════╧══════════════════════════════════════════════════════════════════════════════════════╛ 182 | 183 | **Reset** 184 | 185 | Of course you can also interact with the boxes. Here is how you request a reset of a box. 186 | 187 | .. code-block:: bash 188 | 189 | $ htb reset -h 190 | # usage: htb reset [-h] BOX 191 | 192 | # positional arguments: 193 | # BOX The name of the box to reset. Resetting may take a few minutes 194 | # to take effect and may be cancelled by another user. 195 | 196 | # optional arguments: 197 | # -h, --help show this help message and exit 198 | 199 | $ htb reset mango 200 | # Attempting to reset Mango. This request often takes ~30 seconds, so be patient please... 201 | # success: 1 202 | # output: Mango will be reset in 2 minutes. 203 | # used: 0 204 | # of : 2 total resets 205 | # total: 2 206 | 207 | **Own** 208 | 209 | You can submit flags with the own command. 210 | 211 | .. code-block:: bash 212 | 213 | $ htb own -h 214 | # usage: htb own [-h] -f FLAG -d [1-10] BOX 215 | 216 | # positional arguments: 217 | # BOX The name of the box you want to own. 218 | 219 | # optional arguments: 220 | # -h, --help show this help message and exit 221 | # -f FLAG, --flag FLAG The flag you want to submit to own the box. user/root 222 | # is automatically determined by the server based on 223 | # what flag you submit. 224 | # -d [1-10], --difficulty [1-10] 225 | # The rating of how difficult you thought it was from 226 | # 1-10. 227 | 228 | 229 | $ htb own --flag=abcdefghijklmnopqrstuvwxyz123456 --difficulty=5 heist 230 | # Attempting to own Heist with flag: abcdefghijklmnopqrstuvwxyz123456 and rating: 5/9... 231 | # Heist user is now owned. 232 | # 1 233 | 234 | VIP Only 235 | -------- 236 | 237 | **Spawn** 238 | 239 | You can interact with the new VIP interface's on demand launch capability with the spawn command. 240 | 241 | .. code-block:: bash 242 | 243 | 244 | $ htb spawn -h 245 | # usage: htb spawn [-h] BOX 246 | 247 | # positional arguments: 248 | # BOX The name of the box to spawn. This will fail if you have another 249 | # box currently spawned. Terminate any spawned boxes and wait 250 | # until it actually shuts down before running this. 251 | 252 | # optional arguments: 253 | # -h, --help show this help message and exit 254 | 255 | $ htb spawn chainsaw 256 | # Attempting to spawn Chainsaw. This request often takes ~30 seconds, so be patient please... 257 | # success: 1 258 | # status: You have been assigned as an owner of this machine. 259 | 260 | **Terminate** 261 | 262 | And once youre done owning a box. Just terminate it and move on. 263 | 264 | .. code-block:: bash 265 | 266 | $ htb terminate -h 267 | # usage: htb terminate [-h] BOX 268 | 269 | # positional arguments: 270 | # BOX The name of the box to terminate. Termination may take up to a 271 | # few minutes to take effect. Until then you will not be able to 272 | # spawn any new boxes. 273 | 274 | # optional arguments: 275 | # -h, --help show this help message and exit 276 | 277 | $ htb terminate chainsaw 278 | # Attempting to terminate Chainsaw. This request often takes ~30 seconds, so be patient please... 279 | # success: 1 280 | # status: Machine scheduled for termination. 281 | 282 | Suggestions 283 | ----------- 284 | 285 | If anyone has any feature requests, I will gladly hear them out but can't guarantee I will have time to implement them. 286 | 287 | I'm @devx00 on HTB. And I am an admin of a Discord server dedicated to helping people get into InfoSec and (ethical) hacking in general. 288 | Feel free to message me at either, or on github. 289 | 290 | Heres a link to the Discord server for anyone interested. `NullzSec Discord `_ 291 | -------------------------------------------------------------------------------- /htbcli/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A wrapper around the Hack the Box API 3 | """ 4 | import requests 5 | from htb import HTB, HTBAPIError 6 | 7 | 8 | __version__ = '1.1.7' 9 | 10 | class HTBCLIError(HTBAPIError): 11 | """Raised when API fails""" 12 | def __init__(self, expression, message=""): 13 | self.expression = expression 14 | self.message = message 15 | 16 | class HTBAPI(HTB): 17 | """ 18 | Extends Hack the Box API Wrapper 19 | 20 | :attr api_key: API Key used for authenticated queries 21 | :attr user_agent: The User-Agent to be used with all requests 22 | """ 23 | 24 | def __init__(self, api_key, user_agent='Python HTB Client/{}'.format(__version__)): 25 | HTB.__init__(self, api_key, user_agent) 26 | self.headers['Authorization'] = f"Bearer {api_key}" 27 | HTB._validate_response = HTBAPI._validate_response 28 | 29 | @staticmethod 30 | def _validate_response(response): 31 | """ 32 | Overridden to implement additional error message 33 | Validate the response from the API 34 | 35 | :params response: the response dict received from an API call 36 | :returns: the response dict if the call was successfull 37 | """ 38 | if "success" in response and response['success'] != '1' and response['success'] != 1: 39 | message = "\n".join([ f"{k}: {v}" for k,v in response.items() ]) 40 | raise HTBCLIError("success != 1", message=message) 41 | return response 42 | 43 | def get_owns(self) -> dict: 44 | """ 45 | Get which machines the user has owned. 46 | 47 | :params self: HTB object in use 48 | :returns: machines dict 49 | """ 50 | return requests.get(self.BASE_URL + self._auth('/machines/owns'), headers=self.headers).json() 51 | 52 | def get_assigned(self) -> dict: 53 | """ 54 | Get which machines the user has assigned to them. 55 | 56 | :params self: HTB object in use 57 | :returns: machines dict 58 | 59 | VIP Only 60 | """ 61 | return requests.get(self.BASE_URL + self._auth('/machines/assigned'), headers=self.headers).json() 62 | 63 | def get_difficulties(self) -> dict: 64 | """ 65 | Get machines difficulty. 66 | 67 | :params self: HTB object in use 68 | :returns: machines dict 69 | """ 70 | return requests.get(self.BASE_URL + self._auth('/machines/difficulty'), headers=self.headers).json() 71 | 72 | 73 | def spawn_machine(self, mid: int, lab="vip") -> (int, str): 74 | """ 75 | Spawn a machine 76 | 77 | :params self: HTB object in use 78 | :params mid: Machine ID or 0 for arena. 79 | :params lab: vip for vip users, unknown for free. 80 | :returns: bool if successful, str status message 81 | """ 82 | try: 83 | if mid == 0: 84 | resp = self._get('/machines/release/spawn') 85 | else: 86 | resp = self._post(self._auth('/vm/{}/assign/{}'.format(lab, mid))) 87 | status = resp['status'] if 'status' in resp else resp['message'] 88 | return (resp['success'], status) 89 | except HTBAPIError as e: 90 | print(e.message) 91 | return False, "An Error Occurred" 92 | 93 | def terminate_machine(self, mid: int, lab="vip") -> (int, str): 94 | """ 95 | Terminate a machine 96 | 97 | :params self: HTB object in use 98 | :params mid: Machine ID or 0 for arena 99 | :params lab: vip for vip users, unknown for free. 100 | :returns: bool if successful, str status message 101 | """ 102 | try: 103 | if mid == 0: 104 | resp = self._get('/machines/release/terminate') 105 | else: 106 | resp = self._post(self._auth('/vm/{}/remove/{}'.format(lab, mid))) 107 | status = resp['status'] if 'status' in resp else resp['message'] 108 | return (resp['success'], status) 109 | except HTBAPIError as e: 110 | print(e.message) 111 | return False, "An Error Occurred" 112 | 113 | def own_machine(self, mid: int, hsh: str, diff: int) -> (int, str): 114 | """ 115 | Own a challenge on a machine 116 | 117 | :params self: HTB object in use 118 | :params mid: Machine ID or 0 for arena 119 | :params hsh: Flag Hash 120 | :params diff: difficult (10-100) 121 | :returns: bool if successful 122 | """ 123 | try: 124 | endpoint = "/machines/release/own" if mid == 0 else '/machines/own' 125 | resp = self._post(self._auth(endpoint), {"id": mid, "flag": hsh, "difficulty": diff}) 126 | status = resp['status'] if 'status' in resp else resp['message'] 127 | return (resp['success'], status) 128 | except HTBAPIError as e: 129 | print(e.message) 130 | return False, "An Error Occurred" 131 | 132 | def reset_machine(self, mid: int) -> dict: 133 | """ 134 | Reset a machine 135 | 136 | :params self: HTB object in use 137 | :params mid: Machine ID or 0 for arena 138 | :returns: dict of info 139 | """ 140 | try: 141 | if mid == 0: 142 | resp = self._get('/machines/release/reset') 143 | else: 144 | resp = super().reset_machine(mid) 145 | return resp 146 | except HTBAPIError as e: 147 | print(e.message) 148 | return False, "An Error Occurred" 149 | 150 | def arena_stats(self) -> dict: 151 | """ 152 | Get arena stats. 153 | 154 | :params self: HTB object in use 155 | :returns: dict of arena info. 156 | """ 157 | try: 158 | return self._get('/machines/release/stats') 159 | except HTBAPIError as e: 160 | print(e.message) 161 | return False, "An Error Occurred" 162 | 163 | def arena_owns(self, mid: int) -> dict: 164 | """ 165 | Get arena machine own info. 166 | 167 | :params self: HTB object in use 168 | :params mid: Machine ID 169 | :returns: dict of info 170 | """ 171 | try: 172 | resp = self._get(f"/machines/release/owns/{mid}") 173 | return resp["owns"] 174 | except HTBAPIError as e: 175 | print(e.message) 176 | return False, "An Error Occurred" 177 | 178 | def active_arena(self) -> dict: 179 | """ 180 | Get active arena machine info. 181 | 182 | :params self: HTB object in use 183 | :returns: dict of info 184 | """ 185 | try: 186 | resp = self._get(f"/machines/release/active") 187 | return resp 188 | except HTBAPIError as e: 189 | print(e.message) 190 | return {'spawned': False} 191 | 192 | def select_arena(self, region:str) -> bool: 193 | """ 194 | Select arena region. 195 | 196 | :params self: HTB object in use 197 | :params region: us | eu. 198 | :returns: bool success 199 | """ 200 | try: 201 | resp = self._post(self._auth(f"/labs/switch/{region.lower()}release")) 202 | return resp['status'] == 1 203 | except HTBAPIError as e: 204 | print(e.message) 205 | return False 206 | 207 | -------------------------------------------------------------------------------- /htbcli/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A command line utility for interacting with the Hack the Box API 3 | """ 4 | from htbcli import HTBAPI 5 | import argparse 6 | from argparse import ArgumentParser 7 | from pathlib import Path 8 | import configparser 9 | import sys 10 | from tabulate import tabulate 11 | from termcolor import colored 12 | from math import floor 13 | apiKey = None 14 | lab = None 15 | machines = [] 16 | api = None 17 | configfile = None 18 | config = None 19 | 20 | def init_api(apiKey): 21 | global api 22 | api = HTBAPI(apiKey) 23 | 24 | def init_config(): 25 | """ 26 | Initialize the configuration directory and files. This will touch the file whether it already exists or not. 27 | """ 28 | global config, configfile 29 | 30 | config = configparser.ConfigParser() 31 | 32 | configdir = Path.home().joinpath(".config/htb") 33 | configdir.mkdir(parents=True, exist_ok=True) 34 | 35 | configfile = configdir.joinpath('htb.conf') 36 | configfile.touch() 37 | 38 | def write_config(): 39 | """ 40 | Write the config object to the config file. 41 | """ 42 | with configfile.open('w') as f: 43 | config.write(f) 44 | 45 | def load_config(): 46 | """ 47 | Load the config object from the config file. 48 | """ 49 | global configfile, config, apiKey, lab 50 | init_config() 51 | config.read(configfile) 52 | if "Auth" not in config.sections(): 53 | config['Auth'] = {'apiKey': '', 'lab':'free'} 54 | write_config() 55 | if "List" not in config.sections(): 56 | defaultFields = ["id", "name", "os", "rating", "difficulty", 57 | "points", "owned_user", "owned_root", "active"] 58 | config['List'] = {'defaultFields': ','.join(defaultFields)} 59 | write_config() 60 | 61 | if "Info" not in config.sections(): 62 | defaultFields = ["id", "name", "os", "rating", "difficulty", 63 | "points", "owned_user", "owned_root", "user_blood", "root_blood", "maker", "maker2", "active"] 64 | config['Info'] = {'defaultFields': ','.join(defaultFields)} 65 | write_config() 66 | 67 | apiKey = config['Auth'].get('apiKey') 68 | lab = config['Auth'].get('lab') 69 | 70 | def print_config(): 71 | print("\nCurrent Configuration") 72 | for section in config.sections(): 73 | for key, val in config.items(section): 74 | print(f"\t{key}:\t{val}") 75 | print("\n") 76 | 77 | def load_machines(): 78 | """ 79 | Load the machines, and the users owned machines 80 | and store them globally, since everything uses them. 81 | """ 82 | global machines 83 | def add_vals(machine): 84 | """ 85 | Adds whether the user has owned user or root on this box yet or not. 86 | """ 87 | machine['owned_user'] = owns_dict[machine['id']]['owned_user'] if machine['id'] in owns_dict else False 88 | machine['owned_root'] = owns_dict[machine['id']]['owned_root'] if machine['id'] in owns_dict else False 89 | machine['difficulty'] = format_diff(diffs_dict[machine['id']]['difficulty_ratings']) if machine['id'] in diffs_dict else "" 90 | machine['difficulty_arr'] = diffs_dict[machine['id']]['difficulty_ratings'] if machine['id'] in diffs_dict else [] 91 | machine['active'] = not machine['retired'] 92 | return machine 93 | 94 | owns = api.get_owns() 95 | owns_dict = dict(zip([m['id'] for m in owns], owns)) 96 | diffs = api.get_difficulties() 97 | diffs_dict = dict(zip([m['id'] for m in diffs], diffs)) 98 | machines = list(map(add_vals, api.get_machines())) 99 | 100 | def get_id(name): 101 | """ 102 | A helper function to translate a name into the machine's 103 | id number since thats what the api expects. 104 | """ 105 | for machine in machines: 106 | if machine['name'].lower() == name.lower(): 107 | return machine['id'] 108 | return None 109 | 110 | 111 | def get_box(name): 112 | """ 113 | Finds and returns a box with the given name from the machines list. 114 | """ 115 | for machine in machines: 116 | if machine['name'].lower() == name.lower(): 117 | return machine 118 | return None 119 | 120 | def format_value(val): 121 | """ 122 | A helper function to format fields for table output. 123 | Converts nested dicts into multilist strings. 124 | """ 125 | if isinstance(val, dict): 126 | return "\n".join([ f"{k}: {val[k]}" for k in val.keys()]) 127 | return val 128 | 129 | def normalized_diff(difficulty): 130 | max_val = max(difficulty) 131 | step_size = float(max_val) / 7.0 132 | if step_size > 0: 133 | return list(map(lambda x: floor(float(x)/step_size), difficulty)) 134 | else: 135 | return list(map(lambda x: 0, difficulty)) 136 | 137 | 138 | def format_diff(difficulty): 139 | bars = "▁▂▃▄▅▆▇█" 140 | bar_arr = list(bars) 141 | colors = ["green", "green", "green", "yellow", "yellow", "yellow", "yellow", "red", "red", "red"] 142 | normal_diff = normalized_diff(difficulty) 143 | formatted = [] 144 | for i in range(10): 145 | val = normal_diff[i] 146 | bar_val = bar_arr[val] 147 | color = colors[i] 148 | formatted.append(colored(bar_val, color)) 149 | return "".join(formatted) 150 | 151 | def print_machine(machine, fields=None): 152 | """ 153 | Print a machine and all of the requested fields. 154 | In the future we will implement more formatting options. 155 | """ 156 | fields = fields or machine.keys() 157 | vals = list(map(lambda x: format_value(machine[x]), fields)) 158 | items = list(zip(fields, vals)) 159 | print(tabulate(items, tablefmt="fancy_grid")) 160 | 161 | def print_machine_list(machines, fields=["id", "name", "os", "rating", "owned_user", "owned_root", "active"]): 162 | 163 | reduced_machines = list(map(lambda machine: [ format_value(machine[field]) for field in fields], machines)) 164 | print(tabulate(reduced_machines,headers=fields, tablefmt="fancy_grid")) 165 | 166 | def do_config(args): 167 | """ 168 | Handle configuration commands. 169 | Currently this only includes setting or retrieving the current api key. 170 | """ 171 | global apiKey, lab, config 172 | config['Auth']['apiKey'] = args.apiKey or config['Auth']['apiKey'] 173 | config['Auth']['lab'] = args.lab or config['Auth']['lab'] 174 | config['List']['defaultFields'] = ",".join(args.listFields) if args.listFields else config['List']['defaultFields'] 175 | write_config() 176 | apiKey = args.apiKey 177 | 178 | print_config() 179 | 180 | 181 | def do_list(args): 182 | """ 183 | Handle the list command. 184 | Current filters limited to --retired and --incomplete, 185 | but more will hopefully be implemented in the near future. 186 | """ 187 | global machines 188 | def filter_machine(machine): 189 | """ 190 | Check if machine matches the flags passed. (ie retired, owned_user, owned_root, or assigned) 191 | """ 192 | machine_retired = machine['retired'] 193 | machine_incomplete = not machine['owned_user'] or not machine['owned_root'] 194 | matches = (machine_incomplete or not args.incomplete) and (not machine_retired or args.retired) 195 | if machine_ids is not None: 196 | matches = matches and machine['id'] in machine_ids 197 | return matches 198 | def sort_value(machine): 199 | sort_field = args.sort_by 200 | sort_val = machine[sort_field] if sort_field in machine else machine['id'] 201 | if sort_field == 'difficulty' and isinstance(machine['difficulty_arr'], list): 202 | sort_val = machine['difficulty_arr'] 203 | total_votes = sum(sort_val) 204 | weighted_vals = [ sort_val[i] * (i + 1) for i in range(len(sort_val))] 205 | weighted_sum = sum(weighted_vals) 206 | avg_diff = float(weighted_sum)/float(total_votes) 207 | sort_val = avg_diff 208 | 209 | if sort_val is None: 210 | sort_val = default_type() 211 | if type(sort_val) not in [str, bool, int, float]: 212 | sort_val = machine['id'] 213 | 214 | 215 | return sort_val 216 | 217 | 218 | for machine in machines: 219 | if args.sort_by in machine and machine[args.sort_by] is not None: 220 | default_type = type(machine[args.sort_by]) 221 | break 222 | 223 | 224 | if args.all_fields: 225 | fields = machines[0].keys() if len(machines) > 0 else [] 226 | else: 227 | fields = args.fields 228 | 229 | machine_ids=None 230 | if args.assigned: 231 | assigned = api.get_assigned() 232 | machine_ids = list(map(lambda x: x['id'], assigned)) 233 | 234 | 235 | filtered_machines = list(filter(filter_machine, machines)) 236 | filtered_machines.sort(key=sort_value, reverse=args.reverse) 237 | if args.quiet: 238 | print(args.row_separator.join([ args.separator.join([ str(machine[field]) for field in fields]) for machine in filtered_machines])) 239 | else: 240 | print_machine_list(filtered_machines, fields) 241 | 242 | def do_info(args): 243 | """ 244 | Handle the info command. 245 | Currently only prints out the values of the specified box. 246 | Eventually, this should include options to better format the output. 247 | """ 248 | machine = get_box(args.box) 249 | machine.update(api.get_machine(machine['id'])) 250 | if args.all_fields: 251 | fields = machine.keys() 252 | else: 253 | fields = args.fields or machine.keys() 254 | 255 | if args.quiet: 256 | vals = [ str(machine[f]) for f in fields ] 257 | print(args.separator.join(vals)) 258 | elif args.list_format: 259 | print_machine_list([machine], fields) 260 | else: 261 | print_machine(machine, fields) 262 | 263 | 264 | def do_spawn(args): 265 | """ 266 | Handle the spawn command. 267 | This will try to spawn the specified box. 268 | This command will fail if the user already has a box that is 269 | assigned to them. Make sure to terminate any boxes assigned to you 270 | before running this. 271 | """ 272 | if lab == "free": 273 | print("Free users cannot spawn machines. Please use reset instead.") 274 | sys.exit(1) 275 | print(f"Attempting to spawn {args.box.capitalize()}. This request often takes ~30 seconds, so be patient please...") 276 | res, message = api.spawn_machine(get_id(args.box)) 277 | print(message) 278 | sys.exit(res) 279 | 280 | def do_own(args): 281 | """ 282 | Handle the own command. 283 | This will try to own the specified box with the specified flag 284 | and the difficulty rating passed. 285 | NOTE: The difficulty rating is mandatory. 286 | """ 287 | flag = args.flag 288 | diff = args.difficulty 289 | print(f"Attempting to own {args.box.capitalize()} with flag: {flag} and rating: {diff}/9...") 290 | res, message = api.own_machine(get_id(args.box), flag, diff*10) 291 | print(message) 292 | sys.exit(res) 293 | 294 | def do_terminate(args): 295 | """ 296 | Handle the terminate command. 297 | This will attempt to terminate the specified box. 298 | NOTE: This command can take up to a couple minutes to successfully terminate 299 | or de-assign the box from the user. 300 | """ 301 | if lab == "free": 302 | print("Free users cannot terminate machines. Please use reset instead.") 303 | sys.exit(1) 304 | print(f"Attempting to terminate {args.box.capitalize()}. This request often takes ~30 seconds, so be patient please...") 305 | res, message = api.terminate_machine(get_id(args.box)) 306 | print(message) 307 | sys.exit(res) 308 | 309 | def do_reset(args): 310 | """ 311 | Handle the reset command. 312 | This will attempt to reset the specified box. 313 | NOTE: This command can take up to a couple minutes to 314 | successfully reset and can be cancelled by other users. 315 | """ 316 | print(f"Attempting to reset {args.box.capitalize()}. This request often takes ~30 seconds, so be patient please...") 317 | response = api.reset_machine(get_id(args.box)) 318 | print(format_value(response)) 319 | 320 | def show_help(parser, command=None): 321 | args = [] 322 | if command is not None: 323 | args = [command] 324 | if not "-h" in sys.argv and not "--help" in sys.argv: 325 | args.append('-h') 326 | print("\n") 327 | parser.parse_args(args) 328 | 329 | def do_arena_spawn(args): 330 | stat, _ = api.spawn_machine(0) 331 | if stat: 332 | active = api.active_arena() 333 | print(active['ip']) 334 | else: 335 | print("Failed to spawn arena machine.") 336 | sys.exit(1) 337 | 338 | def do_arena_terminate(args): 339 | _, message = api.terminate_machine(0) 340 | print(message) 341 | 342 | def do_arena_reset(args): 343 | stat, message = api.reset_machine(0) 344 | if stat: 345 | active = api.active_arena() 346 | print(active['ip']) 347 | else: 348 | print(message) 349 | sys.exit(1) 350 | 351 | def do_arena_own(args): 352 | _, message = api.own_machine(0, hsh=args.flag, diff=args.difficulty) 353 | print(message) 354 | 355 | def do_arena_view(args): 356 | active = api.active_arena() 357 | stats = api.arena_stats() 358 | load_machines() 359 | owns = api.arena_owns(get_id(stats['machine'])) 360 | details = {**stats, **owns, **active} 361 | print_machine(details) 362 | 363 | 364 | def do_arena_region(args): 365 | resp = api.select_arena(args.region) 366 | if resp: 367 | print(f"Successfully switched to server.\nBe sure to download your new connection pack.") 368 | else: 369 | print(f"Failed to switch to server") 370 | 371 | def parse_args(): 372 | """ 373 | Setup the argument parser. 374 | The parser is setup to use subcommands so that each command can be extended in the future with its own arguments. 375 | """ 376 | ArgumentParser() 377 | parser = ArgumentParser( 378 | prog="htb", 379 | description="This is a simple command line utility for interacting with the Hack the Box API. In order to use this tool you must find your api key in your HTB settings and configure this tool to use it with `htb config --apiKey=YOURAPIKEY`", 380 | epilog="You must set your apiKey by running: htb config --apiKey=APIKEY --lab=[free or vip]" if not apiKey else "Try: htb [command] --help" 381 | ) 382 | parser.set_defaults(command=None) 383 | command_parsers = parser.add_subparsers(title="commands", prog="htb") 384 | 385 | config_parser = command_parsers.add_parser("config", help="configure this tool") 386 | config_parser.add_argument("--apiKey", type=str, help="Your HTB api key. You can find this on your HTB settings page. THIS MUST BE SET BEFORE YOU CAN USE THIS TOOL FOR ANYTHING ELSE.") 387 | config_parser.add_argument("--lab", type=str, choices=["free", "vip"], help="Which lab you connect to, either free or vip. If you do not pay for your HTB account then you need to pass free or this wont work.") 388 | config_parser.add_argument("--listFields", type=str, nargs="+", metavar="field", help="The default fields to show in List mode.") 389 | config_parser.set_defaults(func=do_config, command="config") 390 | 391 | defaultListFields = [x.strip() for x in config['List'].get('defaultFields').split(',')] 392 | list_parser = command_parsers.add_parser("list", help="list machines") 393 | list_parser.add_argument("--retired", action="store_true", help="Include retired boxes in the output. [NOTE: Retired boxes are only available to VIP users and cannot be accessed by a free user.]") 394 | list_parser.add_argument("--assigned", action="store_true", help="Show what machines are assigned to you. [VIP Only]") 395 | list_parser.add_argument("--incomplete", action="store_true", help="Only show incomplete boxes in the output.\nAn incomplete box is one where you haven't owned both user and root.") 396 | list_parser.add_argument("--sort-by", type=str, default="id", metavar="field", help="Field to sort by.\nThis will sort the boxes by the passed field. To see more or less what fields you can sort by list the boxes with the -a flag and look at the column headers. Not all fields are valid sort-by fields though. Defaults to 'id' if not present or invalid field") 397 | list_parser.add_argument("--reverse", action="store_true", help="Reverse the order of boxes.\nThis will return the list sorted by the sort field in reverse.") 398 | list_parser.add_argument("-s", "--separator", type=str, default=" ", help="The separator to use when outputting the fields when -q is set") 399 | list_parser.add_argument("-rs", "--row-separator", type=str, default="\n", help="The separator to use between rows when outputting the fields when -q is set") 400 | list_parser.add_argument("-q", "--quiet", action="store_true", help="Output only the field values without any formatting. Useful when parsing the output.") 401 | list_parser.add_argument("-f", "--fields", metavar="field", default=defaultListFields, nargs="+", type=str, 402 | help="Limit the output to only these fields. The default fields to show can be set with the config command.") 403 | list_parser.add_argument("-a", "--all-fields", action="store_true", help="Output every field on the machines.") 404 | list_parser.set_defaults(func=do_list, command="list") 405 | 406 | defaultInfoFields = [x.strip() for x in config['Info'].get('defaultFields').split(',')] 407 | info_parser = command_parsers.add_parser("info", help="get info about a machine") 408 | info_parser.add_argument("box", metavar="BOX", type=str, help="The name of the box you want info for.") 409 | info_parser.add_argument("-s", "--separator", type=str, default=" ", help="The separator to use when outputting the fields when -q is set") 410 | info_parser.add_argument("-q", "--quiet", action="store_true", help="Output only the field values without any formatting. Useful when parsing the output.") 411 | info_parser.add_argument("-f", "--fields", metavar="field", nargs="+", type=str, default=defaultInfoFields, help="Limit the output to only these fields. The default fields to show can be set with the config command.") 412 | info_parser.add_argument("-a", "--all-fields", action="store_true", help="Output every field on the machine.") 413 | info_parser.add_argument("-l", "--list-format", action="store_true", help="Display the machine info in the same format as the default list command.") 414 | info_parser.set_defaults(func=do_info, command="info") 415 | 416 | spawn_parser = command_parsers.add_parser("spawn", help="[VIP Only] spawn a machine") 417 | spawn_parser.add_argument("box", metavar="BOX", type=str, help="The name of the box to spawn. This will fail if you have another box currently spawned. Terminate any spawned boxes and wait until it actually shuts down before running this.") 418 | spawn_parser.set_defaults(func=do_spawn, command="spawn") 419 | 420 | own_parser = command_parsers.add_parser("own", help="submit a flag to own a box") 421 | own_parser.add_argument("box", metavar="BOX", type=str, help="The name of the box you want to own.") 422 | own_parser.add_argument("-f", "--flag", type=str, required=True, help="The flag you want to submit to own the box. user/root is automatically determined by the server based on what flag you submit.") 423 | own_parser.add_argument("-d", "--difficulty", required=True, type=int, metavar="[1-10]", choices=range(1,11), help="The rating of how difficult you thought it was from 1-10.") 424 | own_parser.set_defaults(func=do_own, command="own") 425 | 426 | terminate_parser = command_parsers.add_parser("terminate", help="[VIP Only] terminate a box") 427 | terminate_parser.add_argument("box", metavar="BOX", type=str, help="The name of the box to terminate. Termination may take up to a few minutes to take effect. Until then you will not be able to spawn any new boxes.") 428 | terminate_parser.set_defaults(func=do_terminate, command="terminate") 429 | 430 | reset_parser = command_parsers.add_parser("reset", help="[Free Only] reset a box") 431 | reset_parser.add_argument("box", metavar="BOX", type=str, help="The name of the box to reset. Resetting may take a few minutes to take effect and may be cancelled by another user. ") 432 | reset_parser.set_defaults(func=do_reset, command="reset") 433 | 434 | arena_parser = command_parsers.add_parser("arena", help="Control the arena box.") 435 | arena_commands = arena_parser.add_subparsers(help="arena help") 436 | 437 | arena_spawn_parser = arena_commands.add_parser("spawn", help="spawn the arena box. this returns the new arena IP on success.") 438 | arena_spawn_parser.set_defaults(func=do_arena_spawn, command="spawn") 439 | arena_terminate_parser = arena_commands.add_parser("terminate", help="terminate the arena box.") 440 | arena_terminate_parser.set_defaults(func=do_arena_terminate, command="terminate") 441 | arena_reset_parser = arena_commands.add_parser("reset", help="reset the arena box. this returns the new arena ip on success.") 442 | arena_reset_parser.set_defaults(func=do_arena_reset, command="reset") 443 | 444 | arena_own_parser = arena_commands.add_parser("own", help="submit a flag for the arena box.") 445 | arena_own_parser.add_argument("-f", "--flag", type=str, required=True, help="The flag you want to submit to own the box. user/root is automatically determined by the server based on what flag you submit.") 446 | arena_own_parser.add_argument("-d", "--difficulty", required=True, type=int, metavar="[1-10]", choices=range(1,11), help="The rating of how difficult you thought it was from 1-10.") 447 | arena_own_parser.set_defaults(func=do_arena_own, command="own") 448 | 449 | arena_view_parser = arena_commands.add_parser("view", help="view the arena box details. NOTE: This command is relatively slow, so keep that in mind when using this tool in speed-runs.") 450 | arena_view_parser.set_defaults(func=do_arena_view, command="view") 451 | 452 | arena_region_parser = arena_commands.add_parser("region", help="set the region to use for the arena. [us,eu]. NOTE: Switching, or setting region for first time, requires re-downloading your connection pack from the arena page on the HTB site.") 453 | arena_region_parser.add_argument("box", metavar="REGION", choices=['eu', 'us'], type=str, help="The region to use for the arena.") 454 | arena_region_parser.set_defaults(func=do_arena_region, command="region") 455 | 456 | try: 457 | args = parser.parse_args() 458 | except: 459 | subcommand=None 460 | if len(sys.argv) >= 2: 461 | subcommand = sys.argv[1] 462 | show_help(parser, subcommand) 463 | sys.exit(1) 464 | return args, parser 465 | 466 | def main(): 467 | load_config() 468 | args, parser = parse_args() 469 | if args.command is None: 470 | parser.print_help() 471 | sys.exit(1) 472 | if args.command != "config" and (not apiKey or not lab): 473 | print("You must configure your apiKey before interacting with the HTB API.\n\nTry running 'htb config --apiKey=[your apiKey here]'") 474 | sys.exit(1) 475 | init_api(apiKey) 476 | if args.command not in ["config", "arena"]: 477 | load_machines() 478 | args.func(args) 479 | 480 | 481 | if __name__ == "__main__": 482 | main() 483 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.21.0 2 | tabulate==0.8.5 3 | argparse==1.4.0 4 | htb==1.1.0 5 | termcolor==1.1.0 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Always prefer setuptools over distutils 2 | from setuptools import setup, find_packages 3 | # To use a consistent encoding 4 | from codecs import open 5 | from os import path 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | # Get the long description from the README file 10 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 11 | long_description = f.read() 12 | 13 | setup( 14 | name='htbcli', 15 | version='1.1.7', 16 | 17 | description='Hack the Box CLI', 18 | long_description=long_description, 19 | 20 | url='https://github.com/zachhanson94/htbcli', 21 | 22 | author='Zach Hanson', 23 | author_email='zachhanson94@gmail.com', 24 | 25 | license='ISC', 26 | 27 | classifiers=[ 28 | 'Development Status :: 3 - Alpha', 29 | 'License :: OSI Approved :: ISC License (ISCL)', 30 | 'Programming Language :: Python :: 3.7', 31 | ], 32 | 33 | entry_points = { 34 | 'console_scripts': ['htb=htbcli.__main__:main'] 35 | }, 36 | 37 | keywords='hackthebox', 38 | 39 | packages=find_packages(), 40 | 41 | install_requires=['requests', 'argparse', 'tabulate', 'htb'], 42 | ) 43 | --------------------------------------------------------------------------------