-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
240 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
ISC License | ||
|
||
Copyright (c) 2019, Nicklaus McClendon <[email protected]> | ||
|
||
Permission to use, copy, modify, and/or distribute this software for any | ||
purpose with or without fee is hereby granted, provided that the above | ||
copyright notice and this permission notice appear in all copies. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | ||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | ||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | ||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | ||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | ||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,3 @@ Hack the Box | |
============ | ||
|
||
A Python wrapper to interact with hackthebox.eu | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,56 +1,227 @@ | ||
""" | ||
A wrapper around the Hack the Box API | ||
""" | ||
import requests | ||
from bs4 import BeautifulSoup | ||
|
||
class HTBAPIError(Exception): | ||
"""Raised when API fails""" | ||
pass | ||
|
||
class HTB: | ||
""" | ||
Hack the Box API Wrapper | ||
:attr api_key: API Key used for authenticated queries | ||
""" | ||
|
||
BASE_URL = 'https://www.hackthebox.eu/api' | ||
|
||
def __init__(self, email, password): | ||
self.session = requests.Session() | ||
self.__login(email, password) | ||
self.update_machines() | ||
|
||
def __login(self, email, password): | ||
'''Initializes a Hack the Box session''' | ||
login_page = self.session.get('https://www.hackthebox.eu/login').text | ||
login_parse = BeautifulSoup(login_page, 'html.parser') | ||
csrf_token = login_parse.find('meta', {'name': 'csrf-token'})['content'] | ||
post_data = {'_token': csrf_token, 'email': email, 'password': password} | ||
logged_in_page = self.session.post('https://www.hackthebox.eu/login', data=post_data).text | ||
if 'These credentials do not match our records.' in logged_in_page: | ||
raise Exception('Login Failed') | ||
|
||
def __update_machines(self, url): | ||
'''Update attr to a dict of machines''' | ||
page = self.session.get(url).text | ||
parse = BeautifulSoup(page, 'html.parser') | ||
table = parse.find('table') | ||
# Ignore the first entry, it's the header | ||
entries = table.findAll('tr')[1:] | ||
machines = {} | ||
for entry in entries: | ||
name, machine = HTB.__parse_machine_row(entry) | ||
machines[name] = machine | ||
return machines | ||
|
||
def update_machines(self): | ||
'''Update all machine lists''' | ||
self.update_active_machines() | ||
self.update_retired_machines() | ||
|
||
def update_active_machines(self): | ||
'''Update active_machines with a dict of the currently active machines''' | ||
self.active_machines = self.__update_machines('https://www.hackthebox.eu/home/machines/list') | ||
|
||
def update_retired_machines(self): | ||
'''Update retired_machines with a dict of currently retired machines''' | ||
self.retired_machines = self.__update_machines('https://www.hackthebox.eu/home/machines/retired') | ||
def __init__(self, api_key): | ||
self.api_key = api_key | ||
|
||
@staticmethod | ||
def __parse_machine_row(soup_tr): | ||
machine = {} | ||
soup_tds = soup_tr.findAll('td') | ||
name = soup_tds[0].find('a').getText() | ||
machine['id'] = soup_tds[0].find('a')['href'].split('/')[-1] | ||
machine['author'] = soup_tds[1].find('a').getText() | ||
machine['os'] = soup_tds[2].getText().strip() | ||
machine['ip'] = soup_tds[3].getText().strip() | ||
return name, machine | ||
def _validate_response(response): | ||
""" | ||
Validate the response from the API | ||
:params response: the response dict received from an API call | ||
:returns: the response dict if the call was successfull | ||
""" | ||
if response['success'] != '1': | ||
raise HTBAPIError("success != 1") | ||
return response | ||
|
||
@classmethod | ||
def _get(cls, path: str) -> dict: | ||
""" | ||
Helper function to get an API endpoint and validate the response | ||
:params cls: the HTB class | ||
:params path: the path to get including leading forward slash | ||
:returns: the response dict from the endpoint | ||
""" | ||
return HTB._validate_response(requests.get(cls.BASE_URL + path).json()) | ||
|
||
@classmethod | ||
def _post(cls, path: str, data: dict = None) -> dict: | ||
""" | ||
Helper function to get an API endpoint and validate the response | ||
:params cls: the HTB class | ||
:params path: the path to get including leading forward slash | ||
:returns: the response dict from the endpoint | ||
""" | ||
return HTB._validate_response(requests.post(cls.BASE_URL + path, data=data).json()) | ||
|
||
def _auth(self, path: str) -> str: | ||
""" | ||
Helper function to generate an authenticated URL | ||
:params self: HTB object in use | ||
:params path: string containing path to query | ||
:returns: path to authenticated query | ||
""" | ||
return "{}?api_token={}".format(path, self.api_key) | ||
|
||
@classmethod | ||
def global_stats(cls) -> dict: | ||
""" | ||
Returns current stats about Hack the Box | ||
:params cls: the HTB class | ||
:returns: global stats dict | ||
""" | ||
return cls._post('/stats/global') | ||
|
||
@classmethod | ||
def overview_stats(cls) -> dict: | ||
""" | ||
Returns overview stats about Hack the Box | ||
Doesn't include success key | ||
:params cls: the HTB class | ||
:returns: overview stats dict | ||
""" | ||
return requests.get(cls.BASE_URL + '/stats/overview').json() | ||
|
||
@classmethod | ||
def daily_owns(cls, count: int = 30) -> dict: | ||
""" | ||
Returns the number of owns and total number of users after the last COUNT days | ||
:params cls: the HTB class | ||
:params count: the number of days to get data from | ||
:returns: daily owns dict | ||
""" | ||
return cls._post('/stats/daily/owns/{}'.format(count)) | ||
|
||
def list_conversations(self) -> dict: | ||
""" | ||
Return the conversations dict | ||
Doesn't include success key | ||
:params self: HTB object in use | ||
:returns: conversations dict | ||
""" | ||
return requests.post(self.BASE_URL + self._auth('/conversations/list/')).json() | ||
|
||
def vpn_freeslots(self) -> dict: | ||
""" | ||
Return information about free slots on the VPN | ||
:params self: HTB object in use | ||
:returns: vpn_freeslots dict | ||
""" | ||
return self._post(self._auth('/vpnserver/freeslots/')) | ||
|
||
def vpn_statusall(self) -> dict: | ||
""" | ||
Return information about the status of every VPN | ||
:params self: HTB object in use | ||
:returns: vpn_statusall dict | ||
""" | ||
return self._get(self._auth('/vpnserver/status/all/')) | ||
|
||
def connection_status(self) -> dict: | ||
""" | ||
Return connection status information | ||
Success key seems to be behaving incorrectly | ||
:params self: HTB object in use | ||
:returns: connection_status dict | ||
""" | ||
return requests.post(self.BASE_URL + self._auth('/users/htb/connection/status/')).json() | ||
|
||
def fortress_connection_status(self) -> dict: | ||
""" | ||
Return fortress connection status information | ||
Success key seems to be behaving incorrectly | ||
:params self: HTB object in use | ||
:returns: fortress_connection_status dict | ||
""" | ||
return requests.post(self.BASE_URL + self._auth('/users/htb/fortress/connection/status/')).json() | ||
|
||
def switch_vpn(self, lab: str) -> dict: | ||
""" | ||
Switch the VPN your profile is connected to | ||
Success key doesn't exist | ||
:params self: HTB object in use | ||
:params lab: the lab to connect to, either free, usvip or euvip | ||
:returns: switch_vpn dict | ||
""" | ||
|
||
if lab not in ("free", "usvip", "euvip"): | ||
raise HTBAPIError("invalid lab") | ||
else: | ||
return requests.post(self.BASE_URL + self._auth('/labs/switch/{}/'.format(lab))).json() | ||
|
||
def get_machines(self) -> dict: | ||
""" | ||
Get all machines on the network | ||
:params self: HTB object in use | ||
:returns: machines dict | ||
""" | ||
return requests.get(self.BASE_URL + self._auth('/machines/get/all/')).json() | ||
|
||
def get_machine(self, mid: int) -> dict: | ||
""" | ||
Get a single machine on the network | ||
:params self: HTB object in use | ||
:params mid: Machine ID | ||
:returns: machine dict | ||
""" | ||
return requests.get(self.BASE_URL + self._auth('/machines/get/{}/'.format(mid))).json() | ||
|
||
def own_machine_user(self, mid: int, hsh: str, diff: int) -> bool: | ||
""" | ||
Own a user challenge on a machine | ||
:params self: HTB object in use | ||
:params mid: Machine ID | ||
:params hsh: User Hash | ||
:params diff: difficult (10-100) | ||
:returns: bool if successful | ||
""" | ||
try: | ||
self._post(self._auth('/machines/own/user/{}/'.format(mid)), | ||
{"hash": hsh, "diff": diff}) | ||
return True | ||
except HTBAPIError: | ||
return False | ||
|
||
def own_machine_root(self, mid: int, hsh: str, diff: int) -> bool: | ||
""" | ||
Own a root challenge on a machine | ||
:params self: HTB object in use | ||
:params mid: Machine ID | ||
:params hsh: Root Hash | ||
:params diff: difficult (10-100) | ||
:returns: bool if successful | ||
""" | ||
try: | ||
self._post(self._auth('/machines/own/root/{}/'.format(mid)), | ||
{"hash": hsh, "diff": diff}) | ||
return True | ||
except HTBAPIError: | ||
return False | ||
|
||
def reset_machine(self, mid: int) -> dict: | ||
""" | ||
Reset a machine on the network | ||
:params self: HTB object in use | ||
:params mid: Machine ID | ||
:returns: reset_machine dict | ||
""" | ||
return self._post(self._auth('/vm/reset/{}/'.format(mid))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,12 +12,12 @@ | |
|
||
setup( | ||
name='htb', | ||
version='0.3.0', | ||
version='0.4.0', | ||
|
||
description='Hack the Box API', | ||
long_description=long_description, | ||
|
||
url='https://gitlab.com/kulinacs/htb', | ||
url='https://github.com/kulinacs/htb', | ||
|
||
author='Nicklaus McClendon', | ||
author_email='[email protected]', | ||
|
@@ -27,13 +27,12 @@ | |
classifiers=[ | ||
'Development Status :: 3 - Alpha', | ||
'License :: OSI Approved :: ISC License (ISCL)', | ||
'Programming Language :: Python :: 3.6', | ||
'Programming Language :: Python :: 3.7', | ||
], | ||
|
||
keywords='hackthebox', | ||
|
||
packages=find_packages(), | ||
|
||
install_requires=['bs4', | ||
'requests'], | ||
install_requires=['requests'], | ||
) |