{
	"id": "efa74c2e-c578-4504-b799-093fef66df3f",
	"created_at": "2026-04-06T00:08:00.734137Z",
	"updated_at": "2026-04-10T13:11:35.79444Z",
	"deleted_at": null,
	"sha1_hash": "5fe62b6b63181a31375412748fcec152de599257",
	"title": "How to Identify XenoRAT C2 Servers",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 291254,
	"plain_text": "How to Identify XenoRAT C2 Servers\r\nBy Axel Mahr\r\nPublished: 2024-12-13 · Archived: 2026-04-05 18:38:44 UTC\r\nXenoRAT is a relatively new RAT, that is open-source and used by low-sophisticated cyber criminals but also\r\nAPT groups. In this post, we will look at how we can detect XenoRAT C2 servers through scanning. But, before\r\nwe come to that, we need to take a quick look at XenoRAT’s C2 protocol.\r\nXenoRAT’s C2 Packet Formats\r\nIn general, implant and C2 server communicate over raw TCP while using dedicated packet formats which are set\r\nas TCP payload. The packet formats are different, depending on whether the internal server state\r\ndoProtocolUpgrade is true or false. When a new client connects, this state is always false. Then, the following\r\nthe packet format is used:\r\nTCP Payload Format if doProtocolUprgade is false\r\nThe first four bytes give the length of the following data. The next byte indicates, whether the payload is\r\ncompressed or not. The rest of the packet constitutes the actual message, which is AES-encrypted. The AES key is\r\nthe same for both communication directions and is derived by calculating the SHA256 hash of the password\r\nspecified on the server side by the RAT operator. The default password is “1234” and the IV is always zero. The\r\nencrypted message is additionally compressed using LZNT1 and preceded by four bytes indicating the\r\nuncompressed length, if compression reduces the length of the encrypted message. However, this is very unlikely,\r\nas the encrypted message has already high entropy.\r\nThe packet format, that is used when doProtocolUprgade is true, is the following:\r\nTCP Payload Format if doProtocolUprgade is true\r\nIt looks pretty similar. However, notice that the fifth byte is always 0x3 and that the first byte of the plaintext to\r\nbe encrypted is always 0 or 1 and precedes the actual message.\r\nDetection\r\nhttps://axmahr.github.io/posts/xenorat-detection/\r\nPage 1 of 5\n\nIn order to detect XenoRAT C2 servers proactively through scanning, we can exploit, how the handshake between\r\nan implant and C2 server works:\r\nThe XenoRAT Handshake between a regular implant and the C2 server\r\nRight after the TCP handshake, the C2 server sends a packet using the first format ( doProtocolUprgade is false).\r\n0x71 indicates the length of the following data, which is 0x70 bytes occupied by the AES-encrypted 100 random\r\nbytes and the preceding single zero indicating that the payload is not compressed. In the usual XenoRAT\r\nhandshake, the implant answers with the respective random bytes while using the second packet format, which is\r\nin general the only format a regular implant uses. By setting the fifth byte to 0x3, the C2 server changes\r\ndoProtocolUpgrade to true internally. By that, also the C2 server continues to solely use this format for future\r\npackets to send. When the implant has sent the 100 random bytes back correctly, the server sends a further packet\r\nconsisting of the message moom825 .\r\nThe Low-Hanging Fruit\r\nIn order to exploit this handshake procedure to detect XenoRAT C2 servers, first of all, the characteristic byte\r\npatterns of the initial server message can be used as an indicator. This is, 71 00 00 00 00 [0x70 bytes of\r\nencrypted data] (due to the mentioned fact that compression doesn’t occur here). This would translate to a\r\nCensys query of services.banner_hex:\"7100000000*\" . And looking at the results of that query, we already find\r\nnumerous XenoRAT C2 servers, that Censys doesn’t label as such. However, this query is not very specific, false\r\npositives are possible.\r\nGoing one step further\r\nhttps://axmahr.github.io/posts/xenorat-detection/\r\nPage 2 of 5\n\nTechnically, having received the initial server packet, a scanner could not proceed with the handshake, because,\r\nsince the AES key is not known, it wouldn’t be able to construct a valid packet of the second packet format (with\r\nthe additional single zero in the plaintext), which a real implant would answer with. And, if the client’s response is\r\nnot valid, the server closes the connection.\r\nHowever, weirdly and luckily for us, due to an unused code path inside the XenoRAT C2 server, it also possible to\r\nanswer with the respective 100 random bytes using the first packet format. A real implant wouldn’t do this, since it\r\nexclusively uses the second packet format but it works nevertheless. Because the same AES key is used for both\r\ncommunication directions, a client actually doesn’t need to know the AES key for sending a valid response, that\r\ncontains the 100 random bytes. Hence, any client can respond validly by just sending the received initial\r\nserver packet back as is. After the C2 server has received this packet, which constitutes a valid response, it sends\r\nthe further packet containing the encrypted string “moom825”. And, as the fifth byte of the client packet is not\r\n0x3, doProtocolUpgrade is not changed to true and the C2 server sticks with the first packet format.\r\nTechnique to identify XenoRAT C2 server\r\nBy that, we didn’t only achieve that the XenoRAT C2 server sends a second characteristic packet to the client,\r\ngiving us a considerably more accurate indicator. With that second server packet, we can additionally determine,\r\nwhether the default password “1234” ist used, since the plaintext (“moom825”) is known. When this default\r\npassword is used, the respective packet bytes are 11 00 00 00 00 dc db 8d 8b 56 4b f3 37 ae 1a e8 c3 b7 2e\r\n8c 8c and one is able to further impersonate a real implant, since the AES key is then known and the client thus\r\ncan construct valid messages.\r\nWith the following code snippet, we can realize the described detection method:\r\nhttps://axmahr.github.io/posts/xenorat-detection/\r\nPage 3 of 5\n\npython\r\nimport socket\r\nimport binascii\r\nimport sys\r\nimport re\r\nregex = re.compile(rb\"\\x71\\x00\\x00\\x00\\x00[\\x00-\\xff]{112}\", re.DOTALL)\r\nsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r\nsock.connect((sys.argv[1], int(sys.argv[2])))\r\nserver_packet_1 = sock.recv(1280)\r\nprint(\"initial packet received from server:\", binascii.hexlify(server_packet_1))\r\nif re.search(regex, server_packet_1):\r\nprint(\"packet matches initial xeno rat server message!\")\r\nprint(\"\\nsending the same packet back...\")\r\nsock.send(server_packet_1)\r\nserver_packet_2_hex = binascii.hexlify(sock.recv(1280))\r\nprint(\"response:\", server_packet_2_hex)\r\nif (server_packet_2_hex == b\"1100000000dcdb8d8b564bf337ae1ae8c3b72e8c8c\"):\r\nprint(\"xeno rat server uses the default password '1234'!\")\r\nprint(\"=\u003e the AES key is 03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4\")\r\nprint(\"and the IV is 00000000000000000000000000000000\")\r\nExample\r\nQuerying Censys with services.banner_hex:\"7100000000*\" , I got an interesting match for the domain\r\ndaddeln[.]eu at port 4444, which is the default port for XenoRAT. Probing the server with the code snippet\r\nfrom above yields the following output:\r\npython3 xenorat_connector.py daddeln.eu 4444\r\ninitial packet received from server: b'71000000007589af3a2b3415e2b73f1d88443ba63f7fdb521e99662dc83f22c445f1a5aa0\r\npacket matches initial xeno rat server message!\r\nsending the same packet back...\r\nresponse: b'1100000000dcdb8d8b564bf337ae1ae8c3b72e8c8c'\r\nxeno rat server uses the default password '1234'!\r\n=\u003e the AES key is 03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4\r\nand the IV is 00000000000000000000000000000000\r\nIn this example, the RAT operator didn’t even bother to change the default password. Hence, we would be able to\r\nimpersonate an implant.\r\nhttps://axmahr.github.io/posts/xenorat-detection/\r\nPage 4 of 5\n\nSource: https://axmahr.github.io/posts/xenorat-detection/\r\nhttps://axmahr.github.io/posts/xenorat-detection/\r\nPage 5 of 5",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://axmahr.github.io/posts/xenorat-detection/"
	],
	"report_names": [
		"xenorat-detection"
	],
	"threat_actors": [],
	"ts_created_at": 1775434080,
	"ts_updated_at": 1775826695,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/5fe62b6b63181a31375412748fcec152de599257.pdf",
		"text": "https://archive.orkl.eu/5fe62b6b63181a31375412748fcec152de599257.txt",
		"img": "https://archive.orkl.eu/5fe62b6b63181a31375412748fcec152de599257.jpg"
	}
}