adm1nkyj

소스코드

index.php

<?php
    error_reporting(0);
    
    include("./config.php"); // hidden column name, $FLAG.
 
    mysql_connect("localhost","adm1nkyj","adm1nkyj_pz");
    mysql_select_db("adm1nkyj");
 
    /**********************************************************************************************************************/
 
    function rand_string()
    {
        $string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz";
        return str_shuffle($string);
    }
 
    function reset_flag($COUNT_COLUMN_NAME, $FLAG_COLUMN_NAME)
    {
        $flag = rand_string();
        # findflag_2 테이블에 저장된 모든 $COUNT_COLUMN_NAME, $FLAG_COLUMN_NAME 행을 조회하여 배열로 반환
        # mysql_fetch_array 함수를 사용하면 컬럼명으로 값을 조회할 수 있음
        $query = mysql_fetch_array(mysql_query("SELECT $COUNT_COLUMN_NAME, $FLAG_COLUMN_NAME FROM findflag_2"));
 
        # findflag_2 테이블 조회결과 중 $COUNT_COLUMN_NAME 값이 150인 행이 있으면,
        if($query[$COUNT_COLUMN_NAME] == 150)
        {
            # 해당 행의 $FLAG_COLUMN_NAME 값을 새로 발급한 랜덤 플래그 값으로 업데이트
            if(mysql_query("UPDATE findflag_2 SET $FLAG_COLUMN_NAME='{$flag}';"))
            {   
                # 그리고 모든 행의 $COUNT_COLUMN_NAME 컬럼값을 0으로 초기화하고 화면에 reset flag 값을 출력
                mysql_query("UPDATE findflag_2 SET $COUNT_COLUMN_NAME=0;");
                echo "reset flag<hr>";
            }
            return $flag;
        }
        else # findflag_2 테이블 조회결과 중 $COUNT_COLUMN_NAME 값이 150인 행이 없으면,
        {   
            # 모든 행의 $COUNT_COLUMN_NAME 컬럼값에 +1을 추가
            mysql_query("UPDATE findflag_2 SET $COUNT_COLUMN_NAME=($query[$COUNT_COLUMN_NAME] + 1);");
        }
        return $query[$FLAG_COLUMN_NAME];
    }
 
    function get_pw($PW_COLUMN_NAME){
        # 쿼리문 조회결과 중 1번째 행의 $PW_COLUMN_NAME 값을 가져와 반환
        $query = mysql_fetch_array(mysql_query("select $PW_COLUMN_NAME from findflag_2 limit 1"));
        return $query[$PW_COLUMN_NAME];
    }
 
    /**********************************************************************************************************************/
 
    $tmp_flag = "";
    $tmp_pw = "";
    $id = $_GET['id'];
    $pw = $_GET['pw'];
    $flags = $_GET['flag'];
    if(isset($id))
        # GET 요청 시 $id 파라미터로 전달된 값이 있으면 52-68 코드블럭 진입
    {
        # GET 요청 시 $id, $pw 파라미터에 전달된 값 중 아래 문자열이 존재하면 공격으로 탐지 (대소문자 구분 안 함)
        if(preg_match("/information|schema|user/i", $id) || substr_count($id,"(") > 1) exit("no hack");
        if(preg_match("/information|schema|user/i", $pw) || substr_count($pw,"(") > 1) exit("no hack");
 
 
        # $COUNT_COLUMN_NAME, $FLAG_COLUMN_NAME, $ID_COLUMN_NAME, $PW_COLUMN_NAME → config.php 파일에 선언되어 있던 변수
        $tmp_flag = reset_flag($COUNT_COLUMN_NAME, $FLAG_COLUMN_NAME);
        $tmp_pw = get_pw($PW_COLUMN_NAME);
        $query = mysql_fetch_array(mysql_query("SELECT * FROM findflag_2 WHERE $ID_COLUMN_NAME='{$id}' and $PW_COLUMN_NAME='{$pw}';"));
        if($query[$ID_COLUMN_NAME])
        {
            if(isset($pw) && isset($flags) && $pw === $tmp_pw && $flags === $tmp_flag)
            {
                echo "good job!!<br />FLAG : <b>".$FLAG."</b><hr>";
            }
            else
            {
                echo "Hello ".$query[$ID_COLUMN_NAME]."<hr>";
            }
        }
    } else {
        highlight_file(__FILE__);
    }
?>

  • 68라인 코드를 보면 사용자로부터 입력받은 $id, $pw 파라미터가 SQL 쿼리문에 그대로 사용되고 있었다. 따라서 해당 구간에 SQLi 공격을 시도해볼 수 있다. (입력된 아이디/패스워드가 DB에 저장된 값과 일치하지 않는 경우에는 ‘else {…}’ 절로 코드가 분기되기 때문에 findflag_2 테이블의 1번째 행에 저장된 ID_COLUMN_NAME 값을 확인할 수 있다.)

  • 아래와 같이 id 파라미터에 SQLi 공격을 수행하여 adm1ngnngn 계정명을 획득했다.

  • 다음으로 adm1ngnngn 계정의 패스워드를 구해보자. 68라인에서 실행된 SQL 쿼리문에서 조회된 결과가 없으면 화면에 아무런 정보도 반환되지 않지만 유효한 계정이 확인되는 경우에는 78라인이 출력된다. 이러한 서버 측 응답 차이를 이용하여 Blind SQLi 공격을 수행해볼 수 있다.

  • 여기서 쿼리문에 사용되는 $id, $pw 값은 약간의 문자열 검증 로직이 존재하기 때문에 이를 우회해야 한다.

  • 이는 UNION 절을 이용하면 간단히 우회할 수 있었다.
  • 아래와 같이 id 파라미터에 인젝션 페이로드를 삽입하면 오류가 발생하지 않고 쿼리 실행이 가능하다.
    • (findflag_2 테이블의 컬럼 수는 5개였음)
    • (공격 페이로드) adm1ngnngn'+UNION+SELECT+NULL,NULL,NULL,NULL,NULL+--+

  • id, pw 파라미터에 아래와 같은 값을 입력했을 때 “Hello and xPw4coaa1sslfe=” 응답이 반환됐다.
  • 이를 통해 $ID_COLUMN_NAME 컬럼은 findflag_2 테이블의 2번째 컬럼이라는 것을 알 수 있다.
  • 추가로, $PW_COLUMN_NAME 변수에 저장된 값(컬럼명)은 “xPw4coaa1sslfe” 였다.
    • (' and $PW_COLUMN_NAME=') 문자열이 2번째 컬럼의 값으로 전달되면 $PW_COLUMN_NAME 값이 본래의 값으로 변환되기 때문에 원래 어떤 값이었는지를 확인할 수 있다.
  • (공격 페이로드) SELECT * FROM findflag_2 WHERE $ID_COLUMN_NAME='123'+UNION+SELECT+1,' and $PW_COLUMN_NAME=',3,4,5--+';

  • 다음으로 adm1ngnngn 계정의 패스워드는 !@SA#$! 인 것을 확인했다.
    • 맨 앞의 1은 DB에 존재하지 않는 임의의 계정명을 입력해서 좌측 쿼리가 아무런 값도 반환하지 않도록 하기 위함이다.
      • UNION 절을 사용할 때, 좌측/우측 쿼리에서 모두 값이 조회되면 좌측 쿼리 결과 값이 우선 반환된다.
    • 이후 UNION 절 뒤에 오는 우측 쿼리는 findflag_2 테이블로부터 패스워드(xPw4coaa1sslfe) 컬럼 값을 추출하는데, 이렇게 추출된 값은 (line 82) "Hello ".$query[$ID_COLUMN_NAME] 코드에서 $ID__COLUMN_NAME 값으로 대신 출력된다.
  • (공격 페이로드) '+UNION+SELELCT+1,xPw4coaa1sslfe,3,4,5+FROM+findflag_2+--+

  • 이제 DB에 저장된 계정의 아이디/패스워드 정보를 획득했으니 플래그가 저장된 컬럼명을 찾아야 한다.
    • ($flags === $tmp_flag) 검증 로직을 우회하는데 필요함


  • 위 내용을 토대로 AS를 이용하여 $FLAG_COLUMN_NAME 값 추출을 시도한다.
  • 현재 $FLAG_COLUMN_NAME 컬럼이 몇 번째 컬럼에 위치한지 모르기 때문에 서브 쿼리 내부에서 1, 3, 4번째 컬럼 모두 테스트를 진행해봐야 한다. (2번째 컬럼은 이전 과정에서 $ID_COLUMN_NAME 인 것을 확인했다.)
    • 확인 결과 순서대로 (인덱스 번호, ID, PW, 플래그) 값이 저장돼 있는 것을 알 수 있었다.
    • (공격 페이로드) '+UNION+SELECT+1,subQuery.FLAG,3,4,5+FROM+(SELECT+1,2,3,4+AS+FLAG,5+UNION+SELECT*+FROM+findflag_2+LIMIT+1,1)+subQuery--+

  • 지금까지 획득한 정보(계정명, 계정 패스워드, 플래그 값)를 사용하여 서버로 요청을 보내면 플래그를 확인할 수 있다.

번외

  • MySQL(MariaDB)에서 서브 쿼리와 달리, 이로부터 파생된 테이블은 반드시 자신의 별칭을 가져야 한다고 한다.
    • 익스플로잇 과정에서 플래그 컬럼명을 추출할 때 서브 쿼리를 사용했는데, 이 때 파생된 테이블에 대한 별칭을 설정하지 않아 제대로 값이 출력되지 않았는데 이러한 이유가 있었다. (여기서 삽질을 많이 했다.)
    • '+UNION+SELECT+1,subQuery.FLAG,3,4,5+FROM+(SELECT+1,2,3,4+AS+FLAG,5+UNION+SELECT*+FROM+findflag_2+LIMIT+1,1)+subQuery--+

🐛.. 🐛.. 🐛..


REFERENCE

  1. https://sqltest.net/
  2. https://gogogameboy.tistory.com/entry/mariadb-as
  3. https://dev.mysql.com/doc/refman/8.4/en/derived-tables.html