[Codegate 2018] WEB – rbSql

해당 문제는 소스를 공개하는 오픈소스 형태의 문제입니다. 가장 먼저 문제 페이지에 접속해보면..

ㅗㅜㅑ… 익숙한 페이지가 눈 앞에 나타납니다. 최근 몇년간의 국내 CTF를 참가해 웹 분야의 문제를 풀어보았다면 많이 봤을 크리스탈입니다. 딱 봤을 때 있는 기능이라면 Photo와 M/V가 있으며, Join과 Login 기능이 존재합니다. 문제 이름에 SQL이 들어가기 때문에 SQL Injection이라고 생각되지만, 자세한 것은 소스코드를 확인해봐야 알 것 같습니다.

문제 설명에 첨부되어있는 파일을 다운받아 압축을 해제하면 크게 두가지 메인 소스코드로 나뉘어져 있다는 것을 알 수 있습니다.

index.php와 dbconn.php입니다.

일반적으로 dbconn.php에는 해당 웹서버에서 사용하고 있는 데이터베이스에 대한 설정(아이디, 패스워드, 호스트, 포트, 사용할 DB명 등)을 담고 다른 페이지에서 import하여 사용하는 형태이지만, 이 문제는 달랐습니다. rbSql이라는 제목이 허투루 나온 것이 아니라 출제자 본인이 직접 생각하여 구현한 데이터베이스 형태인 것 같습니다. dbconn.php 내에는 저장할 데이터에 대한 파싱 방법, 패킹 방법, 그리고 데이터를 삽입/삭제/조회할 수 있는 기능들을 구현해 놓았습니다.

다음은 index.php와 dbconn.php의 소스코드입니다.

index.php

dbconn.php

 

자. 그럼 이 두 소스 내에 존재하는 취약점을 이용해 플래그를 얻어내야 하는데요, index.php의 하단을 살펴보면 ‘lvl’ 세션이 2일 경우(Admin의 권한일 경우) 플래그를 출력하도록 되어있습니다.

flag routine in index.php

또한 출력한 이후 해당 멤버의 정보를 삭제하는 기능이 포함되어 있는 것으로 보아 insert injection의 기운이 느껴졌습니다. 하지만 이 문제는 일반적인 SQL이 아닌 rbSql입니다. 따라서 dbconn.php 내에 포함되어 있는 기능들을 분석하여 데이터 삽입 시 취약한 부분을 찾아내 익스플로잇을 해야겠죠.

rbSql의 구조는 간단합니다.

rbSqlSchema라는 메인 스키마 파일이 존재하며, 이 파일이 관리하는 서브테이블(멤버테이블)들이 존재합니다. 각 서브테이블에 있는 데이터를 얻기 위해서는 rbSqlSchema를 참조하여 멤버 이름에 해당하는 서브테이블을 찾아가 패킹된 데이터를 파싱해야 합니다. 이를 위해 dbconn.php를 분석해보죠.

dbconn.php 내에는 rbSql, rbParse, rbPack, rbGetPath, rbReadFile, rbWriteFile로 총 6개의 함수가 존재합니다. 우리가 원하는 것은 회원가입 시 취약한 파라미터에 untrusted input을 삽입해 멤버 권한(lvl)을 2로 만드는 것입니다.

join_chk in index.php

index.php에서 회원가입을 수행하는 코드입니다. 전달 받는 파라미터는 uid와 umail, upw인데 uid의 경우 ctype_alnum 함수로 인해 알파벳과 숫자만을 입력받으며, upw의 경우 전달하기 전 이미 md5 hash 되기 때문에 공격 벡터로 적절하지 못합니다. 따라서 남은 파라미터는 umail이 될 것입니다.

회원가입 시 rbSql 함수의 create와 insert 기능을 수행하기 때문에 dbconn.php에서 이 부분을 집중적으로 확인해야합니다.

“create” of rbSql function in dbconn.php

create의 경우 SCHEMA 파일을 읽어온 후 3번째 데이터부터 현재 존재하는 서브테이블의 이름과 일치할 경우(멤버 이름 일치) 에러를 발생시킵니다. SCHEMA는 dbconn.php 상단부에 define 함수로 rbSqlSchema의 경로를 정의한 상수입니다. 이 부분을 통과하게 되면 새로운 서브테이블을 만들기 위해 rand 함수를 이용해 rbSql_[random value 16자리]의 파일명으로 서브테이블을 만들 준비를 하며, SCHEMA 파일에 해당 값을 입력합니다. 그리고 해당 파일을 생성 후 권한을 할당하고, index.php에서 전달받은 배열의 값을 서브테이블에 입력합니다.

“insert” of rbSql function in dbconn.php

