[De1CTF 2019]SSRF Me 1.查看提示:
2.打开网页:
观察代码,感觉是flask注入,整理代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 from flask import Flaskfrom flask import requestimport socketimport hashlibimport urllibimport sysimport osimport 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)): 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 resultdef checkSign (self ): if (getSign(self.action, self.param) == self.sign): return True else : return False @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 )
3.代码审计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 from flask import Flaskfrom flask import requestimport socketimport hashlibimport urllibimport sysimport osimport 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)): 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 @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 )
知识点介绍: 参考:Flask Flask request.args.get 获取所有参数(Python)|极客教程 (geek-docs.com)
urllib.unquote(string):将url编码的字符串进行解码
1 2 3 4 5 import urllib.parse str = '%E6%B3%95%E5%9B%BD%E7%BA%A2%E9%85%92' print(urllib.parse.unquote(str))
输出:
使用request.args.get获取指定参数的值 例子:假设我们有一个URL为/user?name=John&age=25
,我们想要获取到对应参数的值
1 2 3 4 5 6 7 8 9 10 11 12 13 from flask import Flask, request app = Flask(__name__) @app.route('/user', methods=['GET']) def get_user(): name = request.args.get('name') age = request.args.get('age') return f"Name: {name}, Age: {age}" if __name__ == '__main__': app.run()
输出:
1 2 在上面的代码中,我们定义了一个路由/user,并指定该路由只支持GET方法。 在get_user函数中,我们使用request.args.get方法获取了URL参数中名为name和age的值,并将其拼接成字符串返回。当我们访问/user?name=John&age=25时,页面会显示Name: John, Age: 25
使用request.args.get获取所有参数的值 例子:有时候我们并不知道URL参数的具体名称,或者URL参数的数量是可变的。这时,我们可以使用request.args.get
方法的另一种形式,即不传递参数名,直接获取所有参数的值
1 2 3 4 5 6 7 8 9 10 11 12 13 from flask import Flask, request app = Flask(__name__)@app.route('/user' , methods=['GET' ] ) def get_user (): args = request.args.get() return f"All Params: {args} " if __name__ == '__main__' : app.run()
输出:
1 在上述代码中,我们使用request.args.get方法获取所有URL参数的值,并将其拼接成字符串返回。例如,当我们访问/user?name=John&age=25&city=New+York时,页面会显示All Params: ImmutableMultiDict([('name', 'John'), ('age', '25'), ('city', 'New York')])。
需要注意的是,request.args.get方法返回的是一个ImmutableMultiDict对象,它类似于字典但是不可修改。我们可以使用其提供的方法来获取参数值,比如args.getlist(‘name’)可以获取名为name的参数的所有值 例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from flask import Flask, request app = Flask(__name__)@app.route('/user' , methods=['GET' ] ) def get_user (): args = request.args.get() name = args.getlist('name' ) return f"Name: {name} " if __name__ == '__main__' : app.run()
输出:
1 在上述代码中,我们使用args.getlist方法获取名为name的参数的所有值,并将其拼接成字符串返回。当我们访问/user?name=John&name=Tom&name=Jack时,页面会显示Name: ['John', 'Tom', 'Jack']
hashlib.md5(): 1 2 3 4 5 import hashlib md = hashlib.md5() md.update('how to use md5 in hashlib?' .encode('utf-8' )) print (md.hexdigest())
输出结果:
1 d26a53750bc40b38b65a520292f69306
str.strip([chars]):移除在字符串str中的首尾字符chars,默认移除空格 1 2 3 4 5 6 str = "0000hello0000".strip('0') print(str) # hello str = " hello ".strip() print(str) # hello
str.lower():将字符串str所有字符转小写 1 2 3 str = "AAAAAsssDDD" .lower()print (str )
Python startswith() 方法用于检查字符串是否是以指定子字符串开头,如果是则返回 True,否则返回 False。如果参数 beg 和 end 指定值,则在指定范围内检查: 参考:Python startswith()方法 | 菜鸟教程 (runoob.com)
urllib.urlopen(path): 1 2 3 4 from urllib.request import urlopen myURL = urlopen("https://www.runoob.com/" )print (myURL.read())
解释:
1 2 3 4 以上代码使用 urlopen 打开一个 URL,然后使用 read() 函数获取网页的 HTML 实体代码。 read() 是读取整个网页内容,我们可以指定读取的长度 如读取长度为300:read(300)
REMOTE_ADDR: 参考:HTTP 请求头中的 Remote_Addr,X-Forwarded-For,X-Real-IP - 23云恋49枫 - 博客园 (cnblogs.com)
1 表示发出请求的远程主机的 IP 地址,remote_addr代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器(Nginx,Apache等)就会把remote_addr设为你的机器IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP
x_forwarded_for: 1 简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,只有在通过了HTTP 代理或者负载均衡服务器时才会添加该项,正如上面所述,当你使用了代理时,web服务器就不知道你的真实IP了,为了避免这个情况,代理服务器通常会增加一个叫做x_forwarded_for的头信息,把连接它的客户端IP(即你的上网机器IP)加到这个头信息里,这样就能保证网站的web服务器能获取到真实IP
os.path.exists(): 1 2 os即operating system(操作系统),Python 的 os 模块封装了常见的文件和目录操作。os.path模块主要用于文件的属性获取,exists是“存在”的意思,所以顾名思义,os.path.exists()就是判断括号里的文件是否存在的意思,括号内的可以是文件路径 原文链接:https://blog.csdn.net/u012424313/article/details/82216092
4.分析: (1).有代码分析得访问url/geneSign只会返回md5(secert_key + param + action)的结果,访问url/只会返回code.txt文件的内容,所以我们只能去访问url/De1ta:
1 http://25f82c99-8160-4244-bc6c-fee7c6ef602e.node4.buuoj.cn:81/De1ta
对路由器@app.route(‘/De1ta’, methods=[‘GET’, ‘POST’])下的函数进行分析后得:
我们需要令param=flag.txt,同时action中必须有scan,这样就可以调用
scan(param),去访问当前网页下的文件flag.txt(如果直接访问是访问不到的,可能是flask框架的作用)
(2).但是如果只有scan是无法对文件内容进行显示的,只有action中包含有read时,才能进行显示文件内容:
read可以将写入flag.txt的内容的文件result.txt再次进行读取,并存如最终需要显示的result字典中:
所以action=scanread
(3).要触发上述内容需要绕过checkSign()的检测:md5(secert_key + param + action)=sign
5.最终payload的构造:
(1).secert_key:
由于flask框架中,网页显示(执行)的都是路由器的内容,所以对于网页来说,在路由器之前所定义的secert_key在网页显示后都是唯一的,即在网页初始化的时候对secert_key赋值,之后该全局变量都是不变的
(2).参数的基本情况:
1 2 3 4 param=flag.txt action=scanread secert_key=xxx sign=md5(xxxflag.txtscanread)
现在就是要获取md5(xxxflag.txtscanread)的值
(3)url/geneSign的利用:
该路由器会返回:md5(secert_key+param+scan)的值
所以如果我们令param=flag.txtread
则该路由器返回的就是md5(xxxflag.txtreadscan)
就可以活得sign的值: payload:
输出:
1 5927506259f0daa2cdfb35867203b343
所以此时sign=5927506259f0daa2cdfb35867203b343=xxxflag.txtreadscan
(3).最终的payload:
1 2 3 4 url/De1ta ?param=flag.txt Cookie:action=readscan;sign=5927506259f0daa2cdfb35867203b343
flag=flag{ceb51b96-bdcf-415a-bb56-da305f851028}