CRLF + SSRF

什么是SSRF

SSRF(Server-Side Request Forgery:服务器端请求伪造)是一种由攻击者构造形成并由服务端发起恶意请求的一个安全漏洞。正是因为恶意请求由服务端发起,而服务端能够请求到与自身相连而与外网隔绝的内部网络系统,所以一般情况下,SSRF的攻击目标是攻击者无法直接访问的内网系统。

SSRF漏洞的形成大多是由于服务端提供了从其他服务器应用获取数据的功能而没有对目标地址做过滤和限制。例如,黑客操作服务端从指定URL地址获取网页文本内容,加载指定地址的图片,下载等,利用的就是服务端请求伪造,SSRF漏洞可以利用存在缺陷的WEB应用作为代理攻击远程和本地的服务器。

一些前置知识

HTTP协议

在这里,给大家简单的介绍一下我们是怎么通过浏览器上网的

IP

和我们购物时候必须填写地址一样,在网络空间中我们也必须有一个属于自己的地址,而这个地址便是IP地址

IP地址有两大类,是IPv4和IPv6

1
2
3
114.114.114.114                                 //ipv4

2001:0000:0000:0000:0000:25de:0000:cafe //ipv6

v4所有地址已经被分配完毕,所以 有了ipv6这个东西。

又因为ipv6很长,看起来不太美观,于是有了更简洁的写法

  • 每项数字前导的0可以省略,省略后前导数字仍是0则继续,例如下组IPv6是等价的。

      2001:0db8:02de:0000:0000:0000:0000:0e13
      2001:db8:2de:0000:0000:0000:0000:e13
      2001:db8:2de:000:000:000:000:e13
      2001:db8:2de:00:00:00:00:e13
      2001:db8:2de:0:0:0:0:e13
    
  • 可以用双冒号“::”表示一组0或多组连续的0,但只能出现一次:

    • 如果四组数字都是零,可以被省略。遵照以上省略规则,下面这两组IPv6都是相等的。

        2001:db8:2de:0:0:0:0:e13
        2001:db8:2de::e13
      
        2001:0db8:0000:0000:0000:0000:1428:57ab
        2001:0db8:0000:0000:0000::1428:57ab
        2001:0db8:0:0:0:0:1428:57ab
        2001:0db8:0::0:1428:57ab
        2001:0db8::1428:57ab
      
    • 2001::25de::cade 是非法的,因为双冒号出现了两次。它有可能是下种情形之一,造成无法推断。

        2001:0000:0000:0000:0000:25de:0000:cade
        2001:0000:0000:0000:25de:0000:0000:cade
        2001:0000:0000:25de:0000:0000:0000:cade
        2001:0000:25de:0000:0000:0000:0000:cade
      

      如果这个地址实际上是IPv4的地址,后32位可以用10进制数表示;因此::ffff:192.168.89.9 相等于::ffff:c0a8:5909。

Hosts文件

Hosts文件是一个没有扩展名的操作系统文件,以表的形式存储了主机名和IP地址的映射关系Hosts又称host table,译为“主机表”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
# 102.54.94.97 rhino.acme.com # source server
# 38.25.63.10 x.acme.com # x client host

# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
219.217.199.8 earth.local terratest.earth.local
10.110.110.250 vpn.misakanetworks.cf
# Added by Docker Desktop
192.168.1.8 host.docker.internal
192.168.1.8 gateway.docker.internal
# To allow the same kube context to work on the host and the container:
127.0.0.1 kubernetes.docker.internal
# End of section
127.0.0.1 activate.navicat.com

我们可以手动给一个Ip取一个好听的名字然后用这个名字访问,只需要按照host文件的格式在下面添加就行了

DNS

使用host是无法记住所有ip的,数据量太大了,而且日常上网也并不需要记住所有的ip,DNS就这样诞生了…

域名系统(英语:Domain Name System,缩写:DNS)

它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53

HTTP协议

HTTP请求结构

HTTP请求报文由3部分组成(请求行+请求头部+请求正文)

  • 请求行

    请求行: 是由 请求字段、URL字段、HTTP协议版本字段 三个字段组成,他们用空格分隔,例如:GET/material/index HTTP/1.1\r\n根据HTTP标准,HTTP请求可以使用多种请求方法

    HTTP1.0定义了三种请求方法:GET,POST,HEAD方法。HTTP1.1新增了五种请求方法:OPTIONS,PUT,DELETE,TRACE,CONNECT方法

  • 请求头部

    请求头部: HTTP客户程序(例如浏览器),向服务器发送请求的时候必须指明请求类型(一般是GET或者 POST),如有必要,客户程序还可以选择发送其他的请求头。大多数请求头并不是必需的,但Content-Length除外,对于POST请求来说 Content-Length必须出现

    请求报头通知服务器关于客户端求求的信息,典型的请求头有:

  • 空行

    他的作用是告诉服务器 请求头部信息到此为止

  • 请求正文

    若方法是 GET,则该项为空。(数据都在url 地址栏里面)
    若方法是 post 字段,则通常放置的是要 提交的数据

HTTP响应结构:

HTTP的响应报文是由( 状态行、响应头部、响应正文) 三部分组成

  • 响应行

    响应行: 描述了响应的状态,一般由协议版本、状态码及其描述组成 比如 HTTP/1.1200OK\r\n,其中协议版本HTTP/1.1或者HTTP/1.0,200就是它的状态码,OK则为它的描述。

五种可能的取值:

常见状态码:

响应头部

响应头部: 用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它回送的数据。

  • 响应体

