记一次代码审计 [De1CTF 2019]SSRF Me
[De1CTF 2019]SSRF Me
第一次审计这么长的代码,很值得记录一下
以下是审计过程中的一些笔记
#! /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)
#_init_初始化
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
#第一个if:如果checkSign(self) 返回 True ,则进入下一个if
#审计checkSign(self)
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
#如果scan在action里面,则我们可以让param进入scan这个函数,并的目录下创建一个result.txt
#然后通过scan()函数把名字为param的网址里的内容写到result.txt中,由于param是可控的,所以很容易想到这里把flag.txt传给param。
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
#如果read在action里面,则我们可以读取读取result.txt的内容赋值给result
def checkSign(self):
if (getSign(self.action,self.param) == self.sign):
return True
else:
return False
#如果getSign(self.action, self.param)和self.sign相等则返回True ,否则返回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)
#提取get方法传入的,参数名叫param对应得值,并将其url编码后赋值给param
#将scan赋值给action
#审计 getSign()
@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())
#以get方法传入param参数值,在cookie里面传递action和sign的值
#使param绕过waf,审计waf
#用我们传进去的 action 、 param 、sign 、ip 这四个参数构造一个Task类对象,并且执行它的Exec方法
#审计Task
@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()
#将secert_key 、param 、 action这三个值连接起来后进行md5加密,并将其作为十六进制数据字符串值返回
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
#移除param头尾指定的字符(默认为空格或换行符),并将param中中所有大写字符转化为小写
#若param以gopher或file为前缀,返回True,否者返回False
#回到challenge()
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0')
总结
审计从路由开始,然后在慢慢延申出去
这里有三个路由:
- /geneSign
- /De1ta
- /
- 从/De1ta开始看起,首先是创建了一个Task的类,action、sign的值是由cookie得到,而param的值就是直接通过GET方法传递param参数的值得到,ip就是你的ip地址,接着param参数会经过waf,如果过了waf,则执行这个类的Exec。
-
顺着这个思路,我们追溯到waf这个方法上,通过审计我们知道要绕过waf,param的值不能以 gopher和file开头
-
接下去执行Task里的Exec方法,通过审计我们发现如果checkSign(self) 为真 ,则可以传递/De1ta页面的param参数进入到scan方法,并的目录下创建一个result.txt ,然后通过scan()函数把参数param的值写到result.txt中,由于param是可控的,所以很容易想到这里把flag.txt传给param。
-
审计checkSign(self) 函数,发现如果getSign(cookie传入的action, get传入的param)和cookie传入的sign相等则返回True ,否则返回False
-
审计getSign(),我们发现不知道secert_key的值,但是路由/geneSign有一个return getSign(scan, param),这里我们另/geneSign页面的参数param的值为
flag.txtread
(这里为什么后面会讲到),通过getSign得到的sign值即为md5(secert_key + 'flag.txtread' + 'scan') -
回到Task类的Exec方法
if "read" in self.action:
如果read在action里面,则我们可以读取读取result.txt的内容赋值给result,这里result.txt的值实际上是我们传入的param的值 -
在这里就可以解释为什么/geneSign页面我们传入的param的值要为flag.txtread了,因为结合Exec方法,我们要实现写入文件和读出的功能,就必须另//De1ta页面的action为readsacn或scanread,此时的getSign(),返回的值就是hashlib.md5(secert_key + flag.txt + readscan).hexdigest(),而此时只有另/geneSign页面的param参数为flag.txtread才能使
getSign(self.action, self.param) == getSign(flag.txt+readscan) ,
即md5(secret_key+flag.txtread+scan) == md5(secret_key+flag.txt+readscan)
所以这里总的做法就是在/geneSign页面get ?param=flag.txtread,获得mds值,而这个值其实是等于/De1ta页面的sign值的
所以第二步就是在/De1ta页面,get ?param=flag.txt ,cookie action=readscan ,sign=我们在/geneSign页面得到的md5值,这样就可以得到flag了