| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
|
| 7 |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 8 |
source "$SCRIPT_DIR/../config.sh" |
| 9 |
|
| 10 |
OUT_FILE="$OUT/27_database_storage.txt" |
| 11 |
echo "=== 27. Database & Storage Security ===" | tee "$OUT_FILE" |
| 12 |
echo "Target: $TARGET API: $API_TARGET" | tee -a "$OUT_FILE" |
| 13 |
echo "" | tee -a "$OUT_FILE" |
| 14 |
|
| 15 |
HOST=$(echo "$TARGET" | sed 's|https\?://||' | cut -d/ -f1) |
| 16 |
IP=$(dig +short "$HOST" | grep -oP '^\d+\.\d+\.\d+\.\d+$' | head -1) |
| 17 |
[ -z "$IP" ] && IP="$HOST" |
| 18 |
echo "Resolved $HOST โ $IP" | tee -a "$OUT_FILE" |
| 19 |
echo "" | tee -a "$OUT_FILE" |
| 20 |
|
| 21 |
do_port() { |
| 22 |
local label="$1"; local port="$2" |
| 23 |
if timeout 3 bash -c "echo >/dev/tcp/$IP/$port" 2>/dev/null; then |
| 24 |
printf '[OPEN] %s (port %s) โ EXPOSED\n' "$label" "$port" | tee -a "$OUT_FILE" |
| 25 |
else |
| 26 |
printf '[CLOSED] %s (port %s)\n' "$label" "$port" | tee -a "$OUT_FILE" |
| 27 |
fi |
| 28 |
} |
| 29 |
|
| 30 |
do_test() { |
| 31 |
local label="$1"; shift |
| 32 |
local code |
| 33 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "$@") |
| 34 |
printf '[%s] %s\n' "$code" "$label" | tee -a "$OUT_FILE" |
| 35 |
} |
| 36 |
|
| 37 |
do_body() { |
| 38 |
local label="$1"; local pattern="$2"; shift 2 |
| 39 |
local body code |
| 40 |
body=$(curl -sk --max-time 8 "$@") |
| 41 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "$@") |
| 42 |
printf '[%s] %s\n' "$code" "$label" | tee -a "$OUT_FILE" |
| 43 |
if [ -n "$pattern" ] && echo "$body" | grep -qiE "$pattern"; then |
| 44 |
echo " [WARN] Pattern matched: $(echo "$body" | grep -ioE "$pattern" | head -2 | tr '\n' ' ')" | tee -a "$OUT_FILE" |
| 45 |
fi |
| 46 |
} |
| 47 |
|
| 48 |
| 49 |
echo "--- Database Port Exposure ---" | tee -a "$OUT_FILE" |
| 50 |
do_port "PostgreSQL" 5432 |
| 51 |
do_port "MySQL / MariaDB" 3306 |
| 52 |
do_port "MongoDB" 27017 |
| 53 |
do_port "Redis" 6379 |
| 54 |
do_port "Elasticsearch" 9200 |
| 55 |
do_port "Elasticsearch (9300)" 9300 |
| 56 |
do_port "CouchDB" 5984 |
| 57 |
do_port "Cassandra" 9042 |
| 58 |
do_port "InfluxDB" 8086 |
| 59 |
do_port "Neo4j" 7474 |
| 60 |
do_port "MSSQL" 1433 |
| 61 |
do_port "Oracle" 1521 |
| 62 |
do_port "ClickHouse HTTP" 8123 |
| 63 |
do_port "ClickHouse native" 9000 |
| 64 |
do_port "Memcached" 11211 |
| 65 |
echo "" | tee -a "$OUT_FILE" |
| 66 |
|
| 67 |
| 68 |
echo "--- Database Admin Interfaces ---" | tee -a "$OUT_FILE" |
| 69 |
do_test "phpMyAdmin /phpmyadmin/" -A "$BROWSER_UA" "$TARGET/phpmyadmin/" |
| 70 |
do_test "phpMyAdmin /pma/" -A "$BROWSER_UA" "$TARGET/pma/" |
| 71 |
do_test "pgAdmin /pgadmin/" -A "$BROWSER_UA" "$TARGET/pgadmin/" |
| 72 |
do_test "pgAdmin /pgadmin4/" -A "$BROWSER_UA" "$TARGET/pgadmin4/" |
| 73 |
do_test "Adminer /adminer.php" -A "$BROWSER_UA" "$TARGET/adminer.php" |
| 74 |
do_test "Adminer /adminer/" -A "$BROWSER_UA" "$TARGET/adminer/" |
| 75 |
do_test "MongoDB Express :8081" "http://$IP:8081/" |
| 76 |
do_test "Redis Commander :8081" "http://$IP:8081/redis" |
| 77 |
do_test "RedisInsight :8001" "http://$IP:8001/" |
| 78 |
do_test "Elasticsearch Kibana :5601" "http://$IP:5601/" |
| 79 |
do_test "InfluxDB UI :8086" "http://$IP:8086/signin" |
| 80 |
do_test "Neo4j Browser :7474" "http://$IP:7474/browser/" |
| 81 |
do_test "CouchDB /_utils Fauxton" "http://$IP:5984/_utils/" |
| 82 |
do_test "ClickHouse play UI" "http://$IP:8123/play" |
| 83 |
echo "" | tee -a "$OUT_FILE" |
| 84 |
|
| 85 |
| 86 |
echo "--- Unauthenticated DB HTTP API Access ---" | tee -a "$OUT_FILE" |
| 87 |
do_body "Elasticsearch cluster health" '"cluster_name"\|"status"' "http://$IP:9200/_cluster/health" |
| 88 |
do_body "Elasticsearch indices" '"health"\|"index"' "http://$IP:9200/_cat/indices?v" |
| 89 |
do_body "Elasticsearch mappings" '"properties"' "http://$IP:9200/_mapping" |
| 90 |
do_body "CouchDB server info" '"couchdb"' "http://$IP:5984/" |
| 91 |
do_body "CouchDB all databases" '\[' "http://$IP:5984/_all_dbs" |
| 92 |
do_body "InfluxDB databases" '"results"' "http://$IP:8086/query?q=SHOW+DATABASES" |
| 93 |
do_body "MongoDB (wire proto probe)" '.' -X POST -H 'Content-Type: application/octet-stream' \ |
| 94 |
-d $'\x3d\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xd4\x07\x00\x00\x00\x00\x00\x00' \ |
| 95 |
"http://$IP:27017/" |
| 96 |
echo "" | tee -a "$OUT_FILE" |
| 97 |
|
| 98 |
| 99 |
echo "--- NoSQL Injection ---" | tee -a "$OUT_FILE" |
| 100 |
| 101 |
NOSQL_PAYLOADS=( |
| 102 |
'{"username":{"$gt":""},"password":{"$gt":""}}' |
| 103 |
'{"username":{"$ne":"invalid"},"password":{"$ne":"invalid"}}' |
| 104 |
'{"username":{"$regex":".*"},"password":{"$regex":".*"}}' |
| 105 |
'{"username":{"$where":"1==1"}}' |
| 106 |
'{"$or":[{"username":"admin"},{"username":"root"}],"password":{"$ne":""}}' |
| 107 |
) |
| 108 |
for PAYLOAD in "${NOSQL_PAYLOADS[@]}"; do |
| 109 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 \ |
| 110 |
-A "$BROWSER_UA" -X POST -H 'Content-Type: application/json' \ |
| 111 |
-d "$PAYLOAD" "$API_TARGET/encrypt") |
| 112 |
printf '[%s] NoSQL injection: %s\n' "$code" "${PAYLOAD:0:50}..." | tee -a "$OUT_FILE" |
| 113 |
done |
| 114 |
echo "" | tee -a "$OUT_FILE" |
| 115 |
|
| 116 |
| 117 |
echo "--- Database Backup & Dump Exposure ---" | tee -a "$OUT_FILE" |
| 118 |
DB_FILES=( |
| 119 |
"/backup.sql" |
| 120 |
"/dump.sql" |
| 121 |
"/db.sql" |
| 122 |
"/database.sql" |
| 123 |
"/${PROJECT_NAME}.sql" |
| 124 |
"/${PROJECT_NAME}_backup.sql" |
| 125 |
"/db_backup.sql" |
| 126 |
"/db_dump.sql" |
| 127 |
"/data.sql" |
| 128 |
"/schema.sql" |
| 129 |
"/backup.sql.gz" |
| 130 |
"/dump.sql.gz" |
| 131 |
"/db.sql.gz" |
| 132 |
"/backup.tar.gz" |
| 133 |
"/db_backup.tar.gz" |
| 134 |
"/backup/" |
| 135 |
"/backups/" |
| 136 |
"/db/" |
| 137 |
"/database/" |
| 138 |
"/data/" |
| 139 |
"/exports/" |
| 140 |
"/mysqldump.sql" |
| 141 |
"/pgdump.sql" |
| 142 |
"/mongo_dump/" |
| 143 |
"/.pgpass" |
| 144 |
"/.my.cnf" |
| 145 |
"/db.sqlite" |
| 146 |
"/database.sqlite" |
| 147 |
"/db.sqlite3" |
| 148 |
"/app.db" |
| 149 |
) |
| 150 |
FOUND_ANY=0 |
| 151 |
for DB_FILE in "${DB_FILES[@]}"; do |
| 152 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 -A "$BROWSER_UA" "$TARGET$DB_FILE") |
| 153 |
if [ "$code" = "200" ]; then |
| 154 |
printf ' [200] %s โ EXPOSED\n' "$DB_FILE" | tee -a "$OUT_FILE" |
| 155 |
FOUND_ANY=1 |
| 156 |
fi |
| 157 |
done |
| 158 |
[ "$FOUND_ANY" -eq 0 ] && echo " [OK] No database backup files exposed" | tee -a "$OUT_FILE" |
| 159 |
echo "" | tee -a "$OUT_FILE" |
| 160 |
|
| 161 |
| 162 |
echo "--- Object Storage (S3 / GCS / Azure Blob) ---" | tee -a "$OUT_FILE" |
| 163 |
HOST_BASE=$(echo "$HOST" | sed 's/^www\.//' | sed 's/\./\-/g') |
| 164 |
S3_VARIANTS=( |
| 165 |
"${PROJECT_NAME}" |
| 166 |
"${PROJECT_NAME}-backup" |
| 167 |
"${PROJECT_NAME}-data" |
| 168 |
"${PROJECT_NAME}-uploads" |
| 169 |
"${PROJECT_NAME}-assets" |
| 170 |
"${PROJECT_NAME}-db" |
| 171 |
"${PROJECT_NAME}-logs" |
| 172 |
"${PROJECT_NAME}-static" |
| 173 |
) |
| 174 |
for BUCKET in "${S3_VARIANTS[@]}"; do |
| 175 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "https://$BUCKET.s3.amazonaws.com/") |
| 176 |
body=$(curl -sk --max-time 8 "https://$BUCKET.s3.amazonaws.com/") |
| 177 |
if echo "$body" | grep -q 'ListBucketResult'; then |
| 178 |
printf ' [OPEN] S3 bucket publicly listable: %s\n' "$BUCKET" | tee -a "$OUT_FILE" |
| 179 |
elif [ "$code" = "403" ]; then |
| 180 |
printf ' [403] S3 bucket exists (private): %s\n' "$BUCKET" | tee -a "$OUT_FILE" |
| 181 |
else |
| 182 |
printf ' [%s] S3: %s\n' "$code" "$BUCKET" | tee -a "$OUT_FILE" |
| 183 |
fi |
| 184 |
done |
| 185 |
echo "" | tee -a "$OUT_FILE" |
| 186 |
|
| 187 |
| 188 |
echo "--- Encryption-at-Rest / Key Rotation Signals ---" | tee -a "$OUT_FILE" |
| 189 |
| 190 |
API_STATUS=$(curl -sk -A "$BROWSER_UA" --max-time 8 "$API_TARGET/status" 2>/dev/null) |
| 191 |
for FIELD in encryption_at_rest key_rotation key_version last_rotated kms_key_id vault_transit; do |
| 192 |
if echo "$API_STATUS" | grep -qi "\"$FIELD\""; then |
| 193 |
echo " Key metadata field '$FIELD' in API response" | tee -a "$OUT_FILE" |
| 194 |
fi |
| 195 |
done |
| 196 |
echo " [OK] Encryption-at-rest field scan complete" | tee -a "$OUT_FILE" |
| 197 |
|
| 198 |
| 199 |
TIMING_LEAK=$(curl -sk -A "$BROWSER_UA" --max-time 8 \ |
| 200 |
-X POST -H 'Content-Type: application/json' \ |
| 201 |
-d '{"data":"'"$(python3 -c "print('x'*50000)")"'"}' \ |
| 202 |
"$API_TARGET/encrypt" 2>/dev/null) |
| 203 |
if echo "$TIMING_LEAK" | grep -qiE '(pgsql|postgres|mysql|mongodb|redis|connection refused|SQLSTATE)'; then |
| 204 |
echo " [WARN] DB connection info in oversized payload error" | tee -a "$OUT_FILE" |
| 205 |
else |
| 206 |
echo " [OK] No DB connection details in error responses" | tee -a "$OUT_FILE" |
| 207 |
fi |
| 208 |
echo "" | tee -a "$OUT_FILE" |
| 209 |
|
| 210 |
| 211 |
echo "--- SQL Timing Inference (non-API endpoints) ---" | tee -a "$OUT_FILE" |
| 212 |
| 213 |
do_sleep_test() { |
| 214 |
local label="$1"; local url="$2"; local payload="$3" |
| 215 |
local t_start t_end elapsed |
| 216 |
t_start=$(python3 -c "import time; print(int(time.time()*1000))") |
| 217 |
curl -sk -o /dev/null --max-time 12 -A "$BROWSER_UA" \ |
| 218 |
-X POST -H 'Content-Type: application/x-www-form-urlencoded' \ |
| 219 |
-d "$payload" "$url" 2>/dev/null |
| 220 |
t_end=$(python3 -c "import time; print(int(time.time()*1000))") |
| 221 |
elapsed=$(python3 -c "print($t_end - $t_start)") |
| 222 |
if [ "$elapsed" -gt 4500 ]; then |
| 223 |
printf ' [TIMING-VULN] %s โ %dms โ SLEEP SUCCEEDED\n' "$label" "$elapsed" | tee -a "$OUT_FILE" |
| 224 |
else |
| 225 |
printf ' [OK] %s โ %dms\n' "$label" "$elapsed" | tee -a "$OUT_FILE" |
| 226 |
fi |
| 227 |
} |
| 228 |
do_sleep_test "Admin login SLEEP injection" "$TARGET/admin/" \ |
| 229 |
"username=admin'%3BSELECT+SLEEP(5)--&password=test" |
| 230 |
do_sleep_test "Contact form SLEEP injection" "$TARGET/contact/" \ |
| 231 |
"email=test'%3BSELECT+SLEEP(5)--&message=test" |
| 232 |
do_sleep_test "Search SLEEP injection" "$TARGET/" \ |
| 233 |
"q=test'%3BSELECT+SLEEP(5)--" |
| 234 |
echo "" | tee -a "$OUT_FILE" |
| 235 |
|
| 236 |
| 237 |
echo "--- Audit Logging Completeness Check ---" | tee -a "$OUT_FILE" |
| 238 |
| 239 |
W() { printf '%s' "$1" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=''))"; } |
| 240 |
AUDIT_URL_1="$TARGET/?q=$(W "1' OR 1=1--")" |
| 241 |
AUDIT_URL_2="$TARGET/?q=$(W "<script>alert(1)</script>")" |
| 242 |
AUDIT_URL_3="$TARGET/$(W "../../../../etc/passwd")" |
| 243 |
AUDIT_URL_4="$TARGET/admin/?attempt=brute" |
| 244 |
AUDIT_CHECKS=( |
| 245 |
"SQL in query param:$AUDIT_URL_1" |
| 246 |
"XSS in param:$AUDIT_URL_2" |
| 247 |
"Path traversal:$AUDIT_URL_3" |
| 248 |
"Admin brute force:$AUDIT_URL_4" |
| 249 |
) |
| 250 |
for CHECK in "${AUDIT_CHECKS[@]}"; do |
| 251 |
LABEL="${CHECK%%:*}" |
| 252 |
URL="${CHECK#*:}" |
| 253 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 -A "$BROWSER_UA" "$URL") |
| 254 |
printf ' [%s] %s โ proxy returned %s (should be 403 for attack)\n' \ |
| 255 |
"$( [ "$code" = "403" ] && echo "OK" || echo "WARN" )" "$LABEL" "$code" | tee -a "$OUT_FILE" |
| 256 |
done |
| 257 |
echo "" | tee -a "$OUT_FILE" |
| 258 |
|
| 259 |
| 260 |
echo "--- Row-Level Security / Data Isolation ---" | tee -a "$OUT_FILE" |
| 261 |
| 262 |
do_test "IDOR: user ID 0 (admin?)" \ |
| 263 |
-A "$BROWSER_UA" "$API_TARGET/users/0" |
| 264 |
do_test "IDOR: user ID 1" \ |
| 265 |
-A "$BROWSER_UA" "$API_TARGET/users/1" |
| 266 |
do_test "IDOR: negative ID" \ |
| 267 |
-A "$BROWSER_UA" "$API_TARGET/users/-1" |
| 268 |
do_test "IDOR: UUID zero" \ |
| 269 |
-A "$BROWSER_UA" "$API_TARGET/users/00000000-0000-0000-0000-000000000000" |
| 270 |
do_test "IDOR: large integer overflow" \ |
| 271 |
-A "$BROWSER_UA" "$API_TARGET/users/9999999999" |
| 272 |
do_test "Keys endpoint without auth" \ |
| 273 |
-A "$BROWSER_UA" "$API_TARGET/keys" |
| 274 |
do_test "Vault endpoint without auth" \ |
| 275 |
-A "$BROWSER_UA" "$API_TARGET/vault" |
| 276 |
do_test "Analytics endpoint without auth" \ |
| 277 |
-A "$BROWSER_UA" "$API_TARGET/analytics" |
| 278 |
echo "" | tee -a "$OUT_FILE" |
| 279 |
|
| 280 |
echo "=== 27. Database & Storage Security COMPLETE ===" | tee -a "$OUT_FILE" |
| 281 |
|