0x00
0ctf题目真的很强,萌新表示只能赛后复现writeup了。做完了misc两个签到题就跪了。正文还没有整理完毕,目前Ezdoor和Login的writeup思路已经写在了正文中,部分扩展和引用内容均已标注。
EzDoor
看起来很友好的一题,直接给了源码。
<?php
error_reporting(0);
$dir = 'sandbox/' . sha1($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
if(!file_exists($dir . "index.php")){
touch($dir . "index.php");
}
function clear($dir)
{
if(!is_dir($dir)){
unlink($dir);
return;
}
foreach (scandir($dir) as $file) {
if (in_array($file, [".", ".."])) {
continue;
}
unlink($dir . $file);
}
rmdir($dir);
}
switch ($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'phpinfo':
echo file_get_contents("phpinfo.txt");
break;
case 'reset':
clear($dir);
break;
case 'time':
echo time();
break;
case 'upload':
if (!isset($_GET["name"]) || !isset($_FILES['file'])) {
break;
}
if ($_FILES['file']['size'] > 100000) {
clear($dir);
break;
}
$name = $dir . $_GET["name"];
if (preg_match("/[^a-zA-Z0-9.\/]/", $name) ||
stristr(pathinfo($name)["extension"], "h")) {
break;
}
move_uploaded_file($_FILES['file']['tmp_name'], $name);
$size = 0;
foreach (scandir($dir) as $file) {
if (in_array($file, [".", ".."])) {
continue;
}
$size += filesize($dir . $file);
}
if ($size > 100000) {
clear($dir);
}
break;
case 'shell':
ini_set("open_basedir", "/var/www/html/$dir:/var/www/html/flag");
include $dir . "index.php";
break;
default:
highlight_file(__FILE__);
break;
}
对每个ip建立沙箱并初始化一个index.php,并且沙箱内页面无法直接访问。依次测试了各参数:phpinfo给出了部分信息,php7.0.28,opcache,session upload process,等等;上传功能对扩展名进行了判断,尝试使用最新的解析漏洞绕过失败;根据shell函数,标准思路因该是覆盖沙箱内index.php逻辑,从而getshell。
之后没了思路,上传一直没有效果,后来师傅说是opcache,找到了php7.0 opcache getshell,根据原文在给出的phpinfo中查看了几项要求,发现打开了时间戳校检,也给了如何绕过,利用time参数可以获取当前时间戳,利用clean可初始化index.php时间戳,基本的过程都有了参考。
基本条件已经满足,本地搭建同版本同目录测试环境生成bin文件上传覆盖,选择使用fpm的php花掉了很多时间去把环境运行起来,心态炸了。
覆盖时注意将服务器获取的时间戳写入本地生成的bin文件中。
scandir(/var/www/html/flag)返回
Array
(
[0] => .
[1] => ..
[2] => 93f4c28c0cf0b07dfd7012dca2cb868cc0228cad
)
读文件,拿到一个opcache文件,010打开后发现格式有问题,比较后添加一个00。
使用opcache_disassembler.py还原,可得opcode,还原出大概逻辑如下:
def encode(data):
return output.encode('hex')
def encrypt(flag,strings):
mt_srand(1337)
output = ""
for i in range(len(flag)):
output += chr(ord(flag[i])^ord(strings[i])^mt_rand(0,255))
return encode(output)
flag = raw_input('input_your_flag_here')
if encrypt(flag,this_is_a_very_secret_key)==="85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab":
print 'Congratulation! You got it!'
else:
'Wrong Answer'
解密即可。
cdxy给出了vld精准指令还原的过程。
Loginme
nodejs和mongodb结合,一度以为是mongodb盲注(x)
贴一下代码和注释
var express = require('express')
var app = express()
var bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({}));
var path = require("path");
var moment = require('moment');
var MongoClient = require('mongodb').MongoClient;
var url = "mongodb://localhost:27017/";
MongoClient.connect(url, function(err, db) { # 连接mongodb
if (err) throw err;
dbo = db.db("test_db");
var collection_name = "users";
var password_column = "password_"+Math.random().toString(36).slice(2) # 初始化字段名
var password = "XXXXXXXXXXXXXXXXXXXXXX";
// flag is flag{password}
var myobj = { "username": "admin", "last_access": moment().format('YYYY-MM-DD HH:mm:ss Z')};
myobj[password_column] = password;
dbo.collection(collection_name).remove({}); # 清空
dbo.collection(collection_name).update(
{ name: myobj.name },
myobj,
{ upsert: true } # 强制更新
);
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname,'index.html'));
})
app.post('/check', function (req, res) {
var check_function = 'if(this.username == #username# && #username# == "admin" && hex_md5(#password#) == this.'+password_column+'){\nreturn 1;\n}else{\nreturn 0;}';
for(var k in req.body){ # 对post参数进行检查
var valid = ['#','(',')'].every((x)=>{return req.body[k].indexOf(x) == -1}); # black list
if(!valid) res.send('Nope');
check_function = check_function.replace(
new RegExp('#'+k+'#','gm')
,JSON.stringify(req.body[k])) # 正则替换,字符串转换强行增加一对""
}
var query = {"$where" : check_function};
var newvalue = {$set : {last_access: moment().format('YYYY-MM-DD HH:mm:ss Z')}}
dbo.collection(collection_name).updateOne(query,newvalue,function (e,r){
if(e) throw e;
res.send('ok'); # 查询成功(无结果)返回ok
// ... implementing, plz dont release this.
});
})
app.listen(8081)
});
拿到题目后把源码看懂用了不少时间,逻辑分析清楚后把点放在了mongodb的盲注上,然而黑名单的限制导致延时函数无法注入,卡了很久之后队友告诉我是利用正则贪婪,然而最后也没有搞明白,赛后搜wp时也没看懂(菜),还好有一位师傅写的很具体,在这里我引用了他的wp,也结合了自己的一点分析。
源码中还有一点没分析到,当查询异常时回返回异常,这里也可以利用。
分析我们可控的位置,即req.body字典,更清晰来说是字典的keys和values(之前一直卡在keys限于username和password上了,一直没看懂师傅们的解题思路)。然后body插入进了正则和替换后的字符串中,我们考虑如何改变正则来控制查询条件。
首先自带的语句中包含了#界定符,我们可以直接使用|忽略掉。然后考虑如何构造。
即重点在... && hex_md5(#password#) == this.password_column)
如何更改。
如下三种思路:
... && #password# == this.password_column.substr( ... )){ ... // 无法替换出 ( ),放弃
... && #password# <= this.password_column){ ... // <= 左右会出现引号,无法处理,放弃
... && #password# <= this["password_column"]){ ... // <= 左右会出现引号,但可以用作 "password" 和 "password_column" 中的引号,可以尝试
// if(this.username == #username# && #username# == "admin" && hex_md5(#password#) == this.password_column){return 1;}else{return 0;}
// 一、替换用户名
username -> "admin"
// 结果: if(this.username == "admin" && "admin" == "admin" && hex_md5(#password#) == this.password_column){return 1;}else{return 0;}
// 二、替换密码判断部分(用到了正则中的断言)
(?=\){) -> "] +" # 这里做了更改,原文为{},可能是手误
== this. -> "<=this["
hex.*?rd.*?" -> "猜测的密码"
"(?=\)) -> ""
// 结果: if(this.username == "admin" && "admin" == "admin" && "猜测的密码" <=this["password_column"] +""){return 1;}else{return 0;}
// 三、将 return 0 换为会报错的 return a
0; -> "asuna"
asuna -> "+a+"
// 结果: if(this.username == "admin" && "admin" == "admin" && "猜测的密码" <=this["password_column"] +""){return 1;}else{return ""+a+""}
之后就是写脚本跑了
话说题目突然给了三个相同的环境...
refer:
https://coxxs.me/676
Easy User Manage System
Welcome to the Easy UMS. http://202.120.7.196:2333/
Since you don't like the waf, port is changed to 2333.
You need bind your phone with exactly 8.8.8.8 to get flag.
题目环境很简单,注册一个账户,填写用户名 密码 和 phone(使用ip即可),登录后服务器向ip发送一个包含token的head请求,将token填写在二次验证的页面,即可登录。
Listening on [0.0.0.0] (port 80)
Connection from 202.120.7.196 34302 received!
HEAD /?5d67ebdbc589ced5321bb4f76b734e33 HTTP/1.1
Host: xx
Accept: */*
登陆后如下:
跟进去
这里便是更改界面了
很明显不能够改为8.8.8.8,根据hint提示,改为8.8.8.8即可得到flag,我们尝试更改ip,发现在提交后有数秒的等待,之后再要求验证新的ip。
有师傅尝试了注入等方法,没有效果,但是有数(3)秒的等待很可能就是出题人在某个条件前故意加了sleep,很显然是判断有没有提交过token,没有就需要提交服务器发送的token。
那么我们想到了条件竞争,在三秒的延时中,完成另一个请求提交token的过程,改变另一个请求提交的ip。
在这里给出在阅读wp的过程中见到的测试过程。
- 登陆后准备两个页面(同一个session),A的ip填写自己的,B填写8.8.8.8,提交A的请求,拿到token,暂时不提交token;这时,提交B的请求,同时(三秒内)在A界面提交A的token。但是发现A界面提交后仍存在延时,之后A,B界面均需要填写token,可能是服务器对单个session的请求严格按照时间顺序执行了,A原有的token已经被B请求的token覆盖,所以A需要重新验证。
- 准备两个session的页面(同一个account),A的ip填写自己的,B填写8.8.8.8,提交A的请求,拿到token,暂时不提交token;这时,提交B的请求,同时(三秒内)在A界面提交A的token。这时,A页面更改成功,拿到flag。
refer:
https://coxxs.me/676
https://whit3hat.com/2018/04/05/0ps-ctf-easy-management-web/
h4x0rs.club 2
backend_www got backup at /var/www/html.tar.gz
Hint: Get open-redirect first, lead admin to the w0rld!
https://h4x0rs.club/game/
https://github.com/l4wio/CTF-challenges-by-me/tree/master/0ctf_quals-2018/h4x0rs.club
Blog
http://202.120.7.197:8090/
wp:
https://blog.cal1.cn/post/0CTF 2018 Quals Bl0g writeup