이후 uid, umail, upw, uip, lvl의 값을 insert 기능을 이용해 create에서 만든 서브테이블에 입력합니다.

그럼 rbSql에서 값을 쓰고, 읽어오는 부분을 확인해봐야겠죠. 먼저 rbWriteFile부터 살펴봅시다.

rbWriteFile function in dbconn.php

먼저 파일 경로에 있는 파일을 불러오고, rbPack 함수를 이용해 파일의 내용을 패킹한 후 해당 경로에 있는 파일에 저장합니다.

rbPack function in dbconn.php

데이터를 패킹하는 rbPack 함수의 구조는 간단합니다. 여기서 dbconn.php 상단부에서 정의한 STR과 ARR 상수가 사용되는데, 이는 각각 0x01, 0x02입니다.

1. 만약 데이터가 string일 경우 0x01, 길이, 데이터의 형태로 저장이 됩니다.

2. 만약 데이터가 array일 경우 0x02, 배열의 갯수, …의 형태로 저장이 됩니다. 이 때 rbPack 함수가 재귀적으로 호출되기 때문에 뒤따라오는 데이터가 string일 경우 1번으로 갈 것이고, array일 경우 다시 2번을 수행하겠죠.

저희는 이를 확인하기 위해 직접 서버를 구축하여 테스트를 진행하였습니다.

쉽게 그림으로 설명하자면 빨간색은 데이터 타입(0x01은 string, 0x02는 array), 초록색은 배열의 갯수, 주황색은 string의 길이, 노란색은 data가 될 것입니다.

그럼 다음으로 rbReadFile을 살펴보죠.

rbReadFile function in dbconn.php

먼저 파일 경로에 있는 파일을 읽어온 후 rbParse 함수를 통해 데이터를 파싱합니다.

rbParse function in dbconn.php

rbParse 함수는 받아온 rawdata를 한 바이트씩 읽어들이는데, 여기서도 STR과 ARR이 사용됩니다.

만약 따라오는 데이터가 string일 경우 1의 값을 가지며 길이 값을 받아오고, 해당하는 길이만큼 parsed에 저장합니다.

string이 아닌 array일 경우 2의 값을 가지며 아래와 같은 프로세스를 가집니다.

1. 배열의 갯수 파싱

2-1. 이후 뒤따라오는 데이터가 또 다시 배열일 경우 해당하는 갯수만큼 string 값을 가져와 parsed에 저장(2차원 배열의 형태)

2-2. 이후 뒤따라오는 데이터가 string일 경우 해당하는 갯수만큼 string 값을 가져와 parsed에 저장(1차원 배열의 형태)

 

자, 익스플로잇에 필요한 메뉴 분석은 모두 끝마쳤습니다. 이제 이 정보를 이용해 umail 파라미터에 데이터를 삽입해 권한을 탈취해야합니다. 하지만 문제는 rbPack 함수의 내용을 토대로 생각해보면 아무리 umail 파라미터에 데이터를 삽입해도 결국 string이기 때문에 파싱 과정에서 해당 길이만큼 평문으로 인식해버린다는 점입니다.

join_chk in index.php

여기서 살펴볼 부분이 umail 파라미터에 대한 길이 제한입니다. hex value의 경우 값의 범위는 0x00~0xff까지입니다. 만약 0xff를 넘어가는 값이 올 경우 overflow로 인해 0x00이 되겠죠. umail의 길이제한을 다시 살펴보면 256바이트까지 입력을 받을 수 있도록 허용해두었습니다. 따라서 rbPack의 포맷을 맞춰 데이터를 삽입하고 256바이트까지 데이터를 입력해 umail의 길이를 00으로 바꾼다면 lvl의 값을 수정할 수 있습니다.

회원가입 시 fiddler를 이용하여 값을 변조하였습니다.

umail 파라미터에 삽입한 페이로드는 다음과 같습니다.

%01%20c4ca4238a0b923820dcc509a6f75849b%01%00%01%012AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

umail 이전의 패킹된 데이터는 0x01,0x00이기 때문에 0x01,0x20,md5 hash값을 넣어주면서 upw를 맞춰주었고, 0x01,0x00은 uip, 0x01,0x01,2는 lvl 부분입니다. 나머지는 256바이트를 맞춰주기 위한 더미 데이터입니다.

현재 페이지는 대회중 구축한 테스트페이지에 적용한 상태이며, 글을 쓰는 현재는 대회가 종료된 시점이기 때문에 실서버에 대한 캡처는 하지 못했습니다.

아무튼 위와 같은 페이로드로 admin의 권한을 획득할 수 있었으며, 플래그를 얻어냈습니다.

 

FLAG : FLAG{akaneTsunemoriIsSoCuteDontYouThinkSo?}

+ Recent posts