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
Open

Scan uploaded levels for ACE #1002

wants to merge 10 commits into from

Conversation

0x1DEA
Copy link

@0x1DEA 0x1DEA commented May 26, 2023

Added a LevelParser class which scans uploads for invalid things. I'd like someone with access to a PoC level to test on a local installation. And also sanity check my late-night code.

@0x1DEA
Copy link
Author

0x1DEA commented May 26, 2023

Using PHP's SPL gzread() function you can read the uncompressed data in a loop. Changing the parser, the pickup trigger scan can be done incrementally. However the code uses zlib_decode() I am still researching the specifics but I was unable to use the gz functions to decompress the level string. Only zlib_decode() seemed to work. This is only an issue for extremely large levels like Ocular Miracle which result in an out-of-memory error.

Copy link
Contributor

@Wyliemaster Wyliemaster left a comment

Choose a reason for hiding this comment

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

I believe this line is unnecessary: if ($obj[1] == 1817) {
Regardless of the object, the itemID property will still work with the array.

Moreover, this fix doesn't account for the level re-upload tool. As this fix is right now, it will only be "safe" assuming other servers don't have an ACE level

Copy link

@qimiko qimiko left a comment

Choose a reason for hiding this comment

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

tried to test this with a proof of concept level, but the code wasn't functional as-is. i did have to make some changes to make the base code work and found a few bugs with the implementation.
unfortunately, those other bugs meant the poc level passed through the check just fine...

in general, error handling needs to be significantly improved. i'm not sure how you tested the code but the current implementation definitely needs more work before it could be called secure.

incl/levels/uploadGJLevel.php Outdated Show resolved Hide resolved
incl/lib/LevelParser.php Outdated Show resolved Hide resolved
<?php

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

incl/lib/LevelParser.php Outdated Show resolved Hide resolved
incl/lib/LevelParser.php Outdated Show resolved Hide resolved
incl/lib/LevelParser.php Outdated Show resolved Hide resolved
$obj = self::map($objs[$i], ',');
// Check for pickup trigger exploit
if ($obj[1] == 1817) {
$id = intval($obj[80]);
Copy link

@qimiko qimiko May 27, 2023

Choose a reason for hiding this comment

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

i would recommend checking the color id property of color triggers as well, but it's probably not necessary. mostly just crashes

Copy link
Author

Choose a reason for hiding this comment

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

What index is color id?

*/
public function validate() {
// Ocular Miracle ain't happening
$max_level_size = 50 * 1024 * 1024; // 50 MB Max uncompressed size
Copy link

Choose a reason for hiding this comment

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

keep in mind that any level over this size could bypass the check entirely. it wouldn't be impossible to add padding in ways that inflate the size of the level while keeping it working on desktop platforms...

incl/lib/LevelParser.php Outdated Show resolved Hide resolved
@0x1DEA
Copy link
Author

0x1DEA commented May 27, 2023

Fixed various things. I think this also necessitates a server scan utility for existing levels.

@Cvolton
Copy link
Owner

Cvolton commented May 28, 2023

does this support levelstrings compressed with both gzdeflate (H4sIAAA header in b64) and gzcompress (eJ header in b64), as well as uncompressed levelstrings?

@0x1DEA
Copy link
Author

0x1DEA commented May 28, 2023

does this support levelstrings compressed with both gzdeflate (H4sIAAA header in b64) and gzcompress (eJ header in b64), as well as uncompressed levelstrings?

Yes, zlib_decode() works on both deflate and gzip data. When the level string is uncompressed, the b64 and decompression don't run.

@0x1DEA
Copy link
Author

0x1DEA commented May 28, 2023

Yes, zlib_decode() works on both deflate and gzip data. When the level string is uncompressed, the b64 and decompression don't run.

Correction, it works as described but I haven't added the magic for deflate so it doesn't run. I'll fix this when I get home.

Comment on lines +80 to +88
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;
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.

@MegaSa1nt
Copy link
Contributor

Hello! I'm creator of this core's fork, could i have your permission to add this PR to my repo? https://github.com/MegaSa1nt/GMDprivateServer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants