9200 - Pentesting Elasticsearch
Basic information
Elasticsearch is a distributed, open source search and analytics engine for all types of data. It is known for its speed, scalability, and simple REST APIs. Built on Apache Lucene, it was first released in 2010 by Elasticsearch N.V. (now known as Elastic). Elasticsearch is the core component of the Elastic Stack, a collection of open source tools for data ingestion, enrichment, storage, analysis, and visualization. This stack, commonly referred to as the ELK Stack, also includes Logstash and Kibana, and now has lightweight data shipping agents called Beats.
What is an Elasticsearch index?
An Elasticsearch index is a collection of related documents stored as JSON. Each document consists of keys and their corresponding values (strings, numbers, booleans, dates, arrays, geolocations, etc.).
Elasticsearch uses an efficient data structure called an inverted index to facilitate fast full-text searches. This index lists every unique word in the documents and identifies the documents in which each word appears.
During the indexing process, Elasticsearch stores the documents and constructs the inverted index, allowing for near real-time searching. The index API is used to add or update JSON documents within a specific index.
Default port: 9200/tcp
Manual Enumeration
Banner
The protocol used to access Elasticsearch is HTTP. When you access it via HTTP you will find some interesting information: http://10.10.10.115:9200/

If you don’t see that response accessing / see the following section.
Authentication
Do not assume that a new deployment is unauthenticated. Current self-managed releases automatically generate TLS for the HTTP and transport layers, set a password for the elastic superuser, and create a short-lived Kibana enrollment token on first startup. Automatic setup may be skipped when Elasticsearch detects an existing configuration or cluster, while legacy and deliberately unsecured deployments may still expose the API without authentication.[2]
Probe both schemes and inspect the status, WWW-Authenticate, and X-Elastic-Product headers:
curl -skD- -o /dev/null https://ELASTICSEARCH-SERVER:9200/
curl -sD- -o /dev/null http://ELASTICSEARCH-SERVER:9200/
curl -sk https://ELASTICSEARCH-SERVER:9200/_security/_authenticate
However, if you send a request to / and receive a response like the following one:
{"error":{"root_cause":[{"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"security\" charset=\"UTF-8\""}}],"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"security\" charset=\"UTF-8\""}},"status":401}
This means that authentication is configured and valid credentials are needed. Elasticsearch REST authentication can use Basic, API key, or Bearer credentials. A 401 usually means no acceptable credential was supplied; a 403 normally means an authenticated (possibly anonymous) principal lacks the requested privilege, so keep testing lower-privileged endpoints.[1]
You can try to bruteforce Basic authentication where permitted. Built-in usernames include elastic (superuser), remote_monitoring_user, beats_system, logstash_system, kibana_system, and apm_system. Modern installations do not assign a shared default password; very old versions used changeme.
curl -sk -u 'user:password' https://IP:9200/
curl -sk -H "Authorization: ApiKey $API_KEY" https://IP:9200/_security/_authenticate
curl -sk -H "Authorization: Bearer $TOKEN" https://IP:9200/_security/_authenticate
Basic User Enumeration
#List all roles on the system:
curl -X GET "ELASTICSEARCH-SERVER:9200/_security/role"
#List all users on the system:
curl -X GET "ELASTICSEARCH-SERVER:9200/_security/user"
#Get more information about the rights of an user:
curl -X GET "ELASTICSEARCH-SERVER:9200/_security/user/<USERNAME>"
The user/role listing APIs commonly require administrative security privileges. With any valid credential, /_security/_authenticate is a better first request because it returns the effective username, roles, authentication realm/type, and API-key metadata. Any user can also test its own privileges without performing a destructive operation:[1]
curl -sk -u 'user:password' -H 'Content-Type: application/json' \
https://ELASTICSEARCH-SERVER:9200/_security/user/_has_privileges -d '{
"cluster": ["monitor", "manage", "manage_security", "read_pipeline", "manage_pipeline"],
"index": [{"names": ["*"], "privileges": ["view_index_metadata", "read", "write", "delete_index"]}]
}'
The response reports every requested privilege separately. Test concrete index patterns as well as *: roles may grant access only to a tenant prefix, data stream, or alias.
Elastic Info
Here are some endpoints that you can access via GET to obtain some information about elasticsearch:
| _cat | /_cluster | /_security |
|---|---|---|
| /_cat/segments | /_cluster/allocation/explain | /_security/user |
| /_cat/shards | /_cluster/settings | /_security/privilege |
| /_cat/repositories | /_cluster/health | /_security/role_mapping |
| /_cat/recovery | /_cluster/state | /_security/role |
| /_cat/plugins | /_cluster/stats | /_security/api_key |
| /_cat/pending_tasks | /_cluster/pending_tasks | |
| /_cat/nodes | /_nodes | |
| /_cat/tasks | /_nodes/usage | |
| /_cat/templates | /_nodes/hot_threads | |
| /_cat/thread_pool | /_nodes/stats | |
| /_cat/ml/trained_models | /_tasks | |
| /_cat/transforms/_all | /_remote/info | |
| /_cat/aliases | ||
| /_cat/allocation | ||
| /_cat/ml/anomaly_detectors | ||
| /_cat/count | ||
| /_cat/ml/data_frame/analytics | ||
| /_cat/ml/datafeeds | ||
| /_cat/fielddata | ||
| /_cat/health | ||
| /_cat/indices | ||
| /_cat/master | ||
| /_cat/nodeattrs | ||
| /_cat/nodes |
These endpoints were taken from the documentation where you can find more.[1]
Also, if you access /_cat the response will contain the /_cat/* endpoints supported by the instance.
In /_security/user (if auth enabled) you can see which user has role superuser.
Hidden indices, aliases and data streams
/_cat/indices alone is incomplete: wildcard expansion may omit hidden targets, and applications commonly access aliases or data streams rather than concrete indices. The resolve API returns matching indices, aliases, and data streams; mappings and field capabilities then show searchable field names without first downloading documents.[1]
ES=https://ELASTICSEARCH-SERVER:9200
curl -sk "$ES/_resolve/index/*?expand_wildcards=all&pretty"
curl -sk "$ES/_data_stream/*?expand_wildcards=all&pretty"
curl -sk "$ES/_alias/*?expand_wildcards=all&pretty"
curl -sk "$ES/*/_mapping?expand_wildcards=all&pretty"
curl -sk "$ES/*/_field_caps?fields=*&expand_wildcards=all&pretty"
Also enumerate objects that disclose data flow, external storage, or other clusters. Success and 403 responses help map the credential’s cluster privileges:
curl -sk "$ES/_ingest/pipeline?pretty"
curl -sk "$ES/_index_template?pretty"
curl -sk "$ES/_component_template?pretty"
curl -sk "$ES/_snapshot?pretty"
curl -sk "$ES/_remote/info?pretty"
Indices
You can gather all the indices accessing http://10.10.10.115:9200/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open .kibana 6tjAYZrgQ5CwwR0g6VOoRg 1 0 1 0 4kb 4kb
yellow open quotes ZG2D1IqkQNiNZmi2HRImnQ 5 1 253 0 262.7kb 262.7kb
yellow open bank eSVpNfCfREyYoVigNWcrMw 5 1 1000 0 483.2kb 483.2kb
To obtain information about which kind of data is saved inside an index you can access: http://host:9200/<index> from example in this case http://10.10.10.115:9200/bank

