Browse Source

Cheat Detection + Don't fetch Owns from cache, if either user or root was not owned yet.

Roman Hergenreder 3 years ago
parent
commit
d2abf13494
1 changed files with 226 additions and 74 deletions
  1. 226 74
      code/htb_api.php

+ 226 - 74
code/htb_api.php

@@ -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;
+    }
+  }
 }