Source code for hackthebox.challenge

"""
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