Dump index
If you want to dump all the contents of an index you can access: http://host:9200/<index>/_search?pretty=true like http://10.10.10.115:9200/bank/_search?pretty=true

Take a moment to compare the contents of the each document (entry) inside the bank index and the fields of this index that we saw in the previous section.
The hits.total field indicates the number of matches, but only 10 hits are returned by default. Use track_total_hits=true when an exact count matters. A larger size works only up to the index’s index.max_result_window (10,000 by default), so a single request is not a reliable complete dump.[1]
For a consistent dump beyond that window, create a point in time (PIT) and repeatedly use the last hit’s sort array as search_after. Always use the newest pit_id returned by the previous response, renew keep_alive, and close the PIT when finished:[1]
PIT=$(curl -skXPOST "$ES/bank/_pit?keep_alive=2m" | jq -r .id)
curl -skXPOST "$ES/_search" -H 'Content-Type: application/json' -d \
"{\"size\":1000,\"track_total_hits\":true,\"query\":{\"match_all\":{}},\"pit\":{\"id\":\"$PIT\",\"keep_alive\":\"2m\"},\"sort\":[{\"_shard_doc\":\"asc\"}]}"
# Save the returned pit_id, then repeat with: "search_after": [<last hit sort values>]
# Set PIT to the most recently returned pit_id before closing it.
curl -skXDELETE "$ES/_pit" -H 'Content-Type: application/json' -d "{\"id\":\"$PIT\"}"
Dump all
In order to dump all you can just go to the same path as before but without indicating any indexhttp://host:9200/_search?pretty=true like http://10.10.10.115:9200/_search?pretty=true
Remember that in this case the default limit of 10 results will be applied. You can use the size parameter to dump a bigger amount of results. Read the previous section for more information.
Search
If you are looking for some information you can do a raw search on all the indices going to http://host:9200/_search?pretty=true&q=<search_term> like in http://10.10.10.115:9200/_search?pretty=true&q=Rockwell

