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