响应体就是响应的消息体,它包含了响应的内容。它可以包含HTML代码,图片,等等。主体是由传输在HTTP消息中紧跟在头部后面的数据字节组成的。

SSRF中的常用协议

HTTP协议

curl_exec()

curl_init(url)函数初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。

1
2
3
4
5
6
7
8
9
<?php 
$url=$_POST['url'];
$ch=curl_init($url); //创造一个curl资源
curl_setopt($ch, CURLOPT_HEADER, 0); //设置url和相应的选项
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch); // 抓取url并将其传递给浏览器
curl_close($ch); //关闭curl资源
echo ($result);
?>

fsockopen()

fsockopen($hostname,$port,$errno,$errstr,$timeout)用于打开一个网络连接或者一个Unix 套接字连接,初始化一个套接字连接到指定主机(hostname),实现对用户指定url数据的获取。该函数会使用socket跟服务器建立tcp连接,进行传输原始数据。 fsockopen()将返回一个文件句柄,之后可以被其他文件类函数调用(例如:fgets(),fgetss(),fwrite(),fclose()还有feof())。如果调用失败,将返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$host=$_GET['url'];
$fp = fsockopen($host, 80, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)<br />\n";
} else {
$out = "GET / HTTP/1.1\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n\r\n";
fwrite($fp, $out);
while (!feof($fp)) {
echo fgets($fp, 128);
}
fclose($fp);
}
?>

可以看到,这个协议是一个比较底层的协议,从TCP手动构造一个http的请求,在这里出现了\r\n,这代表什么呢?

CRLF和LF

CR(回车\r),LF(换行\n)。

CR和LF是缩写,其实他们的全称分别是:”Carriage-Return”和”Line-Feed”。追本溯源的说,CR(Carriage-Return)和LF(Line-Feed)这两个词来源于打字机的发明和使用。

在很久以前的机械打字机时代,CR和LF分别具有不同的作用:LF会将打印纸张上移一行位置,但是保持当前打字的水平位置不变;CR则会将“Carriage”(打字机上的滚动托架)滚回到打印纸张的最左侧,但是保持当前打字的垂直位置不变,即还是在同一行。

当CR和LF组合使用时,则会将打印纸张上移一行,且下一个打字位置将回到该行的最左侧,也就是我们今天所理解的换行操作。

虽然现在机械打字机渐渐地退出了历史舞台。但是回车换行在计算机操作系统中确实必要的,而在计算机中回车换行实则为同样的结果,不再像打字机那样了,计算机的回车换行都是切换到下一行的行首位置了。在操作系统出现的年代,一些操作系统的设计者决定采用单个字符来表示换行符(也许是受限于内存和软盘空间的不足),如Unix的LF、MacIntosh的CR;但是想windows则是使用两个字符表示。他们的意图都是为了进行换行操作,只是当初并没有一个国际标准,所以才有这样字符上的不同。

在http中,则是用\r\n来表示换行

SoapClient

SOAP是简单对象访问协议,简单对象访问协议(Simple Object Access Protocol)是一种轻量的、简单的、基于 XML 的协议,它被设计成在 WEB 上交换结构化的和固化的信息。PHP 的 SoapClient 就是可以基于SOAP协议可专门用来访问 WEB 服务的 PHP 客户端。

这里有个Trick

正常情况下的SoapClient类,调用一个不存在的函数,会去调用__call方法

1
2
3
4
5
6
<?php
$a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://127.0.0.1:5555/path'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();

基于刚才讲到的CRLF,则可以做到手动构造任意请求,

1
2
3
4
5
6
7
8
<?php
$target = "http://127.0.0.1:9999/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "byc\r\nCookie: PHPSESSID=g6ooseaeo905j0q4b9qqn2n471\r\n",
'uri' => "123"));
$payload -> not_exists_function();
?>

File协议

file协议其实类似于任意文件读取,比如file:///etc/passwd

还是刚才那段代码

1
2
3
4
5
6
7
8
9
<?php 
$url=$_POST['url'];
$ch=curl_init($url); //创造一个curl资源
curl_setopt($ch, CURLOPT_HEADER, 0); //设置url和相应的选项
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch); // 抓取url并将其传递给浏览器
curl_close($ch); //关闭curl资源
echo ($result);
?>

file_get_contents() & readfile()

1
2
3
4
<?php
$url = $_GET['url'];
echo file_get_contents($url);
?>

file_get_contents() 函数将整个文件或一个url所指向的文件读入一个字符串中,并展示给用户,我们可以传入一个url连接访问,也可以构造类似?url=../../../../../etc/passwd的paylaod即可读取服务器本地的任意文件

gopher

gopher协议是一个古老且强大的协议,可以理解为是http协议的前身,他可以实现多个数据包整合发送。通过gopher协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。

很多时候在SSRF下,我们无法通过HTTP协议来传递POST数据,这时候就需要用到gopher协议来发起POST请求了。

gopher的协议格式如下:

1
2
3
gopher://<host>:<port>/<gopher-path>_<TCP数据流>
<port>默认为70
发起多条请求每条要用回车换行去隔开使用%0d%0a隔开,如果多个参数,参数之间的&也需要进行URL编码

看起来有点像阉割版的nc,不过gopher会把传入的第一个字符给吞掉了,左边传入的是123456789,右边接收到的则是23456789

在知道以上特性之后,便可以利用gopher协议来构造任意http请求了

比如:

1
2
3
GET /ssrf.php HTTP/1.1\r\n
Host: 127.0.0.1\r\n
\r\n

将其url编码一下

1
%47%45%54%20%2f%73%73%72%66%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%32%37%2e%30%2e%30%2e%31%0d%0a


 web
  

:D 一言句子获取中...