Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scan uploaded levels for ACE #1002

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions config/security.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
$enableCaptcha = false;
$hCaptchaKey = "";
$hCaptchaSecret = "";
$maxUncompressedLevelSize = 100 * 1024 * 1024; // Levels are scanned on upload. Use this to set an upper limit on levels size (bytes)
3 changes: 2 additions & 1 deletion incl/levels/uploadGJLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
include "../lib/connection.php";
require_once "../lib/GJPCheck.php";
require_once "../lib/exploitPatch.php";
require_once "../lib/LevelParser.php";
require_once "../lib/mainLib.php";
$mainLib = new mainLib();
require_once "../lib/mainLib.php";
Expand Down Expand Up @@ -85,7 +86,7 @@
VALUES (:levelName, :gameVersion, :binaryVersion, :userName, :levelDesc, :levelVersion, :levelLength, :audioTrack, :auto, :password, :original, :twoPlayer, :songID, :objects, :coins, :requestedStars, :extraString, :levelString, :levelInfo, :secret, :uploadDate, :userID, :id, :uploadDate, :unlisted, :hostname, :ldm, :wt, :wt2, :unlisted2, :settingsString)");


if($levelString != "" AND $levelName != ""){
if($levelString != "" AND $levelName != "" AND LevelParser::validate($levelString, $binaryVersion)){
$querye=$db->prepare("SELECT levelID FROM levels WHERE levelName = :levelName AND userID = :userID");
$querye->execute([':levelName' => $levelName, ':userID' => $userID]);
$levelID = $querye->fetchColumn();
Expand Down
97 changes: 97 additions & 0 deletions incl/lib/LevelParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/**
* In the future use this class for level validation. Data or other parameters.
*/
class LevelParser {
public $data = [];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh i don't see the point of making this a single member class. this data member is basically useless anyways (unless you were planning on extending the functionality of this class?). just make validate static and pass the level string as its param

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes in the future functionality could be expanded on validating other parts of the level request. However for now I have implemented your suggestions

const MAX_GROUPS_21 = 1099;
const MAX_GROUPS_22 = 9999;

/**
* Convert GD's weird "keyed" array strings to dictionary. Works with out-of-order keys.
*
* @param $list string
* @param $separator string
* @return array
*/
public static function map($list, $separator) {
$bits = explode($separator, $list);
$array = [];
for ($i = 1; $i < count($bits); $i += 2) {
$array[$bits[$i - 1]] = $bits[$i];
}
return $array;
}

/**
* Flatten dictionary to single delimited string
*
* @param $dict array|object
* @param $separator string
* @return string
*/
public static function unmap($dict, $separator) {
$string = '';
foreach ($dict as $key => $value) {
$string[] .= "${separator}${key}${separator}${value}";
}
return $string;
}

/**
* @param $string string
* @return false|string
*/
public static function base64_urlencode($string) {
return base64_encode(strtr($string, '+/', '-_'));
}

/**
* @param $string string
* @return false|string
*/
public static function base64_urldecode($string) {
return base64_decode(strtr($string, '-_', '+/'), true);
}

/**
* Validates level data for various things (currently only ACE exploit)
*
* @return bool
*/
public static function validate($levelString, $version) {
include '../../config/security.php';
try {
$data = $levelString;

// Decode (strict mode) if falsy then
$decoded = self::base64_urldecode($levelString);
if ($decoded !== false) $data = $decoded;

// Check for zlib magic
if(bin2hex(substr($data, 0, 3)) === '1f8b08') {
$data = zlib_decode($data, $maxUncompressedLevelSize);
if (!$data) {
return false;
}
}
// Check if result invalid (any character outside ascii range). Better heuristic for detecting junk?
if (preg_match('/[^\x20-\x7e]/', $data)) return false;
$objs = explode(';', $data);
// Skip level header
for ($i = 1; $i < count($objs); $i++) {
$obj = self::map($objs[$i], ',');
// Clamp groups based on version
if (array_key_exists(80, $obj)) {
$id = intval($obj[80]);
if ($id > ($version > 21 ? self::MAX_GROUPS_22 : self::MAX_GROUPS_21) || $id < 0) return false;
Comment on lines +81 to +89
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowing all ascii characters with preg_match('/[^\x20-\x7e]/', $data)) is a bad idea imo. the reason is the $id = intval($obj[80]);

In the client, this data is parsed using atoi which has different behaviour to intval

atoi does not support scientific notation whereas intval does

C
image
PHP
image

Would be a simple fix if it wasn't for the fact that a larger exponent results in the method erroring to 0
image

This problem allows you to use values in the client while also tricking this code into believing its safe

Copy link
Author

@0x1DEA 0x1DEA May 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's right.

It's also possible to insert other invalid values while conforming to object format. This PR is primarily concerned with ACE protection not validating all level data (though this is worth looking into more).

The check for non-ASCII characters is simply a heuristic for detecting malformed level data (such as attempting to upload arbitrary binary files or catching possible failure of the data decompression) and is not intended as a catch-all solution for level validation.

}
}
} catch (Exception $e) {
return false;
}

return true;
}
}
6 changes: 6 additions & 0 deletions tools/levelReupload.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function chkarray($source, $default = 0){
include "../incl/lib/connection.php";
require "../incl/lib/XORCipher.php";
require "../config/reuploadAcc.php";
require_once "../incl/lib/LevelParser.php";
require_once "../incl/lib/mainLib.php";
$gs = new mainLib();
if(!empty($_POST["levelid"])){
Expand Down Expand Up @@ -63,6 +64,11 @@ function chkarray($source, $default = 0){
//old levelString
$levelString = chkarray($levelarray["a4"]);
$gameVersion = chkarray($levelarray["a13"]);

if(!LevelParser::validate($levelString, $gameVersion)) {
exit("You're attempting to reupload a malformed level.");
}

if(substr($levelString,0,2) == 'eJ'){
$levelString = str_replace("_","/",$levelString);
$levelString = str_replace("-","+",$levelString);
Expand Down