Do you suspect your site is compromised or hacked? This can be a stressful situation and time is of the essence to ensure your data and visitors are safe. Using the tips below can help you mitigate further damage. When you're first notified of or detect a potential breach, your goals are to:
- Preserve data for forensic investigation
- Check users and roles
- Restore the site
- Analyze for security improvements
The focus of your actions may shift due to both the nature of the attack and how you were made aware of the attack, however these guidelines can be used for a variety of security incidents.
Preserve data for forensic investigation
- Take a database backup, which will preserve site content, configuration, and user information.
- Backup your site files.
Check users and roles
Confirm the user roles for your site have the correct permissions:
- Are there advanced permissions set to a role which shouldn't have them, including the "anonymous user" role?
- Is there a new role which you didn't create? Is there one user with that role? If so, it may have the email address and password controlled by the attacker.
- For your roles with expected advanced permissions (admin users, for example), are there any users which shouldn't be included?
Ensure that any roles in the database match what are expected and that none have been added using MySQL. Learn how to access MySQL.
SELECT name FROM role;
Example output:
mysql> SELECT name FROM role;
+--------------------+
| name |
+--------------------+
| administrator |
| anonymous user |
| authenticated user |
+--------------------+
3 rows in set (0.01 sec)
Check that the users with administrative permissions match those expected:
SELECT u.uid, r.name, u.name FROM users_roles ur JOIN users u ON ur.uid=u.uid JOIN role r ON ur.rid=r.rid WHERE ur.rid IN (SELECT rid FROM role_permission WHERE permission IN ('administer users', 'administer permissions', 'administer content', 'administer content types'));
Example output:
mysql> SELECT u.uid, r.name, u.name FROM users_roles ur JOIN users u ON ur.uid=u.uid JOIN role r ON ur.rid=r.rid WHERE ur.rid IN (SELECT rid FROM role_permission WHERE permission IN ('administer users', 'administer permissions', 'administer content', 'administer content types'));
+-----+---------------+--------------+
| uid | name | name |
+-----+---------------+--------------+
| 1 | administrator | site.admin |
+-----+---------------+--------------+
1 row in set (0.08 sec)
Restore the site
Restore from a previous database backup
In most cases, this means rolling back the database to the last known un-compromised date. Acquia Cloud Enterprise and Acquia Cloud Professional customers have access to 3 daily backups, as well as on-demand backups. You can easily accomplish rolling back the database via the Cloud UI. On the Databases page click "View all backups", identify the backup needed, then click Restore.
If you have a local or externally hosted database backup you wish to restore, you can restore the backup via command line.
It's also advisable to copy the last three automated daily database backups into the "on-demand" backups folder to avoid losing the backups due to rotation. Copying the daily backups to the "on-demand" directory gives you the option to restore those via the Cloud UI at a later date, if needed. The "on-demand" backups folder and daily database backups are located at /mnt/gfs/[sitegroup].[environment]/backups.
Investigate or delete individual nodes as an alternative to rolling back the database
If you've verified only one or a few nodes are impacted and don't wish to roll back the database, you can find out more about the node and revision state via the following MySQL commands:
SELECT * from node where nid = [#####];
SELECT * from node_revision where nid=[#####];
Example output:
mysql> SELECT * from node where nid = 11;
+-----+------+------+----------+------------+-----+--------+------------+------------+---------+---------+--------+------+-----------+
| nid | vid | type | language | title | uid | status | created | changed | comment | promote | sticky | tnid | translate |
+-----+------+------+----------+------------+-----+--------+------------+------------+---------+---------+--------+------+-----------+
| 11 | 11 | page | und | Contact Us | 1 | 1 | 1500570693 | 1500570693 | 1 | 0 | 0 | 0 | 0 |
+-----+------+------+----------+------------+-----+--------+------------+------------+---------+---------+--------+------+-----------+
1 row in set (0.00 sec)
mysql> SELECT * from node_revision where nid = 11;
+-----+-----+-----+------------+-----+------------+--------+---------+---------+--------+
| nid | vid | uid | title | log | timestamp | status | comment | promote | sticky |
+-----+-----+-----+------------+-----+------------+--------+---------+---------+--------+
| 11 | 11 | 1 | Contact Us | | 1500570899 | 1 | 1 | 0 | 0 |
| 11 | 16 | 1 | Contact Us | | 1500570910 | 1 | 1 | 0 | 0 |
| 11 | 21 | 1 | Contact Us | | 1500571019 | 1 | 1 | 1 | 1 |
+-----+-----+-----+------------+-----+------------+--------+---------+---------+--------+
3 rows in set (0.00 sec)
If you know keywords or PHP backdoors that were added to one node, try the following to trace other impacted nodes:
select u.name, u.mail, nr.uid, frb.entity_id, FROM_UNIXTIME(nr.timestamp) from \ field_revision_body frb inner join node_revision nr on nr.nid = frb.entity_id inner join users u on u.uid= nr.uid where body_value \ like '%keyword1%' or '%keyword2%' order by nr.timestamp;
Example output:
mysql> select u.name, u.mail, nr.uid, frb.entity_id, FROM_UNIXTIME(nr.timestamp) from field_revision_body frb inner join node_revision nr on nr.nid = frb.entity_id inner join users u on u.uid= nr.uid where body_value like '%pharmaceutical%' or '%curseword%' order by nr.timestamp;
+--------------+-------------------------+-----+-----------+-----------------------------+
| name | mail | uid | entity_id | FROM_UNIXTIME(nr.timestamp) |
+--------------+-------------------------+-----+-----------+-----------------------------+
| malicious.user | maluser123@gmail.com | 1 | 1 | 2017-07-20 17:36:01 |
| fake.person | fakeperson1@gmail.com | 1 | 1 | 2017-07-20 17:36:01 |
| fake.person | fakeperson1@gmail.com | 1 | 1 | 2017-07-20 18:28:02 |
| fake.person | fakeperson1@gmail.com | 1 | 1 | 2017-07-20 18:28:02 |
+--------------+-------------------------+-----+-----------+-----------------------------+
4 rows in set, 1 warning (0.00 sec)
Analyze for security improvements
Prevention
Security improvements can include:
- Disable PHP filter
- Update old modules and core versions
- Require complex passwords via the Password Strength module
- Assign users the lowest level of permissions needed
- Implement TFA (Two-Factor Authentication)
Further security considerations and best practices can be found on the Drupal.org's Securing your site page.
Identify security gaps
The following modules can help identify security gaps in your code and configuration:
More Resources
How can I tell if my website is being attacked?
Securing your site
Google Reported Our Site as Hacked
Anything you can do, XSS can do better