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;
       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) {
     protected function getUser(Condition $condition) {
       $sql = $this->user->getSQL();
       $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",
         "HackTheBoxUser.confirmed", "HackTheBoxUser.token", "HackTheBoxMachineOwn.type as ownType",
         "HackTheBoxMachine.uid as machineId", "HackTheBoxMachine.name as machineName")
         "HackTheBoxMachine.uid as machineId", "HackTheBoxMachine.name as machineName")
         ->from("HackTheBoxUser")
         ->from("HackTheBoxUser")
@@ -103,6 +116,7 @@ namespace Api {
             "confirmed" => $sql->parseBool($row["confirmed"]),
             "confirmed" => $sql->parseBool($row["confirmed"]),
             "uid" => $row["userId"],
             "uid" => $row["userId"],
             "token" => $row["token"],
             "token" => $row["token"],
+            "reported" => $sql->parseBool($row["reported"]),
             "machineOwns" => array()
             "machineOwns" => array()
           );
           );
 
 
@@ -156,6 +170,122 @@ namespace Api {
       $this->success = ($res !== false);
       $this->success = ($res !== false);
       return $this->success;
       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;
       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() {
     private function sendToken() {
 
 
       $message = "Hello $this->userName, " .
       $message = "Hello $this->userName, " .
@@ -348,21 +460,8 @@ namespace Api\Htb {
       $errorMessage = "Your user token was generated but we were not " .
       $errorMessage = "Your user token was generated but we were not " .
         "able to send the token via htb. Please contact the site administration. Reason: ";
         "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;
         $this->lastError = $errorMessage . $this->lastError;
         return false;
         return false;
       }
       }
@@ -389,13 +488,9 @@ namespace Api\Htb {
         return $this->createError("Invalid username");
         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"];
       $this->userName = $response["username"];
@@ -479,28 +574,34 @@ namespace Api\Htb {
       return $this->success;
       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) {
       if (!$this->success) {
         return $this->createError("Unable to fetch owned machines: $this->lastError");
         return $this->createError("Unable to fetch owned machines: $this->lastError");
       }
       }
 
 
       $machineOwns = array();
       $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"];
       $userId = $htbUser["uid"];
       $this->result["user"] = array("name" => $htbUser["name"], "uid" => $userId);
       $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])) {
       if (isset($htbUser["machineOwns"][$machineId])) {
         $userAccess = $htbUser["machineOwns"][$machineId]["user"];
         $userAccess = $htbUser["machineOwns"][$machineId]["user"];
         $rootAccess = $htbUser["machineOwns"][$machineId]["root"];
         $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->result["unlock"] = array("user" => $userAccess, "root" => $rootAccess);
           $this->logAccess($userId, $machineId, $userAccess, $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;
       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 {
   class Stats extends HtbAPI {
@@ -626,7 +740,7 @@ namespace Api\Htb {
 
 
     private function getUsers() {
     private function getUsers() {
       $sql = $this->user->getSQL();
       $sql = $this->user->getSQL();
-      $res = $sql->select("uid", "name", "confirmed", "token", "registered")
+      $res = $sql->select("uid", "name", "confirmed", "token", "registered", "reported")
         ->from("HackTheBoxUser")
         ->from("HackTheBoxUser")
         ->orderBy("registered")
         ->orderBy("registered")
         ->descending()
         ->descending()
@@ -638,6 +752,7 @@ namespace Api\Htb {
       if ($this->success) {
       if ($this->success) {
         foreach ($res as $i => $row) {
         foreach ($res as $i => $row) {
           $res[$i]["confirmed"] = $sql->parseBool($row["confirmed"]);
           $res[$i]["confirmed"] = $sql->parseBool($row["confirmed"]);
+          $res[$i]["reported"] = $sql->parseBool($row["reported"]);
         }
         }
         return $res;
         return $res;
       }
       }
@@ -705,4 +820,41 @@ namespace Api\Htb {
       return $this->success;
       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;
+    }
+  }
 }
 }