"""
Examples:
Starting a challenge and submitting the flag::
challenge = client.get_challenge(100)
instance = challenge.start()
r = remote(instance.ip, instance.port)
# Do the challenge.....
instance.stop()
challenge.submit(flag, difficulty=50)
"""
from __future__ import annotations
import os
import time
from datetime import datetime
from typing import List, Optional, cast, TYPE_CHECKING
import dateutil.parser
from . import htb
from .constants import DOWNLOAD_COOLDOWN
from .errors import IncorrectFlagException, IncorrectArgumentException, NoDockerException, \
NoDownloadException, RateLimitException
if TYPE_CHECKING:
from .htb import HTBClient
from .user import User
[docs]class Challenge(htb.HTBObject):
""" The class representing Hack The Box challenges
Attributes:
name (str): The name of the challenge
retired: Whether the challenge is retired
difficulty: The official difficulty of the challenge
avg_difficulty: The average user-given difficulty
points: The points awarded on completion
difficulty_ratings: A dict of difficulty ratings given
solves: The number of solves a challenge has
likes: The number of likes a challenge has
dislikes: The number of dislikes a challenge has
release_date: The date the challenge was released
solved: Whether the active user has completed the challenge
is_liked: Whether the active user has liked the challenge
is_disliked: Whether the active user has disliked the challenge
description: The challenge description
category: The name of the category
has_download: Whether the challenge has a download available
has_docker: Whether the challenge has a remote instance available
"""
name: str
retired: bool
difficulty: str
avg_difficulty: int
points: int
difficulty_ratings = None
solves: int
likes: int
dislikes: int
release_date: datetime
solved: bool
is_liked: bool
is_disliked: bool
recommended: bool
# noinspection PyUnresolvedReferences
_authors: Optional[List["User"]] = None
_author_ids: List[int]
_detailed_attributes = ('description', 'category', 'has_download', 'has_docker', 'instance')
description: str
category: str
has_download: bool
has_docker: bool
instance: Optional[DockerInstance]
[docs] def submit(self, flag: str, difficulty: int):
""" Submits a flag for a Challenge
Args:
flag: The flag for the Challenge
difficulty: A rating between 10 and 100 of the Challenge difficulty.
Must be a multiple of 10.
"""
if difficulty < 10 or difficulty > 100 or difficulty % 10 != 0:
raise IncorrectArgumentException(reason="Difficulty must be a multiple of 10, between 10 and 100")
submission = cast(dict, self._client.do_request("challenge/own", json_data={
"flag": flag,
"challenge_id": self.id,
"difficulty": difficulty
}))
if submission['message'] == "Incorrect flag":
raise IncorrectFlagException
return True
[docs] def start(self) -> DockerInstance:
"""
Requests the challenge be started
Returns:
The DockerInstance that was started
"""
if not self.has_docker:
raise NoDockerException
instance = cast(dict, self._client.do_request("challenge/start", json_data={"challenge_id": self.id}))
# TODO: Handle failure to start
self.instance = DockerInstance(instance['ip'], instance['port'], self.id, self._client, instance['id'])
return self.instance
[docs] def download(self, path=None) -> str:
"""
Args:
path: The name of the zipfile to download to. If none is provided, it is saved to the current directory.
Returns: The path of the file
"""
if not self.has_download:
raise NoDownloadException
if self._client.challenge_cooldown > time.time():
raise RateLimitException("Challenge download ratelimit exceeded - please do not remove this")
if path is None:
path = os.path.join(os.getcwd(), f"{self.name}.zip")
data = cast(bytes, self._client.do_request(f"challenge/download/{self.id}", download=True))
self._client.challenge_cooldown = int(time.time()) + DOWNLOAD_COOLDOWN
with open(path, 'wb') as f:
f.write(data)
return path
# noinspection PyUnresolvedReferences
@property
def authors(self) -> List["User"]:
"""Fetch the author(s) of the Challenge
Returns: List of Users
"""
if not self._authors:
self._authors = []
for uid in self._author_ids:
self._authors.append(self._client.get_user(uid))
return self._authors
def __repr__(self):
return f"<Challenge '{self.name}'>"
# noinspection PyUnresolvedReferences
def __init__(self, data: dict, client: "HTBClient", summary: bool = False):
"""Initialise a `Challenge` using API data"""
self._client = client
self._detailed_func = client.get_challenge # type: ignore
self.id = data['id']
self.name = data['name']
self.retired = bool(data['retired'])
self.points = int(data['points'])
self.difficulty = data["difficulty"]
self.difficulty_ratings = data['difficulty_chart']
self.solves = data['solves']
self.solved = data['authUserSolve']
self.likes = data['likes']
self.dislikes = data['dislikes']
self.release_date = dateutil.parser.parse(data['release_date'])
if not summary:
self.description = data['description']
self.category = data['category_name']
self._author_ids = [data['creator_id']]
if data['creator2_id']:
self._author_ids.append(data['creator2_id'])
self.has_download = data['download']
self.has_docker = data['docker']
if data['docker_ip']:
self.instance = DockerInstance(data['docker_ip'], data['docker_port'], self.id, self._client)
else:
self.instance = None
else:
self._is_summary = True
class DockerInstance:
"""Representation of an active Docker container instance of a Challenge
Attributes:
container_id: The ID of the container
port: The port the container is listening on
ip: The IP the instance can be reached at
chall_id: The connected challenge
client: The passed-through API client
"""
id: str
port: int
ip: str
chall_id: int
client: htb.HTBClient
def __init__(self, ip: str, port: int, chall_id: int, client: htb.HTBClient, container_id: str = None):
self.client = client
self.id = container_id or ""
self.port = port
self.ip = ip
self.chall_id = chall_id
def stop(self):
"""Request the instance be stopped. Zeroes out all properties"""
self.client.do_request("challenge/stop", json_data={"challenge_id": self.chall_id})
# TODO: Handle failures to stop