If you want just to search on an index you can just specify it on the path: http://host:9200/<index>/_search?pretty=true&q=<search_term>
The q parameter uses Lucene query-string syntax, including /regexp/ expressions when expensive queries are allowed. Prefer a JSON Query DSL body for precise field selection and escaping.
You can also use something like https://github.com/misalabs/horuz to fuzz an elasticsearch service.
Write permissions
You can check your write permissions trying to create a new document inside a new index running something like the following:
curl -X PUT 'http://10.10.10.115:9200/bookindex/_doc/A00-3?refresh=true' \
-H 'Content-Type: application/json' -d '{
"bookId": "A00-3",
"author": "Sankaran",
"publisher": "Mcgrahill",
"name": "how to get a job"
}'
This creates a new index called bookindex and a document with ID A00-3. Current Elasticsearch versions use the typeless /_doc/<ID> endpoint; paths such as /bookindex/books are only relevant to old releases. Prefer /_security/user/_has_privileges for a non-destructive permission check, because this request may auto-create an index and persist data.[1]
Notice how the new index appears now in the list:

And note the automatically created properties:

Ingest pipelines and file-parser attack surface
Inspect /_ingest/pipeline for attachment processors and note their input field, indexed_chars, and failure handlers. The attachment processor base64-decodes attacker-controlled files and passes formats such as PDF and Office documents to Apache Tika on an ingest node, making an otherwise ordinary indexing permission a server-side parser entry point.[3]
A recent example was an XXE in Tika’s PDF parser: when affected Elasticsearch versions processed a crafted XFA document through an attachment pipeline, an authenticated attacker could cause requests to internal/third-party services or read sensitive data. This required the attachment processor and an affected release, so validate both the pipeline and exact version before testing; patched versions and older Java-Security-Manager-based branches were not equally exposed.[3]
Use a harmless file first to prove that a reachable index invokes the processor. If the credential has read_pipeline (or broader pipeline-management) rights, the simulate API avoids persisting a document:[1]
DATA=$(base64 -w0 harmless.pdf)
curl -skXPOST "$ES/_ingest/pipeline/PIPELINE_ID/_simulate" \
-H 'Content-Type: application/json' \
-d "{\"docs\":[{\"_source\":{\"data\":\"$DATA\"}}]}"
Automatic Enumeration
Some tools will obtain some of the data presented before:
msf > use auxiliary/scanner/elasticsearch/indices_enum
Shodan
port:9200 elasticsearch