|
@@ -81,9 +81,22 @@ namespace Api {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+ protected function findUser($username) {
|
|
|
+ $url = "https://www.hackthebox.eu/api/user/id?api_token=" . self::API_TOKEN;
|
|
|
+ $postData = array("username" => $username);
|
|
|
+ $response = $this->postRequest($url, $postData);
|
|
|
+ if ($response === false) {
|
|
|
+ return $this->createError("Error fetching htb user: $this->lastError");
|
|
|
+ } else if (empty($response)) {
|
|
|
+ return $this->createError("HTB User not found.");
|
|
|
+ } else {
|
|
|
+ return $response;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
protected function getUser(Condition $condition) {
|
|
|
$sql = $this->user->getSQL();
|
|
|
- $res = $sql->select("HackTheBoxUser.uid as userId", "HackTheBoxUser.name as userName",
|
|
|
+ $res = $sql->select("HackTheBoxUser.uid as userId", "HackTheBoxUser.name as userName", "HackTheBoxUser.reported as reported",
|
|
|
"HackTheBoxUser.confirmed", "HackTheBoxUser.token", "HackTheBoxMachineOwn.type as ownType",
|
|
|
"HackTheBoxMachine.uid as machineId", "HackTheBoxMachine.name as machineName")
|
|
|
->from("HackTheBoxUser")
|
|
@@ -103,6 +116,7 @@ namespace Api {
|
|
|
"confirmed" => $sql->parseBool($row["confirmed"]),
|
|
|
"uid" => $row["userId"],
|
|
|
"token" => $row["token"],
|
|
|
+ "reported" => $sql->parseBool($row["reported"]),
|
|
|
"machineOwns" => array()
|
|
|
);
|
|
|
|
|
@@ -156,6 +170,122 @@ namespace Api {
|
|
|
$this->success = ($res !== false);
|
|
|
return $this->success;
|
|
|
}
|
|
|
+
|
|
|
+ protected function getProfileActivity($userId) {
|
|
|
+ $url = "https://www.hackthebox.eu/api/v4/user/profile/activity/$userId";
|
|
|
+ $response = $this->getRequest($url);
|
|
|
+ if (!$this->success) {
|
|
|
+ return $this->createError("Unable to fetch owned machines: $this->lastError");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($response["profile"]) && isset($response["profile"]["activity"])) {
|
|
|
+ return $response["profile"]["activity"];
|
|
|
+ } else {
|
|
|
+ return array();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function checkCheating($activities, $verbose=false) {
|
|
|
+ $lastFlagSubmission = null;
|
|
|
+ $numSuspicious = 0;
|
|
|
+ $total = 0;
|
|
|
+
|
|
|
+ $chains = array();
|
|
|
+ $currentChain = 0;
|
|
|
+
|
|
|
+ foreach ($activities as $activity) {
|
|
|
+ if (in_array($activity["object_type"], array("challenge", "fortress", "machine", "endgame")) && $activity["points"] > 0) {
|
|
|
+ try {
|
|
|
+ $total++;
|
|
|
+ $timestamp = new \DateTime($activity["date"]);
|
|
|
+ if ($lastFlagSubmission === null) {
|
|
|
+ $lastFlagSubmission = $timestamp;
|
|
|
+ } else {
|
|
|
+ $diff = abs($timestamp->getTimestamp() - $lastFlagSubmission->getTimestamp());
|
|
|
+ $diffs[] = $diff;
|
|
|
+ if ($diff <= 3 * 60) { // within 3 minutes
|
|
|
+ $currentChain++;
|
|
|
+ $numSuspicious++;
|
|
|
+ } else {
|
|
|
+ if ($currentChain > 2) {
|
|
|
+ $chains[] = $currentChain;
|
|
|
+ }
|
|
|
+ $currentChain = 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ $lastFlagSubmission = $timestamp;
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ // var_dump($e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($currentChain > 2) {
|
|
|
+ $chains[] = $currentChain;
|
|
|
+ }
|
|
|
+
|
|
|
+ $chainChance = empty($chains) ? 0.0 : (1 - exp(1 - 0.5 * max($chains)));
|
|
|
+ $submissionChance = ($total === 0) ? 0.0 : ($numSuspicious / $total);
|
|
|
+
|
|
|
+ if ($verbose) {
|
|
|
+ $this->result["chains"] = $chains;
|
|
|
+ $this->result["submissions_total"] = $total;
|
|
|
+ $this->result["submissions_suspicious"] = $numSuspicious;
|
|
|
+ $this->result["submissions_suspicious_percentage"] = $submissionChance;
|
|
|
+ $this->result["continuous_submissions"] = count($chains);
|
|
|
+
|
|
|
+ if (count($chains) > 0) {
|
|
|
+ $this->result["continuous_submissions_min"] = min($chains);
|
|
|
+ $this->result["continuous_submissions_max"] = max($chains);
|
|
|
+ $this->result["continuous_submissions_avg"] = array_sum($chains)/count($chains);
|
|
|
+ $this->result["continuous_submissions_chance"] = $chainChance;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $chance = 0.0;
|
|
|
+ if ($total > 0) {
|
|
|
+ $chance = 0.75 * $chainChance + 0.25 * $submissionChance;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $chance;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function findConversation($username) {
|
|
|
+ $url = "https://www.hackthebox.eu/api/conversations/list?api_token=" . self::API_TOKEN;
|
|
|
+ $response = $this->postRequest($url, array());
|
|
|
+ if (!$this->success || empty($response)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($response as $conversation) {
|
|
|
+ $conversationId = $conversation["id"];
|
|
|
+ $usernames = $conversation["usernames"];
|
|
|
+ if (count($usernames) === 1 && strcasecmp($usernames[0], $username) === 0) {
|
|
|
+ return $conversationId;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function sendMessage($username, $message) {
|
|
|
+ $conversationId = $this->findConversation($username);
|
|
|
+ if (!$this->success) {
|
|
|
+ return $this->createError($this->lastError);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($conversationId === null) {
|
|
|
+ $url = "https://www.hackthebox.eu/api/conversations/new/?api_token=" . self::API_TOKEN;
|
|
|
+ $postData = array("recipients[]" => $username, "message" => $message);
|
|
|
+ } else {
|
|
|
+ $url = "https://www.hackthebox.eu/api/conversations/send/$conversationId/?api_token=" . self::API_TOKEN;
|
|
|
+ $postData = array("id" => $conversationId, "message" => $message);
|
|
|
+ }
|
|
|
+
|
|
|
+ $response = $this->postRequest($url, $postData);
|
|
|
+ return ($response !== false);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -319,24 +449,6 @@ namespace Api\Htb {
|
|
|
return $this->success;
|
|
|
}
|
|
|
|
|
|
- protected function findConversation() {
|
|
|
- $url = "https://www.hackthebox.eu/api/conversations/list?api_token=" . self::API_TOKEN;
|
|
|
- $response = $this->postRequest($url, array());
|
|
|
- if (!$this->success || empty($response)) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- foreach ($response as $conversation) {
|
|
|
- $conversationId = $conversation["id"];
|
|
|
- $usernames = $conversation["usernames"];
|
|
|
- if (count($usernames) === 1 && strcasecmp($usernames[0], $this->userName) === 0) {
|
|
|
- return $conversationId;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
private function sendToken() {
|
|
|
|
|
|
$message = "Hello $this->userName, " .
|
|
@@ -348,21 +460,8 @@ namespace Api\Htb {
|
|
|
$errorMessage = "Your user token was generated but we were not " .
|
|
|
"able to send the token via htb. Please contact the site administration. Reason: ";
|
|
|
|
|
|
- $conversationId = $this->findConversation();
|
|
|
- if (!$this->success) {
|
|
|
- return $this->createError($errorMessage . $this->lastError);
|
|
|
- }
|
|
|
-
|
|
|
- if ($conversationId === null) {
|
|
|
- $url = "https://www.hackthebox.eu/api/conversations/new/?api_token=" . self::API_TOKEN;
|
|
|
- $postData = array("recipients[]" => $this->userName, "message" => $message);
|
|
|
- } else {
|
|
|
- $url = "https://www.hackthebox.eu/api/conversations/send/$conversationId/?api_token=" . self::API_TOKEN;
|
|
|
- $postData = array("id" => $conversationId, "message" => $message);
|
|
|
- }
|
|
|
|
|
|
- $response = $this->postRequest($url, $postData);
|
|
|
- if ($response === false) {
|
|
|
+ if (!$this->sendMessage($this->userName, $message)) {
|
|
|
$this->lastError = $errorMessage . $this->lastError;
|
|
|
return false;
|
|
|
}
|
|
@@ -389,13 +488,9 @@ namespace Api\Htb {
|
|
|
return $this->createError("Invalid username");
|
|
|
}
|
|
|
|
|
|
- $url = "https://www.hackthebox.eu/api/user/id?api_token=" . self::API_TOKEN;
|
|
|
- $postData = array("username" => $username);
|
|
|
- $response = $this->postRequest($url, $postData);
|
|
|
- if ($response === false) {
|
|
|
- return $this->createError("Error fetching htb user: $this->lastError");
|
|
|
- } else if (empty($response)) {
|
|
|
- return $this->createError("HTB User not found.");
|
|
|
+ $response = $this->findUser($username);
|
|
|
+ if (!$this->success) {
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
$this->userName = $response["username"];
|
|
@@ -479,28 +574,34 @@ namespace Api\Htb {
|
|
|
return $this->success;
|
|
|
}
|
|
|
|
|
|
- private function fetchOwnedMachines($userId) {
|
|
|
+ private function fetchOwnedMachines($userId, $sendReport) {
|
|
|
|
|
|
- $url = "https://www.hackthebox.eu/api/v4/user/profile/activity/$userId";
|
|
|
- $response = $this->getRequest($url);
|
|
|
+ $profileActivity = $this->getProfileActivity($userId);
|
|
|
if (!$this->success) {
|
|
|
return $this->createError("Unable to fetch owned machines: $this->lastError");
|
|
|
}
|
|
|
|
|
|
$machineOwns = array();
|
|
|
- if (isset($response["profile"]) && isset($response["profile"]["activity"])) {
|
|
|
- foreach ($response["profile"]["activity"] as $activity) {
|
|
|
- if (strcmp($activity["object_type"], "machine") === 0) {
|
|
|
- $machineId = $activity["id"];
|
|
|
- $ownType = $activity["type"];
|
|
|
+ foreach ($profileActivity as $activity) {
|
|
|
+ if (strcmp($activity["object_type"], "machine") === 0) {
|
|
|
+ $machineId = $activity["id"];
|
|
|
+ $ownType = $activity["type"];
|
|
|
|
|
|
- if (!isset($machineOwns[$machineId])) {
|
|
|
- $machineOwns[$machineId] = array("user" => false, "root" => false);
|
|
|
- }
|
|
|
-
|
|
|
- if (strcmp($ownType, "user") === 0) $machineOwns[$machineId]["user"] = true;
|
|
|
- if (strcmp($ownType, "root") === 0) $machineOwns[$machineId]["root"] = true;
|
|
|
+ if (!isset($machineOwns[$machineId])) {
|
|
|
+ $machineOwns[$machineId] = array("user" => false, "root" => false);
|
|
|
}
|
|
|
+
|
|
|
+ if (strcmp($ownType, "user") === 0) $machineOwns[$machineId]["user"] = true;
|
|
|
+ if (strcmp($ownType, "root") === 0) $machineOwns[$machineId]["root"] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($sendReport) {
|
|
|
+ $chance = $this->checkCheating($profileActivity);
|
|
|
+ if ($chance >= 0.75) {
|
|
|
+ $minatoUserId = 8308;
|
|
|
+ $chance = intval(round($chance * 100));
|
|
|
+ $this->reportUser($userId, $chance);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -566,36 +667,49 @@ namespace Api\Htb {
|
|
|
$userId = $htbUser["uid"];
|
|
|
$this->result["user"] = array("name" => $htbUser["name"], "uid" => $userId);
|
|
|
|
|
|
+ // fetch from cache, if he already rooted both user and root
|
|
|
if (isset($htbUser["machineOwns"][$machineId])) {
|
|
|
$userAccess = $htbUser["machineOwns"][$machineId]["user"];
|
|
|
$rootAccess = $htbUser["machineOwns"][$machineId]["root"];
|
|
|
- $this->result["unlock"] = array("user" => $userAccess, "root" => $rootAccess);
|
|
|
- $this->logAccess($userId, $machineId, $userAccess, $rootAccess);
|
|
|
- if (!$userAccess && !$rootAccess) {
|
|
|
- $this->createError("You did not own the machine yet.");
|
|
|
- }
|
|
|
- } else {
|
|
|
- $ownedMachines = $this->fetchOwnedMachines($userId);
|
|
|
- if (!$this->success) {
|
|
|
- $this->logAccess($userId, $machineId, false, false);
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- if (!isset($ownedMachines[$machineId])) {
|
|
|
- $this->createError("You did not own the machine yet.");
|
|
|
- $this->logAccess($userId, $machineId, false, false);
|
|
|
- } else {
|
|
|
- $userAccess = $ownedMachines[$machineId]["user"];
|
|
|
- $rootAccess = $ownedMachines[$machineId]["root"];
|
|
|
+ if ($userAccess && $rootAccess) {
|
|
|
$this->result["unlock"] = array("user" => $userAccess, "root" => $rootAccess);
|
|
|
$this->logAccess($userId, $machineId, $userAccess, $rootAccess);
|
|
|
+ return true;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- $this->updateOwnedMachines($userId, $ownedMachines);
|
|
|
+ $ownedMachines = $this->fetchOwnedMachines($userId, !$htbUser["reported"]);
|
|
|
+ if (!$this->success) {
|
|
|
+ $this->logAccess($userId, $machineId, false, false);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isset($ownedMachines[$machineId])) {
|
|
|
+ $this->createError("You did not own the machine yet.");
|
|
|
+ $this->logAccess($userId, $machineId, false, false);
|
|
|
+ } else {
|
|
|
+ $userAccess = $ownedMachines[$machineId]["user"];
|
|
|
+ $rootAccess = $ownedMachines[$machineId]["root"];
|
|
|
+ $this->result["unlock"] = array("user" => $userAccess, "root" => $rootAccess);
|
|
|
+ $this->logAccess($userId, $machineId, $userAccess, $rootAccess);
|
|
|
}
|
|
|
|
|
|
+ $this->updateOwnedMachines($userId, $ownedMachines);
|
|
|
return $this->success;
|
|
|
}
|
|
|
+
|
|
|
+ private function reportUser($userId, int $chance) {
|
|
|
+ $this->sendMessage("MinatoTW", "This player is likely cheating: https://www.hackthebox.eu/home/users/profile/$userId (Chance: $chance%)");
|
|
|
+ if ($this->success) {
|
|
|
+ $this->user->getSQL()
|
|
|
+ ->update("HackTheBoxUser")
|
|
|
+ ->set("reported", true)
|
|
|
+ ->where(new Compare("uid", $userId))
|
|
|
+ ->execute();
|
|
|
+ } else {
|
|
|
+ $this->clearError();
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
class Stats extends HtbAPI {
|
|
@@ -626,7 +740,7 @@ namespace Api\Htb {
|
|
|
|
|
|
private function getUsers() {
|
|
|
$sql = $this->user->getSQL();
|
|
|
- $res = $sql->select("uid", "name", "confirmed", "token", "registered")
|
|
|
+ $res = $sql->select("uid", "name", "confirmed", "token", "registered", "reported")
|
|
|
->from("HackTheBoxUser")
|
|
|
->orderBy("registered")
|
|
|
->descending()
|
|
@@ -638,6 +752,7 @@ namespace Api\Htb {
|
|
|
if ($this->success) {
|
|
|
foreach ($res as $i => $row) {
|
|
|
$res[$i]["confirmed"] = $sql->parseBool($row["confirmed"]);
|
|
|
+ $res[$i]["reported"] = $sql->parseBool($row["reported"]);
|
|
|
}
|
|
|
return $res;
|
|
|
}
|
|
@@ -705,4 +820,41 @@ namespace Api\Htb {
|
|
|
return $this->success;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ class CheckProfile extends HtbAPI {
|
|
|
+
|
|
|
+ public function __construct(User $user, bool $externalCall = false) {
|
|
|
+ parent::__construct($user, $externalCall, array(
|
|
|
+ "username" => new StringType("username", 32)
|
|
|
+ ));
|
|
|
+ $this->csrfTokenRequired = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ public function execute($values = array()) {
|
|
|
+ if (!parent::execute($values)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ $username = $this->getParam("username");
|
|
|
+ $response = $this->findUser($username);
|
|
|
+ if (!$this->success) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ $userId = $response["id"];
|
|
|
+ $activity = $this->getProfileActivity($userId);
|
|
|
+ if (!$this->success) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ $chance = $this->checkCheating($activity, true);
|
|
|
+ $this->result["cheatingChance"] = $chance;
|
|
|
+
|
|
|
+ if ($chance >= 0.75) {
|
|
|
+ $this->lastError = "Player is likely cheating.";
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|