Skip to content

Commit

Permalink
Update to use updated API
Browse files Browse the repository at this point in the history
  • Loading branch information
kulinacs committed Feb 2, 2019
1 parent c2c4164 commit 6465555
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 56 deletions.
15 changes: 15 additions & 0 deletions LICENSE
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.
1 change: 0 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ Hack the Box
============

A Python wrapper to interact with hackthebox.eu

271 changes: 221 additions & 50 deletions htb/__init__.py
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)))
9 changes: 4 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]',
Expand All @@ -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'],
)

0 comments on commit 6465555

Please sign in to comment.