Remote Code Execution for InterSystems Cache
If you manage multiple Caché instances across several servers, you may want to be able to execute arbitrary code from one Caché instance on another. System administrators and technical support specialists may also want to run arbitrary code on remote Caché servers. To address these needs, I have developed a special tool called RCE.
In this article, we will discuss what are the typical ways of solving similar tasks and how RCE (Remote Code Execution) can help.
What possible approaches are available?
Execute OS commands locally
Let's start with the simplest – executing OS commands locally from Caché. You can do it by executing the $zf command:
- $ZF(-1) will call a program or command of the operating system. A call is made from a new process, and the parent process waits until the child process is finished. Once the command is executed, $ZF(-1) returns the resulting code for the child process: 0 if executed successfully, 1 if executed with errors or -1 if the system was unable to create the child process.
It looks like this: set status = $ZF(-1,"mkdir ""test folder""")
- $ZF(-2) is similar command except that the parent process does not wait until the child process is finished. The command returns 0 if the process was created successfully or -1 if the system was unable to create the child process.
There are also methods of the %Net.Remote.Utility class (for InterSystems use only) which provides convenient wrappers for standard functions and displays the results of called processes in a more user-friendly form:
- RunCommandViaCPIPE executes a command using Command Pipe. Returns the created device and the output of the process.
- RunCommandViaZF executes a command using $ZF(-1). Writes the process output to a file and returns the process output
An alternative option is using the terminal command ! (or $ which is the same) that opens the standard shell of the operating system directly within the Caché terminal. Two working modes are available:
- One-line mode – the entire command is passed with ! and immediately executed by the shell's interpreter, while its output is sent to the current Caché device. The previous example looks like this:
SAMPLES>! mkdir ""test folder""
- Multi-line mode – the system executes ! first and then opens the shell where you can enter the necessary commands of the operating system. To close the shell, enter "quit" or "exit" depending on the shell you are working in:
SAMPLES>! C:\InterSystems\Cache\mgr\samples\> mkdir "test folder" C:\InterSystems\Cache\mgr\samples\> quit SAMPLES>
Remote execution of Caché ObjectScript code
Remote execution is possible via the %Net.RemoteConnection class (deprecated) which provides the following functionality:
- Open and modify stored objects;
- Execution of class methods and objects;
- Execution of queries.
Sample code demonstrating these capabilities
Set rc=##class(%Net.RemoteConnection).%New() Set Status=rc.Connect("127.0.0.1","SAMPLES",1972,"_system","SYS") break:'Status Set Status=rc.OpenObjectId("Sample.Person",1,.per) break:'Status Set Status=rc.GetProperty(per,"Name",.value) break:'Status Write value Set Status=rc.ResetArguments() break:'Status Set Status=rc.SetProperty(per,"Name","Jones,Tom "_$r(100),4) break:'Status Set Status=rc.ResetArguments() break:'Status Set Status=rc.GetProperty(per,"Name",.value) break:'Status Write value Set Status=rc.ResetArguments() break:'Status Set Status=rc.AddArgument(150,0) break:'Status // Addition 150+10 Set Status=rc.AddArgument(10,0) break:'Status // Addition 150+10 Set Status=rc.InvokeInstanceMethod(per, "Addition", .AdditionValue, 1) break:'Status Write AdditionValue Set Status=rc.ResetArguments() break:'Status Set Status=rc.InstantiateQuery(.rs,"Sample.Person","ByName")
This code performs several actions:
- Connects to the Caché server
- Opens the Sample.Person class instance with ID=1
- Obtains the attribute value
- Modifies the attribute value
- Sets arguments for the method
- Calls the method of the instance
- Executes the Sample.Person:ByName query
For operation on the server side %Net.RemoteConnection requires installed C++ binding.
It is also worth mentioning the ECP technology. The technology allows you to execute JOB processes remotely on a database server from your application server.
In general, combination of these two approaches can efficiently solve our task, but users still need a simple workflow of creating batch scripts, as these approaches can be rather difficult to understand and implement.
Thus, the project's goals were as follows:
- Execution of scripts on remote servers from Caché;
- No need for configuring remote servers (client side);
- Minimum configurations on local servers (server side);
- Transparent switch between commands of the operating system and Caché ObjectScript;
- Support both Windows-based and Linux-based clients.
Hierarchy of classes in the project looks like this:
The "Machine – OS – Instance" hierarchy stores the information required for accessing remote servers.
All commands are stored in the RCE.Script class which contains the sequential list of RCE.Command class objects serving as either OS commands or Caché ObjectScript code.
Examples of commands:
Set Сommand1 = ##class(RCE.Command).%New("cd 1", 0) Set Сommand2 = ##class(RCE.Command).%New("zn ""%SYS""", 1)
The first argument is the text of the command, the second argument is the execution level: 0 – OS, 1 – Cache.
Sample creation of a new script:
Set Script = ##class(RCE.Script).%New() Do Script.Insert(##class(RCE.Command).%New("touch 123", 0)) Do Script.Insert(##class(RCE.Command).%New("set ^test=1", 1)) Do Script.Insert(##class(RCE.Command).%New("set ^test(1)=2", 1)) Do Script.Insert(##class(RCE.Command).%New("touch 1234", 0)) Do Script.%Save()
In this example, the system will execute the 1st and 4th commands at the OS level and the 2nd and 3rd commands at the Caché level. Switching between these two level is absolutely transparent for users.
Presently, the following execution paths are supported:
|Linux||Linux, Windows (the SSH server must be installed on the client side)|
|Windows||Linux, Windows (you should install an SSH server on the client side or psexec on the server side)|
If ssh is supported on the client side, the server will generate the ssh command and execute it on the client side using the standard %Net.SSH.Session class.
If both server and client operate under Windows OS, the system will generate a BAT file and then execute it on the client side using psexec.
Adding a server
Load classes from the repository into any namespace. If your server operates under Windows and you want to manage other Windows-based servers, then assign the ^settings("exec") global a path to psexec. And that's all!
Adding a client
Adding a client is basically saving all the data required for authentication.
Example of the program code that creates a new hierarchy "PC – OS – Instance"
Set Machine = ##class(RCE.Machine).%New() Set Machine.IP = "IP or Host" Set OS = ##class(RCE.OS).%New("OС") // Linux or Windows Set OS.Username = "Operation system user" Set OS.Password = "User password" Set Instance = ##class(RCE.Instance).%New() Set Instance.Name = "Caché instance name" Set Instance.User = "Caché user name" // Unrequired on minimal security settings Set Instance.Pass = "Caché user password" // Unrequired on minimal security settings Set Instance.Dir = "Path to cterm" // Required only on Windows clients, who don't have cterm in PATH Set Instance.OS = OS Set OS.Machine = Machine Write $System.Status.GetErrorText(Machine.%Save())
And finally, let's execute our scripts. It's very simple – all we need to do is to run the ExecuteScript method from the RCE.Instance class into which the script object and the namespace (%SYS by default) are passed:
Set Status = Instance.ExecuteScript(Script, "USER")
RCE provides a convenient mechanism of remote code execution for InterSystems Caché. Since the tool uses only stored scripts, you need to write each of them only once and then execute them wherever you want on any number of clients.